Spaces:
Running
Running
Restore Space to commit 36e2e7f8
#1
by bep40 - opened
This view is limited to 50 files because it contains too many changes. See the raw diff here.
- .gitignore +0 -2
- AUTO_CONTINUE_FEATURE.md +0 -183
- BUILD_TRIGGER +0 -1
- Dockerfile +43 -114
- README.md +317 -43
- backend/document_parser.py +0 -319
- backend/session_logger.py +0 -467
- configs/__init__.py +0 -0
- configs/cli_agent_config.json +22 -0
- configs/frontend_agent_config.json +5 -8
- fix_space_error_report.json +0 -14
- patch_agent_backend.py +0 -87
- patch_agent_loop.py +0 -74
- patch_apodex_ling_beatapi.py +0 -145
- patch_auto_continue.py +0 -129
- patch_auto_continue_v10.py +0 -202
- patch_auto_continue_v11.py +0 -185
- patch_auto_continue_v12.py +0 -209
- patch_auto_continue_v13.py +0 -197
- patch_auto_continue_v14.py +0 -166
- patch_auto_continue_v15.py +0 -167
- patch_auto_continue_v16.py +0 -142
- patch_auto_continue_v17.py +0 -187
- patch_auto_continue_v2.py +0 -307
- patch_auto_continue_v3.py +0 -229
- patch_auto_continue_v4.py +0 -264
- patch_auto_continue_v5.py +0 -252
- patch_auto_continue_v6.py +0 -243
- patch_auto_continue_v7.py +0 -255
- patch_auto_continue_v8.py +0 -239
- patch_auto_continue_v9.py +0 -227
- patch_beatapi_routing.py +0 -182
- patch_catch_up_context_restore.py +0 -188
- patch_catch_up_dataset_fallback.py +0 -102
- patch_catch_up_fix_backend.py +0 -220
- patch_catch_up_fix_frontend.py +0 -289
- patch_catch_up_fix_v2.py +0 -758
- patch_catch_up_fix_v3.py +0 -675
- patch_chat_input.py +0 -105
- patch_document_upload.py +0 -225
- patch_fix_static_404.py +0 -105
- patch_flint_free.py +0 -229
- patch_flint_free_frontend.py +0 -51
- patch_frontend.py +0 -199
- patch_frontend_apodex_ling_beatapi.py +0 -110
- patch_frontend_multi.py +0 -120
- patch_frontend_new_models.py +0 -71
- patch_frontend_permanent_sessions.py +0 -45
- patch_frontend_uploads.py +0 -81
- patch_frontend_xing_nemotron_vl.py +0 -63
.gitignore
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
__pycache__/
|
| 2 |
-
*.pyc
|
|
|
|
|
|
|
|
|
AUTO_CONTINUE_FEATURE.md
DELETED
|
@@ -1,183 +0,0 @@
|
|
| 1 |
-
# Auto-Continue Feature for Free Models
|
| 2 |
-
|
| 3 |
-
## Tổng quan
|
| 4 |
-
Khi model miễn phí dùng OpenRouter phản hồi bị dừng nhưng nhiệm vụ chưa hoàn thành, hệ thống sẽ:
|
| 5 |
-
1. Phát hiện task incomplete dựa vào trạng thái plan
|
| 6 |
-
2. Hiển thị 2 nút: "Tự động tiếp tục (10s)" và "Tạm dừng"
|
| 7 |
-
|
| 8 |
-
## Các file cần sửa
|
| 9 |
-
|
| 10 |
-
### 1. frontend/src/hooks/useAgentChat.ts
|
| 11 |
-
|
| 12 |
-
```typescript
|
| 13 |
-
// Thêm vào import
|
| 14 |
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
| 15 |
-
|
| 16 |
-
// Trong useAgentChat function, sau callbacksRef
|
| 17 |
-
const autoContinueTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
| 18 |
-
const [showAutoContinue, setShowAutoContinue] = useState(false);
|
| 19 |
-
const [taskIncompleteInfo, setTaskIncompleteInfo] = useState<{
|
| 20 |
-
incompletePlan: Array<{ id: string; content: string; status: string }>;
|
| 21 |
-
} | null>(null);
|
| 22 |
-
|
| 23 |
-
// Thêm vào SideChannelCallbacks interface
|
| 24 |
-
export interface SideChannelCallbacks {
|
| 25 |
-
// ... existing callbacks ...
|
| 26 |
-
onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
// Thêm vào sideChannel trong useMemo
|
| 30 |
-
onTaskIncomplete: (incompletePlan) => {
|
| 31 |
-
setTaskIncompleteInfo({ incompletePlan });
|
| 32 |
-
setShowAutoContinue(true);
|
| 33 |
-
if (autoContinueTimerRef.current) {
|
| 34 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 35 |
-
}
|
| 36 |
-
autoContinueTimerRef.current = setTimeout(() => {
|
| 37 |
-
startAutoContinue();
|
| 38 |
-
}, 10000);
|
| 39 |
-
},
|
| 40 |
-
|
| 41 |
-
// Thêm hàm mới
|
| 42 |
-
const startAutoContinue = useCallback(() => {
|
| 43 |
-
setShowAutoContinue(false);
|
| 44 |
-
setTaskIncompleteInfo(null);
|
| 45 |
-
if (autoContinueTimerRef.current) {
|
| 46 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 47 |
-
autoContinueTimerRef.current = null;
|
| 48 |
-
}
|
| 49 |
-
const incompleteItems = taskIncompleteInfo?.incompletePlan || [];
|
| 50 |
-
const continuationText = incompleteItems.length > 0
|
| 51 |
-
? `Tiếp tục từ các task chưa hoàn thành: ${incompleteItems.map(i => `- ${i.content}`).join('\n')}`
|
| 52 |
-
: 'Tiếp tục nhiệm vụ.';
|
| 53 |
-
chat.sendMessage({
|
| 54 |
-
text: `[TỰ ĐỘNG TIẾP TỤC] ${continuationText}`,
|
| 55 |
-
metadata: { createdAt: new Date().toISOString() }
|
| 56 |
-
});
|
| 57 |
-
}, [taskIncompleteInfo, chat]);
|
| 58 |
-
|
| 59 |
-
const cancelAutoContinue = useCallback(() => {
|
| 60 |
-
setShowAutoContinue(false);
|
| 61 |
-
setTaskIncompleteInfo(null);
|
| 62 |
-
if (autoContinueTimerRef.current) {
|
| 63 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 64 |
-
autoContinueTimerRef.current = null;
|
| 65 |
-
}
|
| 66 |
-
}, []);
|
| 67 |
-
|
| 68 |
-
// Thêm vào return
|
| 69 |
-
return {
|
| 70 |
-
messages: chat.messages,
|
| 71 |
-
sendMessage: chat.sendMessage,
|
| 72 |
-
stop,
|
| 73 |
-
status: chat.status,
|
| 74 |
-
undoLastTurn,
|
| 75 |
-
editAndRegenerate,
|
| 76 |
-
approveTools,
|
| 77 |
-
refreshMessages,
|
| 78 |
-
showAutoContinue,
|
| 79 |
-
taskIncompleteInfo,
|
| 80 |
-
startAutoContinue,
|
| 81 |
-
cancelAutoContinue,
|
| 82 |
-
};
|
| 83 |
-
```
|
| 84 |
-
|
| 85 |
-
### 2. frontend/src/components/Chat/ChatInput.tsx
|
| 86 |
-
|
| 87 |
-
```typescript
|
| 88 |
-
// Thêm import
|
| 89 |
-
import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';
|
| 90 |
-
|
| 91 |
-
// Thêm props
|
| 92 |
-
interface ChatInputProps {
|
| 93 |
-
// ... existing props
|
| 94 |
-
showAutoContinue?: boolean;
|
| 95 |
-
taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
|
| 96 |
-
startAutoContinue?: () => void;
|
| 97 |
-
cancelAutoContinue?: () => void;
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
// Thêm UI buttons (trước hoặc sau input area)
|
| 101 |
-
{showAutoContinue && taskIncompleteInfo && (
|
| 102 |
-
<Stack direction="row" spacing={1} sx={{ mb: 1, px: 2 }}>
|
| 103 |
-
<Button
|
| 104 |
-
variant="contained"
|
| 105 |
-
color="primary"
|
| 106 |
-
size="small"
|
| 107 |
-
onClick={startAutoContinue}
|
| 108 |
-
sx={{ textTransform: 'none' }}
|
| 109 |
-
>
|
| 110 |
-
Tự động tiếp tục (10s)
|
| 111 |
-
</Button>
|
| 112 |
-
<Button
|
| 113 |
-
variant="outlined"
|
| 114 |
-
color="secondary"
|
| 115 |
-
size="small"
|
| 116 |
-
onClick={cancelAutoContinue}
|
| 117 |
-
sx={{ textTransform: 'none' }}
|
| 118 |
-
>
|
| 119 |
-
Tạm dừng
|
| 120 |
-
</Button>
|
| 121 |
-
</Stack>
|
| 122 |
-
)}
|
| 123 |
-
```
|
| 124 |
-
|
| 125 |
-
### 3. frontend/src/lib/sse-chat-transport.ts
|
| 126 |
-
|
| 127 |
-
```typescript
|
| 128 |
-
// Thêm vào case trong createEventToChunkStream
|
| 129 |
-
case 'task_incomplete':
|
| 130 |
-
sideChannel.onTaskIncomplete(
|
| 131 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 132 |
-
);
|
| 133 |
-
break;
|
| 134 |
-
```
|
| 135 |
-
|
| 136 |
-
### 4. agent/core/agent_loop.py
|
| 137 |
-
|
| 138 |
-
```python
|
| 139 |
-
# Trong run_agent, sau khi xử lý LLM response
|
| 140 |
-
def _check_task_incomplete(session: Session, llm_result: LLMResult) -> bool:
|
| 141 |
-
"""Kiểm tra nếu model dừng nhưng task chưa hoàn thành."""
|
| 142 |
-
if llm_result.tool_calls_acc:
|
| 143 |
-
return False # Có tool calls thì chưa incomplete
|
| 144 |
-
|
| 145 |
-
plan = getattr(session, "current_plan", None) or []
|
| 146 |
-
unfinished = [item for item in plan if item.get("status") in ("pending", "in_progress")]
|
| 147 |
-
|
| 148 |
-
return len(unfinished) > 0
|
| 149 |
-
|
| 150 |
-
# Gọi sau khi nhận kết quả LLM:
|
| 151 |
-
if _check_task_incomplete(session, llm_result):
|
| 152 |
-
unfinished = [item for item in plan if item.get("status") in ("pending", "in_progress")]
|
| 153 |
-
await session.send_event(
|
| 154 |
-
Event(
|
| 155 |
-
event_type="task_incomplete",
|
| 156 |
-
data={
|
| 157 |
-
"incomplete_plan": unfinished,
|
| 158 |
-
"message": "Model stopped streaming but task is incomplete."
|
| 159 |
-
}
|
| 160 |
-
)
|
| 161 |
-
)
|
| 162 |
-
```
|
| 163 |
-
|
| 164 |
-
## Logic hoạt động
|
| 165 |
-
|
| 166 |
-
1. Khi agent loop phát hiện model trả lời không có tool calls nhưng plan còn incomplete
|
| 167 |
-
2. Backend gửi event `task_incomplete` với danh sách task chưa hoàn thành
|
| 168 |
-
3. Frontend nhận event qua `onTaskIncomplete` callback
|
| 169 |
-
4. Hiển thị 2 nút:
|
| 170 |
-
- **Tự động tiếp tục (10s)**: Sau 10 giây sẽ tự động gửi tin nhắn tiếp tục
|
| 171 |
-
- **Tạm dừng**: Hủy timer, cho phép user nhập nội dung mới
|
| 172 |
-
|
| 173 |
-
## Áp dụng patch
|
| 174 |
-
|
| 175 |
-
```bash
|
| 176 |
-
# Tải source
|
| 177 |
-
git clone https://huggingface.co/spaces/smolagents/ml-intern /tmp/source
|
| 178 |
-
|
| 179 |
-
# Sửa các file trên
|
| 180 |
-
# Sau đó rebuild frontend
|
| 181 |
-
cd /tmp/source/frontend
|
| 182 |
-
npm install && npm run build
|
| 183 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BUILD_TRIGGER
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
v5-add-recursive-flint-v1.0-free-reasoning-fix-openrouter
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,131 +1,60 @@
|
|
|
|
|
| 1 |
FROM node:20-alpine AS frontend-builder
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
RUN python3 /tmp/patch_frontend.py || true
|
| 8 |
-
COPY patch_space_bunny.py /tmp/patch_space_bunny.py
|
| 9 |
-
RUN python3 /tmp/patch_space_bunny.py || true
|
| 10 |
-
COPY patch_frontend_uploads.py /tmp/patch_frontend_uploads.py
|
| 11 |
-
RUN python3 /tmp/patch_frontend_uploads.py || true
|
| 12 |
-
COPY patch_frontend_multi.py /tmp/patch_frontend_multi.py
|
| 13 |
-
RUN python3 /tmp/patch_frontend_multi.py || true
|
| 14 |
-
COPY patch_frontend_new_models.py /tmp/patch_frontend_new_models.py
|
| 15 |
-
RUN python3 /tmp/patch_frontend_new_models.py || true
|
| 16 |
-
COPY patch_frontend_apodex_ling_beatapi.py /tmp/patch_frontend_apodex_ling_beatapi.py
|
| 17 |
-
RUN python3 /tmp/patch_frontend_apodex_ling_beatapi.py || true
|
| 18 |
-
COPY patch_frontend_xing_nemotron_vl.py /tmp/patch_frontend_xing_nemotron_vl.py
|
| 19 |
-
RUN python3 /tmp/patch_frontend_xing_nemotron_vl.py || true
|
| 20 |
-
COPY patch_flint_free_frontend.py /tmp/patch_flint_free_frontend.py
|
| 21 |
-
RUN python3 /tmp/patch_flint_free_frontend.py || true
|
| 22 |
-
COPY patch_catch_up_dataset_fallback.py /tmp/patch_catch_up_dataset_fallback.py
|
| 23 |
-
RUN python3 /tmp/patch_catch_up_dataset_fallback.py || true
|
| 24 |
-
# Run auto_continue patch BEFORE our frontend fix, since it also patches useAgentChat.ts
|
| 25 |
-
COPY patch_auto_continue_v17.py /tmp/patch_auto_continue_v17.py
|
| 26 |
-
RUN python3 /tmp/patch_auto_continue_v17.py || true
|
| 27 |
-
# Run our frontend catch-up fix AFTER all other frontend patches
|
| 28 |
-
COPY patch_catch_up_fix_frontend.py /tmp/patch_catch_up_fix_frontend.py
|
| 29 |
-
RUN python3 /tmp/patch_catch_up_fix_frontend.py || true
|
| 30 |
-
# v2: comprehensive catch-up fix + shareable URLs (frontend)
|
| 31 |
-
COPY patch_catch_up_fix_v2.py /tmp/patch_catch_up_fix_v2.py
|
| 32 |
-
RUN python3 /tmp/patch_catch_up_fix_v2.py || true
|
| 33 |
-
# v3: FIX THE BUILD — v2 left dead imports (_catchupDone, ShareIcon) and an
|
| 34 |
-
# untyped param that fail `npm run build` -> empty /app/static -> NO frontend.
|
| 35 |
-
# This MUST run after v2 so it can complete v2's half-applied patches.
|
| 36 |
-
COPY patch_catch_up_fix_v3.py /tmp/patch_catch_up_fix_v3.py
|
| 37 |
-
RUN python3 /tmp/patch_catch_up_fix_v3.py || true
|
| 38 |
-
|
| 39 |
-
WORKDIR /source/frontend
|
| 40 |
-
RUN npm config set fetch-timeout 120000 && npm config set fetch-retries 3 && npm install && npm run build || echo "FRONTEND BUILD FAILED"
|
| 41 |
-
# Guarantee the dist dir exists so COPY --from below never fails, even if npm failed.
|
| 42 |
-
RUN mkdir -p /source/frontend/dist
|
| 43 |
|
|
|
|
| 44 |
FROM python:3.12-slim
|
| 45 |
-
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
| 46 |
-
RUN useradd -m -u 1000 user
|
| 47 |
-
WORKDIR /app
|
| 48 |
-
RUN apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates tesseract-ocr tesseract-ocr-eng tesseract-ocr-vie libreoffice-writer libreoffice-calc libreoffice-impress && rm -rf /var/lib/apt/lists/*
|
| 49 |
-
|
| 50 |
-
RUN git clone --depth 1 https://huggingface.co/spaces/smolagents/ml-intern /tmp/source && cp /tmp/source/pyproject.toml /tmp/source/uv.lock ./ && cp -r /tmp/source/agent ./agent && cp -r /tmp/source/backend ./backend && (cp /tmp/source/backend/document_parser.py ./backend/document_parser.py 2>/dev/null || true) && cp -r /tmp/source/configs ./configs && cp -r /tmp/source/scripts ./scripts && rm -rf /tmp/source
|
| 51 |
-
|
| 52 |
-
COPY scripts/build_kpis.py /tmp/build_kpis_patched.py
|
| 53 |
-
RUN cp /tmp/build_kpis_patched.py /app/scripts/build_kpis.py && python3 -c "import ast; ast.parse(open('/app/scripts/build_kpis.py').read())" || echo "KP FAILED"
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
RUN uv sync --no-dev --frozen || echo "UV SYNC FAILED"
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
COPY patch_models.py /tmp/patch_models.py
|
| 63 |
-
RUN python /tmp/patch_models.py || true
|
| 64 |
-
COPY patch_new_models.py /tmp/patch_new_models.py
|
| 65 |
-
RUN python /tmp/patch_new_models.py || true
|
| 66 |
-
COPY patch_apodex_ling_beatapi.py /tmp/patch_apodex_ling_beatapi.py
|
| 67 |
-
RUN python3 /tmp/patch_apodex_ling_beatapi.py || true
|
| 68 |
-
COPY patch_xing_nemotron_vl.py /tmp/patch_xing_nemotron_vl.py
|
| 69 |
-
RUN python3 /tmp/patch_xing_nemotron_vl.py || true
|
| 70 |
-
COPY patch_flint_free.py /tmp/patch_flint_free.py
|
| 71 |
-
RUN python3 /tmp/patch_flint_free.py || true
|
| 72 |
-
COPY patch_openrouter_fix.py /tmp/patch_openrouter_fix.py
|
| 73 |
-
RUN python3 /tmp/patch_openrouter_fix.py || true
|
| 74 |
-
COPY patch_beatapi_routing.py /tmp/patch_beatapi_routing.py
|
| 75 |
-
RUN python3 /tmp/patch_beatapi_routing.py || true
|
| 76 |
-
COPY patch_agent_backend.py /tmp/patch_agent_backend.py
|
| 77 |
-
RUN python3 /tmp/patch_agent_backend.py || true
|
| 78 |
-
COPY patch_document_upload.py /tmp/patch_document_upload.py
|
| 79 |
-
RUN python3 /tmp/patch_document_upload.py || true
|
| 80 |
-
COPY patch_multi_upload.py /tmp/patch_multi_upload.py
|
| 81 |
-
RUN python3 /tmp/patch_multi_upload.py || true
|
| 82 |
-
COPY patch_permanent_session_logging.py /tmp/patch_permanent_session_logging.py
|
| 83 |
-
RUN python3 /tmp/patch_permanent_session_logging.py || true
|
| 84 |
-
COPY patch_catch_up_fix_backend.py /tmp/patch_catch_up_fix_backend.py
|
| 85 |
-
RUN python3 /tmp/patch_catch_up_fix_backend.py || true
|
| 86 |
-
# v2: comprehensive catch-up fix + shareable URLs (backend — runs after all other backend patches)
|
| 87 |
-
COPY patch_catch_up_fix_v2.py /tmp/patch_catch_up_fix_v2.py
|
| 88 |
-
RUN python3 /tmp/patch_catch_up_fix_v2.py || true
|
| 89 |
-
# v3 backend: fix the event-row filter in session_logger.py that v2 broke
|
| 90 |
-
# (wrong indent -> SyntaxError -> filter never applied -> catch-up errors).
|
| 91 |
-
COPY patch_catch_up_fix_v3.py /tmp/patch_catch_up_fix_v3.py
|
| 92 |
-
RUN python3 /tmp/patch_catch_up_fix_v3.py || true
|
| 93 |
|
| 94 |
-
|
| 95 |
-
# Root cause: mkdir -p /app/static creates an empty dir; main.py mounts
|
| 96 |
-
# StaticFiles on the empty dir, shadowing ALL API routes with 404s.
|
| 97 |
-
COPY patch_fix_static_404.py /tmp/patch_fix_static_404.py
|
| 98 |
-
RUN python3 /tmp/patch_fix_static_404.py || echo "STATIC FIX PATCH WARN (BUILD CONTINUES)"
|
| 99 |
|
| 100 |
-
#
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
#
|
| 106 |
-
|
| 107 |
-
# NoopSessionStore never restores sessions; (2) context lost after catch-up
|
| 108 |
-
# because seed_from_summary only used a lossy summary. This patch makes the
|
| 109 |
-
# backend reload full conversations from the permanent sessions dataset.
|
| 110 |
-
COPY patch_catch_up_context_restore.py /tmp/patch_catch_up_context_restore.py
|
| 111 |
-
RUN python3 /tmp/patch_catch_up_context_restore.py || echo "CONTEXT RESTORE PATCH WARN (BUILD CONTINUES)"
|
| 112 |
-
RUN python3 -c "import ast; ast.parse(open('/app/backend/session_manager.py').read()); print('session_manager.py SYNTAX OK')" || echo "SESSION MANAGER SYNTAX CHECK WARN"
|
| 113 |
|
| 114 |
-
#
|
| 115 |
-
#
|
| 116 |
-
|
| 117 |
-
RUN echo "Static dir ready: $(ls /app/static 2>/dev/null | wc -l) files"
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
|
|
|
|
| 123 |
|
| 124 |
-
|
|
|
|
|
|
|
| 125 |
|
|
|
|
| 126 |
USER user
|
| 127 |
-
ENV HOME=/home/user PYTHONUNBUFFERED=1 PYTHONPATH=/app PATH="/app/.venv/bin:$PATH"
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
EXPOSE 7860
|
|
|
|
|
|
|
| 130 |
WORKDIR /app/backend
|
| 131 |
-
CMD ["bash", "start.sh"]
|
|
|
|
| 1 |
+
# Stage 1: Build frontend
|
| 2 |
FROM node:20-alpine AS frontend-builder
|
| 3 |
+
WORKDIR /app/frontend
|
| 4 |
+
COPY frontend/package.json frontend/package-lock.json ./
|
| 5 |
+
RUN npm install
|
| 6 |
+
COPY frontend/ ./
|
| 7 |
+
RUN npm run build
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
# Stage 2: Production
|
| 10 |
FROM python:3.12-slim
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
# Install uv directly from official image
|
| 13 |
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
# Create user with UID 1000 (required for HF Spaces)
|
| 16 |
+
RUN useradd -m -u 1000 user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
WORKDIR /app
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
# Install system dependencies
|
| 21 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 22 |
+
git \
|
| 23 |
+
curl \
|
| 24 |
+
bash \
|
| 25 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 26 |
|
| 27 |
+
# Copy dependency files
|
| 28 |
+
COPY pyproject.toml uv.lock ./
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
+
# Install dependencies into /app/.venv
|
| 31 |
+
# Use --frozen to ensure exact versions from uv.lock
|
| 32 |
+
RUN uv sync --no-dev --frozen
|
|
|
|
| 33 |
|
| 34 |
+
# Copy application code
|
| 35 |
+
COPY agent/ ./agent/
|
| 36 |
+
COPY backend/ ./backend/
|
| 37 |
+
COPY configs/ ./configs/
|
| 38 |
|
| 39 |
+
# Copy built frontend
|
| 40 |
+
COPY --from=frontend-builder /app/frontend/dist ./static/
|
| 41 |
|
| 42 |
+
# Create directories and set ownership
|
| 43 |
+
RUN mkdir -p /app/session_logs && \
|
| 44 |
+
chown -R user:user /app
|
| 45 |
|
| 46 |
+
# Switch to non-root user
|
| 47 |
USER user
|
|
|
|
| 48 |
|
| 49 |
+
# Set environment
|
| 50 |
+
ENV HOME=/home/user \
|
| 51 |
+
PYTHONUNBUFFERED=1 \
|
| 52 |
+
PYTHONPATH=/app \
|
| 53 |
+
PATH="/app/.venv/bin:$PATH"
|
| 54 |
+
|
| 55 |
+
# Expose port
|
| 56 |
EXPOSE 7860
|
| 57 |
+
|
| 58 |
+
# Run the application from backend directory
|
| 59 |
WORKDIR /app/backend
|
| 60 |
+
CMD ["bash", "start.sh"]
|
README.md
CHANGED
|
@@ -1,66 +1,340 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🤖
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
| 8 |
-
short_description: ML Intern V2 (FREE & SAVE MODEL + PERMANENT SESSION LOGGING)
|
| 9 |
hf_oauth: true
|
| 10 |
hf_oauth_expiration_minutes: 43200
|
| 11 |
hf_oauth_scopes:
|
| 12 |
-
- read-repos
|
| 13 |
-
- write-repos
|
| 14 |
-
- contribute-repos
|
| 15 |
-
- manage-repos
|
| 16 |
-
- write-collections
|
| 17 |
-
- inference-api
|
| 18 |
-
- jobs
|
| 19 |
-
- write-discussions
|
| 20 |
-
tags:
|
| 21 |
-
- ml-intern
|
| 22 |
-
- permanent-session-logging
|
| 23 |
-
- persistent-context
|
| 24 |
---
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
#
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
-
|
| 33 |
-
- Không bao giờ mất dữ liệu, ngay cả khi Space restart/crash
|
| 34 |
-
- Lưu vào `bep40/ml-intern-sessions` (dataset chung) + `{user}/ml-intern-sessions` (cá nhân)
|
| 35 |
-
- Mỗi model (dù là model nào) đều được lưu session vĩnh viễn
|
| 36 |
-
- **Không có thông báo "where we here now?"** — giảm thiểu thông báo không cần thiết
|
| 37 |
|
| 38 |
-
###
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
| 45 |
|
| 46 |
-
###
|
| 47 |
|
| 48 |
-
|
| 49 |
-
-
|
| 50 |
-
|
| 51 |
|
| 52 |
-
|
| 53 |
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
|
| 57 |
-
- OpenRouter models: Tencent HY3, Gemma 4 31B, Llama 3.3 70B, DeepSeek V4, Laguna S 2.1
|
| 58 |
-
- BeatAPI: `beatapi/space-bunny-alpha` (provider riêng, cần BEATAPI_API_KEY)
|
| 59 |
-
- Cục bản chất, mọi model đều được lưu session vĩnh viễn
|
| 60 |
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
-
|
| 64 |
-
2. Chat bình thường
|
| 65 |
-
3. Session được lưu vĩnh viễn vào dataset
|
| 66 |
-
4. Sau Space restart, frontend tự động restore từ dataset
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ML Intern
|
| 3 |
emoji: 🤖
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
|
|
|
| 8 |
hf_oauth: true
|
| 9 |
hf_oauth_expiration_minutes: 43200
|
| 10 |
hf_oauth_scopes:
|
| 11 |
+
- read-repos
|
| 12 |
+
- write-repos
|
| 13 |
+
- contribute-repos
|
| 14 |
+
- manage-repos
|
| 15 |
+
- write-collections
|
| 16 |
+
- inference-api
|
| 17 |
+
- jobs
|
| 18 |
+
- write-discussions
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
---
|
| 20 |
|
| 21 |
+
<p align="center">
|
| 22 |
+
<img src="frontend/public/smolagents.webp" alt="smolagents logo" width="160" />
|
| 23 |
+
</p>
|
| 24 |
|
| 25 |
+
# ML Intern
|
| 26 |
|
| 27 |
+
An ML intern that autonomously researches, writes, and ships good quality ML related code using the Hugging Face ecosystem — with deep access to docs, papers, datasets, and cloud compute.
|
| 28 |
|
| 29 |
+
## Quick Start
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
### Installation
|
| 32 |
|
| 33 |
+
```bash
|
| 34 |
+
git clone git@github.com:huggingface/ml-intern.git
|
| 35 |
+
cd ml-intern
|
| 36 |
+
uv sync
|
| 37 |
+
uv tool install -e .
|
| 38 |
+
```
|
| 39 |
|
| 40 |
+
#### That's it. Now `ml-intern` works from any directory:
|
| 41 |
|
| 42 |
+
```bash
|
| 43 |
+
ml-intern
|
| 44 |
+
```
|
| 45 |
|
| 46 |
+
Create a `.env` file in the project root (or export these in your shell):
|
| 47 |
|
| 48 |
+
```bash
|
| 49 |
+
ANTHROPIC_API_KEY=<your-anthropic-api-key> # if using anthropic models
|
| 50 |
+
OPENAI_API_KEY=<your-openai-api-key> # if using openai models
|
| 51 |
+
HF_TOKEN=<your-hugging-face-token>
|
| 52 |
+
GITHUB_TOKEN=<github-personal-access-token>
|
| 53 |
+
```
|
| 54 |
+
If no `HF_TOKEN` is set, the CLI will prompt you to paste one on first launch. To get a GITHUB_TOKEN follow the tutorial [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token).
|
| 55 |
|
| 56 |
+
### Usage
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
+
**Interactive mode** (start a chat session):
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
ml-intern
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
**Headless mode** (single prompt, auto-approve):
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
ml-intern "fine-tune llama on my dataset"
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
**Options:**
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
ml-intern --model anthropic/claude-opus-4-6 "your prompt"
|
| 74 |
+
ml-intern --model openai/gpt-5.5 "your prompt"
|
| 75 |
+
ml-intern --max-iterations 100 "your prompt"
|
| 76 |
+
ml-intern --no-stream "your prompt"
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
## Sharing Traces
|
| 80 |
+
|
| 81 |
+
Every session is auto-uploaded to your **own private Hugging Face dataset**
|
| 82 |
+
in [Claude Code JSONL format](https://huggingface.co/changelog/agent-trace-viewer),
|
| 83 |
+
which the HF Agent Trace Viewer auto-detects so you can browse turns, tool
|
| 84 |
+
calls, and model responses directly on the Hub.
|
| 85 |
+
|
| 86 |
+
By default the dataset is named `{your-hf-username}/ml-intern-sessions` and is
|
| 87 |
+
**created private**. You can flip it to public from inside the CLI:
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
/share-traces # show current visibility + dataset URL
|
| 91 |
+
/share-traces public # publish (anyone can view)
|
| 92 |
+
/share-traces private # lock it back down
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
You can also flip visibility from the dataset page on huggingface.co — the
|
| 96 |
+
agent honours whatever you set there for subsequent uploads.
|
| 97 |
+
|
| 98 |
+
To opt out entirely, set in your CLI config (e.g. `configs/cli_agent_config.json`
|
| 99 |
+
or `~/.config/ml-intern/cli_agent_config.json`):
|
| 100 |
+
|
| 101 |
+
```json
|
| 102 |
+
{ "share_traces": false }
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
To override the destination repo, set:
|
| 106 |
+
|
| 107 |
+
```json
|
| 108 |
+
{ "personal_trace_repo_template": "{hf_user}/my-custom-traces" }
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
The shared `smolagents/ml-intern-sessions` dataset is unrelated and only
|
| 112 |
+
receives anonymized telemetry rows used by the backend KPI scheduler.
|
| 113 |
+
|
| 114 |
+
## Supported Gateways
|
| 115 |
+
|
| 116 |
+
ML Intern currently supports one-way notification gateways from CLI sessions.
|
| 117 |
+
These gateways send out-of-band status updates; they do not accept inbound chat
|
| 118 |
+
messages.
|
| 119 |
+
|
| 120 |
+
### Slack
|
| 121 |
+
|
| 122 |
+
Slack notifications use the Slack Web API to post messages when the agent needs
|
| 123 |
+
approval, hits an error, or completes a turn. Create a Slack app with a bot token
|
| 124 |
+
that has `chat:write`, invite the bot to the target channel, then set:
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
SLACK_BOT_TOKEN=xoxb-...
|
| 128 |
+
SLACK_CHANNEL_ID=C...
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
The CLI automatically creates a `slack.default` destination when both variables
|
| 132 |
+
are present. Optional environment variables for the env-only default:
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
ML_INTERN_SLACK_NOTIFICATIONS=false
|
| 136 |
+
ML_INTERN_SLACK_DESTINATION=slack.ops
|
| 137 |
+
ML_INTERN_SLACK_AUTO_EVENTS=approval_required,error,turn_complete
|
| 138 |
+
ML_INTERN_SLACK_ALLOW_AGENT_TOOL=true
|
| 139 |
+
ML_INTERN_SLACK_ALLOW_AUTO_EVENTS=true
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
For a persistent user-level config, put overrides in
|
| 143 |
+
`~/.config/ml-intern/cli_agent_config.json` or point `ML_INTERN_CLI_CONFIG` at a
|
| 144 |
+
JSON file:
|
| 145 |
+
|
| 146 |
+
```json
|
| 147 |
+
{
|
| 148 |
+
"messaging": {
|
| 149 |
+
"enabled": true,
|
| 150 |
+
"auto_event_types": ["approval_required", "error", "turn_complete"],
|
| 151 |
+
"destinations": {
|
| 152 |
+
"slack.ops": {
|
| 153 |
+
"provider": "slack",
|
| 154 |
+
"token": "${SLACK_BOT_TOKEN}",
|
| 155 |
+
"channel": "${SLACK_CHANNEL_ID}",
|
| 156 |
+
"allow_agent_tool": true,
|
| 157 |
+
"allow_auto_events": true
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
## Architecture
|
| 165 |
+
|
| 166 |
+
### Component Overview
|
| 167 |
+
|
| 168 |
+
```
|
| 169 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 170 |
+
│ User/CLI │
|
| 171 |
+
└────────────┬─────────────────────────────────────┬──────────┘
|
| 172 |
+
│ Operations │ Events
|
| 173 |
+
↓ (user_input, exec_approval, ↑
|
| 174 |
+
submission_queue interrupt, compact, ...) event_queue
|
| 175 |
+
│ │
|
| 176 |
+
↓ │
|
| 177 |
+
┌────────────────────────────────────────────────────┐ │
|
| 178 |
+
│ submission_loop (agent_loop.py) │ │
|
| 179 |
+
│ ┌──────────────────────────────────────────────┐ │ │
|
| 180 |
+
│ │ 1. Receive Operation from queue │ │ │
|
| 181 |
+
│ │ 2. Route to handler (run_agent/compact/...) │ │ │
|
| 182 |
+
│ └──────────────────────────────────────────────┘ │ │ │
|
| 183 |
+
│ ↓ │ │
|
| 184 |
+
│ ┌──────────────────────────────────────────────┐ │ │
|
| 185 |
+
│ │ Handlers.run_agent() │ ├──┤
|
| 186 |
+
│ │ │ │ │
|
| 187 |
+
│ │ ┌────────────────────────────────────────┐ │ │ │
|
| 188 |
+
│ │ │ Agentic Loop (max 300 iterations) │ │ │ │
|
| 189 |
+
│ │ │ │ │ │ │
|
| 190 |
+
│ │ │ ┌──────────────────────────────────┐ │ │ │ │
|
| 191 |
+
│ │ │ │ Session │ │ │ │ │
|
| 192 |
+
│ │ │ │ ┌────────────────────────────┐ │ │ │ │ │
|
| 193 |
+
│ │ │ │ │ ContextManager │ │ │ │ │ │
|
| 194 |
+
│ │ │ │ │ • Message history │ │ │ │ │ │
|
| 195 |
+
│ │ │ │ │ (litellm.Message[]) │ │ │ │ │ │
|
| 196 |
+
│ │ │ │ │ • Auto-compaction (170k) │ │ │ │ │ │
|
| 197 |
+
│ │ │ │ │ • Session upload to HF │ │ │ │ │ │
|
| 198 |
+
│ │ │ │ └────────────────────────────┘ │ │ │ │ │
|
| 199 |
+
│ │ │ │ │ │ │ │ │
|
| 200 |
+
│ │ │ │ ┌────────────────────────────┐ │ │ │ │ │
|
| 201 |
+
│ │ │ │ │ ToolRouter │ │ │ │ │ │
|
| 202 |
+
│ │ │ │ │ ├─ HF docs & research │ │ │ │ │ │
|
| 203 |
+
│ │ │ │ │ ├─ HF repos, datasets, │ │ │ │ │ │
|
| 204 |
+
│ │ │ │ │ │ jobs, papers │ │ │ │ │ │
|
| 205 |
+
│ │ │ │ │ ├─ GitHub code search │ │ │ │ │ │
|
| 206 |
+
│ │ │ │ │ ├─ Sandbox & local tools │ │ │ │ │ │
|
| 207 |
+
│ │ │ │ │ ├─ Planning │ │ │ │ │ │
|
| 208 |
+
│ │ │ │ │ └─ MCP server tools │ │ │ │ │ │
|
| 209 |
+
│ │ │ │ └────────────────────────────┘ │ │ │ │ │
|
| 210 |
+
│ │ │ └──────────────────────────────────┘ │ │ │ │
|
| 211 |
+
│ │ │ │ │ │ │
|
| 212 |
+
│ │ │ ┌──────────────────────────────────┐ │ │ │ │
|
| 213 |
+
│ │ │ │ Doom Loop Detector │ │ │ │ │
|
| 214 |
+
│ │ │ │ • Detects repeated tool patterns │ │ │ │ │
|
| 215 |
+
│ │ │ │ • Injects corrective prompts │ │ │ │ │
|
| 216 |
+
│ │ │ └──────────────────────────────────┘ │ │ │ │
|
| 217 |
+
│ │ │ │ │ │ │
|
| 218 |
+
│ │ │ Loop: │ │ │ │
|
| 219 |
+
│ │ │ 1. LLM call (litellm.acompletion) │ │ │ │
|
| 220 |
+
│ │ │ ↓ │ │ │ │
|
| 221 |
+
│ │ │ 2. Parse tool_calls[] │ │ │ │
|
| 222 |
+
│ │ │ ↓ │ │ │ │
|
| 223 |
+
│ │ │ 3. Approval check │ │ │ │
|
| 224 |
+
│ │ │ (jobs, sandbox, destructive ops) │ │ │ │
|
| 225 |
+
│ │ │ ↓ │ │ │ │
|
| 226 |
+
│ │ │ 4. Execute via ToolRouter │ │ │ │
|
| 227 |
+
│ │ │ ↓ │ │ │ │
|
| 228 |
+
│ │ │ 5. Add results to ContextManager │ │ │ │
|
| 229 |
+
│ │ │ ↓ │ │ │ │
|
| 230 |
+
│ │ │ 6. Repeat if tool_calls exist │ │ │ │
|
| 231 |
+
│ │ └────────────────────────────────────────┘ │ │ │
|
| 232 |
+
│ └──────────────────────────────────────────────┘ │ │
|
| 233 |
+
└────────────────────────────────────────────────────┴──┘
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
### Agentic Loop Flow
|
| 237 |
+
|
| 238 |
+
```
|
| 239 |
+
User Message
|
| 240 |
+
↓
|
| 241 |
+
[Add to ContextManager]
|
| 242 |
+
↓
|
| 243 |
+
╔═══════════════════════════════════════════╗
|
| 244 |
+
║ Iteration Loop (max 300) ║
|
| 245 |
+
║ ║
|
| 246 |
+
║ Get messages + tool specs ║
|
| 247 |
+
║ ↓ ║
|
| 248 |
+
║ litellm.acompletion() ║
|
| 249 |
+
║ ↓ ║
|
| 250 |
+
║ Has tool_calls? ──No──> Done ║
|
| 251 |
+
║ │ ║
|
| 252 |
+
║ Yes ║
|
| 253 |
+
║ ↓ ║
|
| 254 |
+
║ Add assistant msg (with tool_calls) ║
|
| 255 |
+
║ ↓ ║
|
| 256 |
+
║ Doom loop check ║
|
| 257 |
+
║ ↓ ║
|
| 258 |
+
║ For each tool_call: ║
|
| 259 |
+
║ • Needs approval? ──Yes──> Wait for ║
|
| 260 |
+
║ │ user confirm ║
|
| 261 |
+
║ No ║
|
| 262 |
+
║ ↓ ║
|
| 263 |
+
║ • ToolRouter.execute_tool() ║
|
| 264 |
+
║ • Add result to ContextManager ║
|
| 265 |
+
║ ↓ ║
|
| 266 |
+
║ Continue loop ─────────────────┐ ║
|
| 267 |
+
║ ↑ │ ║
|
| 268 |
+
║ └───────────────────────┘ ║
|
| 269 |
+
╚═══════════════════════════════════════════╝
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
## Events
|
| 273 |
+
|
| 274 |
+
The agent emits the following events via `event_queue`:
|
| 275 |
+
|
| 276 |
+
- `processing` - Starting to process user input
|
| 277 |
+
- `ready` - Agent is ready for input
|
| 278 |
+
- `assistant_chunk` - Streaming token chunk
|
| 279 |
+
- `assistant_message` - Complete LLM response text
|
| 280 |
+
- `assistant_stream_end` - Token stream finished
|
| 281 |
+
- `tool_call` - Tool being called with arguments
|
| 282 |
+
- `tool_output` - Tool execution result
|
| 283 |
+
- `tool_log` - Informational tool log message
|
| 284 |
+
- `tool_state_change` - Tool execution state transition
|
| 285 |
+
- `approval_required` - Requesting user approval for sensitive operations
|
| 286 |
+
- `turn_complete` - Agent finished processing
|
| 287 |
+
- `error` - Error occurred during processing
|
| 288 |
+
- `interrupted` - Agent was interrupted
|
| 289 |
+
- `compacted` - Context was compacted
|
| 290 |
+
- `undo_complete` - Undo operation completed
|
| 291 |
+
- `shutdown` - Agent shutting down
|
| 292 |
+
|
| 293 |
+
## Development
|
| 294 |
+
|
| 295 |
+
### Adding Built-in Tools
|
| 296 |
+
|
| 297 |
+
Edit `agent/core/tools.py`:
|
| 298 |
+
|
| 299 |
+
```python
|
| 300 |
+
def create_builtin_tools() -> list[ToolSpec]:
|
| 301 |
+
return [
|
| 302 |
+
ToolSpec(
|
| 303 |
+
name="your_tool",
|
| 304 |
+
description="What your tool does",
|
| 305 |
+
parameters={
|
| 306 |
+
"type": "object",
|
| 307 |
+
"properties": {
|
| 308 |
+
"param": {"type": "string", "description": "Parameter description"}
|
| 309 |
+
},
|
| 310 |
+
"required": ["param"]
|
| 311 |
+
},
|
| 312 |
+
handler=your_async_handler
|
| 313 |
+
),
|
| 314 |
+
# ... existing tools
|
| 315 |
+
]
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
### Adding MCP Servers
|
| 319 |
+
|
| 320 |
+
Edit `configs/cli_agent_config.json` for CLI defaults, or
|
| 321 |
+
`configs/frontend_agent_config.json` for web-session defaults:
|
| 322 |
+
|
| 323 |
+
```json
|
| 324 |
+
{
|
| 325 |
+
"model_name": "anthropic/claude-sonnet-4-5-20250929",
|
| 326 |
+
"mcpServers": {
|
| 327 |
+
"your-server-name": {
|
| 328 |
+
"transport": "http",
|
| 329 |
+
"url": "https://example.com/mcp",
|
| 330 |
+
"headers": {
|
| 331 |
+
"Authorization": "Bearer ${YOUR_TOKEN}"
|
| 332 |
+
}
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
```
|
| 337 |
+
|
| 338 |
+
Note: Environment variables like `${YOUR_TOKEN}` are auto-substituted from `.env`.
|
| 339 |
|
| 340 |
+
```
|
|
|
|
|
|
|
|
|
backend/document_parser.py
DELETED
|
@@ -1,319 +0,0 @@
|
|
| 1 |
-
"""Extract plain-text context from uploaded office/binary documents.
|
| 2 |
-
|
| 3 |
-
The ML Intern agent runtime is a coding agent that mostly runs on text-only
|
| 4 |
-
LLM backends (many of the free OpenRouter models have no vision). So instead
|
| 5 |
-
of just handing the raw binary file to the model, we extract the meaningful
|
| 6 |
-
text out of it and present it as a compact, readable context block.
|
| 7 |
-
|
| 8 |
-
Supported formats:
|
| 9 |
-
PDF (.pdf)
|
| 10 |
-
Images (.png, .jpg, .jpeg, .webp, .bmp, .tiff, .gif)
|
| 11 |
-
Excel (.xlsx, .xlsm, .xls)
|
| 12 |
-
Word (.docx)
|
| 13 |
-
PowerPoint (.pptx)
|
| 14 |
-
OpenDocument (.odt, .ods, .odp) (via LibreOffice)
|
| 15 |
-
Plain text (.txt, .md, .csv, .json, .jsonl, .rtf, .log)
|
| 16 |
-
|
| 17 |
-
IMPORTANT: For OCR we rely on pytesseract + a system tesseract binary.
|
| 18 |
-
For legacy binary office formats (.xls, .doc, .rtf, .odt/.ods/.odp) we rely
|
| 19 |
-
on LibreOffice headless (`libreoffice --headless --convert-to txt`). If those
|
| 20 |
-
system binaries are not installed, extraction degrades gracefully: the file is
|
| 21 |
-
still uploaded and referenced, but with a warning that the raw content could
|
| 22 |
-
not be parsed.
|
| 23 |
-
"""
|
| 24 |
-
|
| 25 |
-
from __future__ import annotations
|
| 26 |
-
|
| 27 |
-
import io
|
| 28 |
-
import os
|
| 29 |
-
import re
|
| 30 |
-
import shutil
|
| 31 |
-
import subprocess
|
| 32 |
-
import tempfile
|
| 33 |
-
from typing import Optional
|
| 34 |
-
|
| 35 |
-
# --- Optional heavy imports, guarded --------------------------------
|
| 36 |
-
try:
|
| 37 |
-
from PIL import Image
|
| 38 |
-
except Exception: # pragma: no cover
|
| 39 |
-
Image = None
|
| 40 |
-
|
| 41 |
-
try:
|
| 42 |
-
from pypdf import PdfReader
|
| 43 |
-
except Exception: # pragma: no cover
|
| 44 |
-
PdfReader = None
|
| 45 |
-
|
| 46 |
-
try:
|
| 47 |
-
import docx # python-docx
|
| 48 |
-
except Exception: # pragma: no cover
|
| 49 |
-
docx = None
|
| 50 |
-
|
| 51 |
-
try:
|
| 52 |
-
import openpyxl
|
| 53 |
-
except Exception: # pragma: no cover
|
| 54 |
-
openpyxl = None
|
| 55 |
-
|
| 56 |
-
try:
|
| 57 |
-
import pptx # python-pptx
|
| 58 |
-
except Exception: # pragma: no cover
|
| 59 |
-
pptx = None
|
| 60 |
-
|
| 61 |
-
MAX_CHARS = 60_000 # cap extracted text so the context block stays small
|
| 62 |
-
_PAGE_BREAK = "\n\n--- PAGE BREAK ---\n\n"
|
| 63 |
-
|
| 64 |
-
# Map extension -> parser kind
|
| 65 |
-
IMAGE_EXTS = {"png", "jpg", "jpeg", "webp", "bmp", "tiff", "tif", "gif"}
|
| 66 |
-
TEXT_EXTS = {"txt", "md", "csv", "json", "jsonl", "log", "rtf"}
|
| 67 |
-
_LO_TEXT_FORMATS = {".xls", ".doc", ".odt", ".ods", ".odp"}
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def _snippet(text: str, limit: int = 120) -> str:
|
| 71 |
-
text = " ".join(text.split())
|
| 72 |
-
return text[:limit] + ("…" if len(text) > limit else "")
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
# --- Individual extractors -------------------------------------------
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def _extract_pdf(data: bytes) -> str:
|
| 79 |
-
if PdfReader is None:
|
| 80 |
-
raise RuntimeError("pypdf not installed")
|
| 81 |
-
reader = PdfReader(io.BytesIO(data))
|
| 82 |
-
pages = []
|
| 83 |
-
for page in reader.pages:
|
| 84 |
-
try:
|
| 85 |
-
pages.append(page.extract_text() or "")
|
| 86 |
-
except Exception:
|
| 87 |
-
pages.append("")
|
| 88 |
-
return _PAGE_BREAK.join(pages)
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def _extract_docx(data: bytes) -> str:
|
| 92 |
-
if docx is None:
|
| 93 |
-
raise RuntimeError("python-docx not installed")
|
| 94 |
-
document = docx.Document(io.BytesIO(data))
|
| 95 |
-
parts = []
|
| 96 |
-
|
| 97 |
-
# Tables first (structure is usually important)
|
| 98 |
-
for table in document.tables:
|
| 99 |
-
for row in table.rows:
|
| 100 |
-
cells = [c.text.strip() for c in row.cells]
|
| 101 |
-
parts.append(" | ".join(cells))
|
| 102 |
-
parts.append("")
|
| 103 |
-
|
| 104 |
-
for para in document.paragraphs:
|
| 105 |
-
t = para.text.strip()
|
| 106 |
-
if t:
|
| 107 |
-
parts.append(t)
|
| 108 |
-
return "\n".join(parts)
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def _extract_xlsx(data: bytes) -> str:
|
| 112 |
-
if openpyxl is None:
|
| 113 |
-
raise RuntimeError("openpyxl not installed")
|
| 114 |
-
wb = openpyxl.load_workbook(io.BytesIO(data), read_only=True, data_only=True)
|
| 115 |
-
parts = []
|
| 116 |
-
for ws in wb.worksheets:
|
| 117 |
-
parts.append(f"### Sheet: {ws.title}")
|
| 118 |
-
for row in ws.iter_rows(values_only=True):
|
| 119 |
-
vals = ["" if v is None else str(v) for v in row]
|
| 120 |
-
line = " | ".join(vals).strip()
|
| 121 |
-
if line:
|
| 122 |
-
parts.append(line)
|
| 123 |
-
parts.append("")
|
| 124 |
-
return "\n".join(parts)
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
def _extract_pptx(data: bytes) -> str:
|
| 128 |
-
if pptx is None:
|
| 129 |
-
raise RuntimeError("python-pptx not installed")
|
| 130 |
-
prs = pptx.Presentation(io.BytesIO(data))
|
| 131 |
-
parts = []
|
| 132 |
-
for idx, slide in enumerate(prs.slides, start=1):
|
| 133 |
-
parts.append(f"### Slide {idx}")
|
| 134 |
-
for shape in slide.shapes:
|
| 135 |
-
if hasattr(shape, "text") and shape.text and shape.text.strip():
|
| 136 |
-
parts.append(shape.text.strip())
|
| 137 |
-
if shape.has_table if hasattr(shape, "has_table") else False:
|
| 138 |
-
for row in shape.table.rows:
|
| 139 |
-
parts.append(" | ".join(c.text.strip() for c in row.cells))
|
| 140 |
-
parts.append("")
|
| 141 |
-
return "\n".join(parts)
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
def _extract_image(data: bytes) -> str:
|
| 145 |
-
"""OCR an image. Returns combined text + a note about the image."""
|
| 146 |
-
if Image is None:
|
| 147 |
-
raise RuntimeError("Pillow not installed")
|
| 148 |
-
tesseract = shutil.which("tesseract")
|
| 149 |
-
if not tesseract:
|
| 150 |
-
raise RuntimeError(
|
| 151 |
-
"OCR unavailable: tesseract binary not found on this runtime. "
|
| 152 |
-
"The image was still uploaded and attached by reference."
|
| 153 |
-
)
|
| 154 |
-
try:
|
| 155 |
-
import pytesseract
|
| 156 |
-
except Exception as exc: # pragma: no cover
|
| 157 |
-
raise RuntimeError(f"OCR unavailable: pytesseract import failed: {exc}")
|
| 158 |
-
|
| 159 |
-
image = Image.open(io.BytesIO(data))
|
| 160 |
-
try:
|
| 161 |
-
text = pytesseract.image_to_string(image)
|
| 162 |
-
finally:
|
| 163 |
-
try:
|
| 164 |
-
image.close()
|
| 165 |
-
except Exception:
|
| 166 |
-
pass
|
| 167 |
-
return text
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
def _extract_rtf(data: bytes) -> str:
|
| 171 |
-
"""RTF is too messy to hand-parse well; route through LibreOffice if present."""
|
| 172 |
-
return _lo_convert_to_text(data, ".rtf")
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
def _extract_legacy_office(data: bytes, ext: str) -> str:
|
| 176 |
-
"""Legacy binary office formats (.xls, .doc) via LibreOffice headless."""
|
| 177 |
-
return _lo_convert_to_text(data, ext)
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
def _lo_convert_to_text(data: bytes, ext: str) -> str:
|
| 181 |
-
"""Use LibreOffice headless to convert a document to plain text."""
|
| 182 |
-
soffice = shutil.which("libreoffice") or shutil.which("soffice")
|
| 183 |
-
if not soffice:
|
| 184 |
-
raise RuntimeError(
|
| 185 |
-
"LibreOffice not installed on this runtime; cannot parse this "
|
| 186 |
-
"format here. The file was still uploaded and attached by reference."
|
| 187 |
-
)
|
| 188 |
-
with tempfile.TemporaryDirectory() as tmp:
|
| 189 |
-
in_path = os.path.join(tmp, f"input{ext}")
|
| 190 |
-
out_dir = os.path.join(tmp, "out")
|
| 191 |
-
os.makedirs(out_dir, exist_ok=True)
|
| 192 |
-
with open(in_path, "wb") as f:
|
| 193 |
-
f.write(data)
|
| 194 |
-
result = subprocess.run(
|
| 195 |
-
[soffice, "--headless", "--convert-to", "txt:Text (encoded):UTF8",
|
| 196 |
-
"--outdir", out_dir, in_path],
|
| 197 |
-
capture_output=True,
|
| 198 |
-
timeout=120,
|
| 199 |
-
)
|
| 200 |
-
if result.returncode != 0:
|
| 201 |
-
raise RuntimeError(
|
| 202 |
-
f"LibreOffice failed to convert this file (rc={result.returncode})."
|
| 203 |
-
)
|
| 204 |
-
out_files = os.listdir(out_dir)
|
| 205 |
-
if not out_files:
|
| 206 |
-
raise RuntimeError("LibreOffice produced no output for this file.")
|
| 207 |
-
with open(os.path.join(out_dir, out_files[0]), "r", encoding="utf-8", errors="ignore") as f:
|
| 208 |
-
return f.read()
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
# --- Public dispatcher -------------------------------------------------
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
def extract_document_text(filename: str, data: bytes) -> tuple[str, str]:
|
| 215 |
-
"""Extract text from an uploaded document.
|
| 216 |
-
|
| 217 |
-
Returns:
|
| 218 |
-
(extracted_text, warning) — `warning` is a non-empty string when the
|
| 219 |
-
extraction is a degraded fallback, otherwise empty.
|
| 220 |
-
"""
|
| 221 |
-
ext = os.path.splitext(filename or "")[1].lower().lstrip(".")
|
| 222 |
-
try:
|
| 223 |
-
if ext == "pdf":
|
| 224 |
-
text = _extract_pdf(data)
|
| 225 |
-
elif ext in IMAGE_EXTS:
|
| 226 |
-
text = _extract_image(data)
|
| 227 |
-
elif ext in {"xlsx", "xlsm"}:
|
| 228 |
-
text = _extract_xlsx(data)
|
| 229 |
-
elif ext == "xls":
|
| 230 |
-
text = _extract_legacy_office(data, ".xls")
|
| 231 |
-
elif ext == "docx":
|
| 232 |
-
text = _extract_docx(data)
|
| 233 |
-
elif ext == "doc":
|
| 234 |
-
text = _extract_legacy_office(data, ".doc")
|
| 235 |
-
elif ext == "pptx":
|
| 236 |
-
text = _extract_pptx(data)
|
| 237 |
-
elif ext in {"odt", "ods", "odp"}:
|
| 238 |
-
text = _extract_legacy_office(data, f".{ext}")
|
| 239 |
-
elif ext == "rtf":
|
| 240 |
-
text = _extract_rtf(data)
|
| 241 |
-
elif ext in TEXT_EXTS:
|
| 242 |
-
text = data.decode("utf-8", errors="replace")
|
| 243 |
-
else:
|
| 244 |
-
return "", f"Unsupported format '.{ext}' — file uploaded but no text extracted."
|
| 245 |
-
except RuntimeError as e:
|
| 246 |
-
return "", str(e)
|
| 247 |
-
except Exception as e: # pragma: no cover
|
| 248 |
-
return "", f"Failed to extract text from this file: {e}"
|
| 249 |
-
|
| 250 |
-
cleaned = "\n".join(line.rstrip() for line in text.splitlines())
|
| 251 |
-
cleaned = re.sub(r"\n{4,}", "\n\n\n", cleaned).strip()
|
| 252 |
-
|
| 253 |
-
if ext in IMAGE_EXTS:
|
| 254 |
-
note = (
|
| 255 |
-
f"[The image is {len(data):,} bytes. The text below is OCR output. "
|
| 256 |
-
"For layout/diagram questions, refer to the uploaded image file.]\n\n"
|
| 257 |
-
)
|
| 258 |
-
cleaned = note + cleaned
|
| 259 |
-
|
| 260 |
-
if len(cleaned) > MAX_CHARS:
|
| 261 |
-
cleaned = cleaned[:MAX_CHARS] + "\n\n... [TRUNCATED — file content exceeds context limit] ..."
|
| 262 |
-
|
| 263 |
-
return cleaned, ""
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
# --- Dispatch helper for the upload note -------------------------------
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
def format_uploaded_document_context(
|
| 270 |
-
*,
|
| 271 |
-
filename: str,
|
| 272 |
-
stored_filename: str,
|
| 273 |
-
repo_id: str,
|
| 274 |
-
path_in_repo: str,
|
| 275 |
-
hub_url: str,
|
| 276 |
-
size_bytes: int,
|
| 277 |
-
file_format: str,
|
| 278 |
-
extracted_text: str,
|
| 279 |
-
warning: str,
|
| 280 |
-
) -> str:
|
| 281 |
-
"""Build the SYSTEM context note injected into the session for the AI."""
|
| 282 |
-
lines = [
|
| 283 |
-
"[SYSTEM: The user uploaded a document/file for this session.",
|
| 284 |
-
"",
|
| 285 |
-
"A text extraction was performed below so you can understand the file's",
|
| 286 |
-
"content directly. If the text is truncated, empty, or you need the",
|
| 287 |
-
"original bytes/format, the original file is preserved on the Hub:",
|
| 288 |
-
"",
|
| 289 |
-
f"- Repo ID: {repo_id}",
|
| 290 |
-
f"- Repo type: dataset",
|
| 291 |
-
f"- File in repo: {path_in_repo}",
|
| 292 |
-
f"- Original filename: {filename}",
|
| 293 |
-
f"- Stored filename: {stored_filename}",
|
| 294 |
-
f"- Format: {file_format}",
|
| 295 |
-
f"- Size: {size_bytes} bytes",
|
| 296 |
-
f"- Hub URL: {hub_url}",
|
| 297 |
-
"",
|
| 298 |
-
]
|
| 299 |
-
if warning:
|
| 300 |
-
lines.extend(
|
| 301 |
-
[
|
| 302 |
-
"WARNING (extraction):",
|
| 303 |
-
warning,
|
| 304 |
-
"",
|
| 305 |
-
"You can still try to load/read the original file from the Hub URL",
|
| 306 |
-
"above, but understand the plain-text extraction did not succeed.",
|
| 307 |
-
"",
|
| 308 |
-
]
|
| 309 |
-
)
|
| 310 |
-
lines.extend(
|
| 311 |
-
[
|
| 312 |
-
"Extracted text content:",
|
| 313 |
-
"```",
|
| 314 |
-
extracted_text if extracted_text else "(no text could be extracted)",
|
| 315 |
-
"```",
|
| 316 |
-
"]",
|
| 317 |
-
]
|
| 318 |
-
)
|
| 319 |
-
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/session_logger.py
DELETED
|
@@ -1,467 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Permanent session & user context logger.
|
| 3 |
-
|
| 4 |
-
Lưu vĩnh viễn ngữ cảnh từng phiên và từng user vào dataset HuggingFace ngay khi
|
| 5 |
-
có message/event mới — fire-and-forget, robust, không bao giờ block agent.
|
| 6 |
-
|
| 7 |
-
Lưu vào:
|
| 8 |
-
- ``bep40/ml-intern-sessions`` : dataset chung, lưu tất cả session
|
| 9 |
-
- ``{hf_user}/ml-intern-sessions``: dataset cá nhân mỗi user
|
| 10 |
-
|
| 11 |
-
Mỗi message được lưu ngay lập tức qua subprocess (detached) để tránh blocking.
|
| 12 |
-
"""
|
| 13 |
-
|
| 14 |
-
from __future__ import annotations
|
| 15 |
-
|
| 16 |
-
import asyncio
|
| 17 |
-
import json
|
| 18 |
-
import logging
|
| 19 |
-
import os
|
| 20 |
-
import subprocess
|
| 21 |
-
import sys
|
| 22 |
-
import time
|
| 23 |
-
from datetime import datetime
|
| 24 |
-
from pathlib import Path
|
| 25 |
-
from typing import Optional
|
| 26 |
-
|
| 27 |
-
from huggingface_hub import HfApi, hf_hub_download
|
| 28 |
-
|
| 29 |
-
logger = logging.getLogger(__name__)
|
| 30 |
-
|
| 31 |
-
PUBLIC_SESSIONS_DATASET = os.environ.get(
|
| 32 |
-
"ML_INTERN_SESSIONS_DATASET", "bep40/ml-intern-sessions"
|
| 33 |
-
)
|
| 34 |
-
|
| 35 |
-
DEFAULT_LOG_DIR = Path(os.environ.get("ML_INTERN_SESSION_LOG_DIR", "/tmp/session_logs"))
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def _now_iso() -> str:
|
| 39 |
-
return datetime.now().astimezone().isoformat()
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def _get_hf_token() -> Optional[str]:
|
| 43 |
-
return (
|
| 44 |
-
os.environ.get("HF_TOKEN")
|
| 45 |
-
or os.environ.get("_ML_INTERN_PERSONAL_TOKEN")
|
| 46 |
-
or os.environ.get("HF_SESSION_UPLOAD_TOKEN")
|
| 47 |
-
or os.environ.get("HF_ADMIN_TOKEN")
|
| 48 |
-
)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def _dataset_card_readme() -> str:
|
| 52 |
-
return """---
|
| 53 |
-
pretty_name: "ML Intern Session Context"
|
| 54 |
-
language:
|
| 55 |
-
- en
|
| 56 |
-
license: other
|
| 57 |
-
task_categories:
|
| 58 |
-
- text-generation
|
| 59 |
-
tags:
|
| 60 |
-
- ml-intern
|
| 61 |
-
- session-traces
|
| 62 |
-
- conversation
|
| 63 |
-
- agent-traces
|
| 64 |
-
configs:
|
| 65 |
-
- config_name: default
|
| 66 |
-
data_files:
|
| 67 |
-
- split: train
|
| 68 |
-
path: "sessions/**/*.jsonl"
|
| 69 |
-
---
|
| 70 |
-
|
| 71 |
-
# ML Intern Session Context
|
| 72 |
-
|
| 73 |
-
**Lưu trữ vĩnh viễn ngữ cảnh từng phiên và từng user.**
|
| 74 |
-
|
| 75 |
-
Mỗi message, tool call, event được lưu ngay khi xảy ra để đảm bảo không bao giờ mất dữ liệu.
|
| 76 |
-
|
| 77 |
-
## Schema
|
| 78 |
-
|
| 79 |
-
| Column | Type | Mô tả |
|
| 80 |
-
|----------------|--------|------------------------------------------|
|
| 81 |
-
| session_id | string | UUID của session |
|
| 82 |
-
| user_id | string | ID người dùng |
|
| 83 |
-
| model_name | string | Model đang dùng |
|
| 84 |
-
| timestamp | string | ISO timestamp |
|
| 85 |
-
| role | string | user / assistant / tool / system / event |
|
| 86 |
-
| content | string | Nội dung message |
|
| 87 |
-
| event_type | string | Loại event |
|
| 88 |
-
| seq | int | Số thứ tự trong session |
|
| 89 |
-
| extra | string | JSON thông tin bổ sung |
|
| 90 |
-
"""
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
def _append_to_local_log(
|
| 94 |
-
session_id: str,
|
| 95 |
-
user_id: str,
|
| 96 |
-
model_name: str,
|
| 97 |
-
role: str,
|
| 98 |
-
content: str,
|
| 99 |
-
event_type: str,
|
| 100 |
-
seq: int,
|
| 101 |
-
extra: dict | None = None,
|
| 102 |
-
directory: str = str(DEFAULT_LOG_DIR),
|
| 103 |
-
) -> Optional[str]:
|
| 104 |
-
try:
|
| 105 |
-
log_dir = Path(directory)
|
| 106 |
-
log_dir.mkdir(parents=True, exist_ok=True)
|
| 107 |
-
row = {
|
| 108 |
-
"session_id": session_id,
|
| 109 |
-
"user_id": user_id,
|
| 110 |
-
"model_name": model_name,
|
| 111 |
-
"timestamp": _now_iso(),
|
| 112 |
-
"role": role,
|
| 113 |
-
"content": content,
|
| 114 |
-
"event_type": event_type,
|
| 115 |
-
"seq": seq,
|
| 116 |
-
"extra": json.dumps(extra) if extra else "{}",
|
| 117 |
-
}
|
| 118 |
-
date_str = datetime.now().strftime("%Y-%m-%d")
|
| 119 |
-
filename = f"session_{session_id}_{date_str}.jsonl"
|
| 120 |
-
filepath = log_dir / filename
|
| 121 |
-
with open(filepath, "a") as f:
|
| 122 |
-
f.write(json.dumps(row, default=str) + "\n")
|
| 123 |
-
return str(filepath)
|
| 124 |
-
except Exception as e:
|
| 125 |
-
logger.debug("Local log write failed: %s", e)
|
| 126 |
-
return None
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
# ─── Dataset reader: per-file scan (never loads 410MB into memory) ──────────
|
| 130 |
-
|
| 131 |
-
_file_list_cache: list[str] | None = None
|
| 132 |
-
_file_list_cache_time: float = 0
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def _list_dataset_files() -> list[str]:
|
| 136 |
-
"""Return all .jsonl file paths in the sessions dataset (cached, 60s TTL)."""
|
| 137 |
-
global _file_list_cache, _file_list_cache_time
|
| 138 |
-
now = time.time()
|
| 139 |
-
if _file_list_cache is not None and (now - _file_list_cache_time) < 60:
|
| 140 |
-
return _file_list_cache
|
| 141 |
-
try:
|
| 142 |
-
api = HfApi(token=_get_hf_token())
|
| 143 |
-
files = api.list_repo_files(PUBLIC_SESSIONS_DATASET, repo_type="dataset")
|
| 144 |
-
_file_list_cache = [f for f in files if f.endswith(".jsonl")]
|
| 145 |
-
_file_list_cache_time = now
|
| 146 |
-
except Exception as e:
|
| 147 |
-
logger.warning("Failed to list dataset files: %s", e)
|
| 148 |
-
_file_list_cache = []
|
| 149 |
-
_file_list_cache_time = now
|
| 150 |
-
return _file_list_cache
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
def _normalize_row(row: dict) -> dict | None:
|
| 154 |
-
"""Normalize a row to the standard schema regardless of source format.
|
| 155 |
-
|
| 156 |
-
The dataset contains two incompatible formats:
|
| 157 |
-
- *New* (session_logger.py): session_id, user_id, role, content, event_type, seq
|
| 158 |
-
- *Old* (Claude Code trace format): type, message, uuid, parentUuid, sessionId, timestamp
|
| 159 |
-
"""
|
| 160 |
-
if not isinstance(row, dict):
|
| 161 |
-
return None
|
| 162 |
-
|
| 163 |
-
# Already in new format — has session_id + role
|
| 164 |
-
if "session_id" in row and "role" in row:
|
| 165 |
-
return row
|
| 166 |
-
|
| 167 |
-
# Old Claude Code format: type + message
|
| 168 |
-
if "type" in row and "message" in row:
|
| 169 |
-
session_id = row.get("sessionId") or row.get("session_id") or ""
|
| 170 |
-
if not session_id:
|
| 171 |
-
return None
|
| 172 |
-
msg = row.get("message")
|
| 173 |
-
role = row.get("type", "unknown")
|
| 174 |
-
content_text = ""
|
| 175 |
-
model_name = ""
|
| 176 |
-
if isinstance(msg, dict):
|
| 177 |
-
role = msg.get("role", role)
|
| 178 |
-
content_val = msg.get("content", "")
|
| 179 |
-
model_name = msg.get("model", "")
|
| 180 |
-
if isinstance(content_val, str):
|
| 181 |
-
content_text = content_val
|
| 182 |
-
elif isinstance(content_val, list):
|
| 183 |
-
parts = []
|
| 184 |
-
for block in content_val:
|
| 185 |
-
if isinstance(block, dict):
|
| 186 |
-
t = block.get("text") or block.get("content")
|
| 187 |
-
if isinstance(t, str):
|
| 188 |
-
parts.append(t)
|
| 189 |
-
elif isinstance(block, str):
|
| 190 |
-
parts.append(block)
|
| 191 |
-
content_text = " ".join(parts)
|
| 192 |
-
elif content_val is None:
|
| 193 |
-
content_text = ""
|
| 194 |
-
return {
|
| 195 |
-
"session_id": session_id,
|
| 196 |
-
"user_id": "",
|
| 197 |
-
"model_name": model_name,
|
| 198 |
-
"timestamp": row.get("timestamp", ""),
|
| 199 |
-
"role": role,
|
| 200 |
-
"content": content_text,
|
| 201 |
-
"event_type": row.get("type", "message"),
|
| 202 |
-
"seq": 0,
|
| 203 |
-
"extra": "{}",
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
return None
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
def _read_jsonl_file(file_path: str) -> list[dict]:
|
| 210 |
-
"""Download a single JSONL file from the dataset and parse its lines.
|
| 211 |
-
|
| 212 |
-
Robust to: empty files, malformed JSON lines, old/new format mismatch.
|
| 213 |
-
"""
|
| 214 |
-
try:
|
| 215 |
-
local = hf_hub_download(
|
| 216 |
-
PUBLIC_SESSIONS_DATASET,
|
| 217 |
-
file_path,
|
| 218 |
-
repo_type="dataset",
|
| 219 |
-
token=_get_hf_token(),
|
| 220 |
-
)
|
| 221 |
-
rows: list[dict] = []
|
| 222 |
-
with open(local, encoding="utf-8", errors="replace") as fh:
|
| 223 |
-
for line in fh:
|
| 224 |
-
line = line.strip()
|
| 225 |
-
if not line:
|
| 226 |
-
continue
|
| 227 |
-
try:
|
| 228 |
-
row = json.loads(line)
|
| 229 |
-
normalized = _normalize_row(row)
|
| 230 |
-
if normalized is not None:
|
| 231 |
-
rows.append(normalized)
|
| 232 |
-
except json.JSONDecodeError:
|
| 233 |
-
continue
|
| 234 |
-
return rows
|
| 235 |
-
except Exception as e:
|
| 236 |
-
logger.debug("Failed to read %s: %s", file_path, e)
|
| 237 |
-
return []
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
def load_session_history(session_id: str) -> list[dict]:
|
| 241 |
-
"""Load all messages/events for a session from the dataset.
|
| 242 |
-
|
| 243 |
-
Reads only the files whose name contains ``session_id`` instead of
|
| 244 |
-
loading the entire 400MB dataset into memory on every call.
|
| 245 |
-
"""
|
| 246 |
-
try:
|
| 247 |
-
all_files = _list_dataset_files()
|
| 248 |
-
matching = [
|
| 249 |
-
f for f in all_files if session_id in os.path.basename(f)
|
| 250 |
-
]
|
| 251 |
-
rows: list[dict] = []
|
| 252 |
-
for f in matching:
|
| 253 |
-
for row in _read_jsonl_file(f):
|
| 254 |
-
if row.get("session_id") == session_id:
|
| 255 |
-
rows.append(row)
|
| 256 |
-
rows.sort(key=lambda r: (r.get("seq", 0), r.get("timestamp", "")))
|
| 257 |
-
return rows
|
| 258 |
-
except Exception as e:
|
| 259 |
-
import traceback
|
| 260 |
-
logger.warning(
|
| 261 |
-
"Failed to load session history: %s\n%s",
|
| 262 |
-
e,
|
| 263 |
-
traceback.format_exc(),
|
| 264 |
-
)
|
| 265 |
-
return []
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
def get_user_sessions(user_id: str, limit: int = 50) -> list[dict]:
|
| 269 |
-
"""Get all sessions for a user from the dataset.
|
| 270 |
-
|
| 271 |
-
Uses per-file scanning exclusively (never calls load_dataset which can
|
| 272 |
-
fail when the dataset has incompatible/mixed schemas). Cached file list
|
| 273 |
-
(60s TTL) avoids repeated list_repo_files calls.
|
| 274 |
-
"""
|
| 275 |
-
sessions: dict[str, dict] = {}
|
| 276 |
-
for f in _list_dataset_files():
|
| 277 |
-
for row in _read_jsonl_file(f):
|
| 278 |
-
if row.get("user_id") == user_id:
|
| 279 |
-
sid = row.get("session_id")
|
| 280 |
-
if sid and sid not in sessions:
|
| 281 |
-
sessions[sid] = {
|
| 282 |
-
"session_id": sid,
|
| 283 |
-
"user_id": user_id,
|
| 284 |
-
"model_name": row.get("model_name"),
|
| 285 |
-
"last_timestamp": row.get("timestamp"),
|
| 286 |
-
"message_count": 0,
|
| 287 |
-
}
|
| 288 |
-
sessions[sid]["message_count"] += 1
|
| 289 |
-
|
| 290 |
-
result = list(sessions.values())
|
| 291 |
-
result.sort(key=lambda s: s.get("last_timestamp", ""), reverse=True)
|
| 292 |
-
return result[:limit]
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
# ─── Upload/subprocess functions (unchanged from original) ──────────────────
|
| 296 |
-
|
| 297 |
-
def _spawn_uploader(
|
| 298 |
-
row_data: dict,
|
| 299 |
-
repo_id: str,
|
| 300 |
-
token: str,
|
| 301 |
-
private: bool = False,
|
| 302 |
-
) -> None:
|
| 303 |
-
"""Spawn detached subprocess to upload a single row to HF dataset."""
|
| 304 |
-
try:
|
| 305 |
-
script = f"""
|
| 306 |
-
import json, sys, time, os
|
| 307 |
-
from huggingface_hub import HfApi
|
| 308 |
-
|
| 309 |
-
row_data = {json.dumps(row_data)}
|
| 310 |
-
repo_id = "{repo_id}"
|
| 311 |
-
token = "{token}"
|
| 312 |
-
private = {private}
|
| 313 |
-
|
| 314 |
-
api = HfApi()
|
| 315 |
-
try:
|
| 316 |
-
api.create_repo(repo_id=repo_id, repo_type="dataset", private=private, token=token, exist_ok=True)
|
| 317 |
-
except: pass
|
| 318 |
-
|
| 319 |
-
# Upload README
|
| 320 |
-
try:
|
| 321 |
-
api.upload_file(path_or_fileobj={_dataset_card_readme()!r}.encode(), path_in_repo="README.md", repo_id=repo_id, repo_type="dataset", token=token)
|
| 322 |
-
except: pass
|
| 323 |
-
|
| 324 |
-
date_str = row_data.get("timestamp", "")[:10]
|
| 325 |
-
session_id = row_data.get("session_id", "unknown")
|
| 326 |
-
seq = row_data.get("seq", 0)
|
| 327 |
-
path_in_repo = f"sessions/{{date_str}}/{{session_id}}_{{seq}}.jsonl"
|
| 328 |
-
payload = json.dumps(row_data)
|
| 329 |
-
|
| 330 |
-
for attempt in range(5):
|
| 331 |
-
try:
|
| 332 |
-
api.upload_file(path_or_fileobj=payload.encode(), path_in_repo=path_in_repo, repo_id=repo_id, repo_type="dataset", token=token, commit_message=f"Add session {{session_id[:8]}} seq {{seq}}")
|
| 333 |
-
break
|
| 334 |
-
except Exception as e:
|
| 335 |
-
if attempt < 4:
|
| 336 |
-
time.sleep(2 ** attempt)
|
| 337 |
-
else:
|
| 338 |
-
sys.exit(1)
|
| 339 |
-
"""
|
| 340 |
-
subprocess.Popen(
|
| 341 |
-
[sys.executable, "-c", script],
|
| 342 |
-
stdin=subprocess.DEVNULL,
|
| 343 |
-
stdout=subprocess.DEVNULL,
|
| 344 |
-
stderr=subprocess.DEVNULL,
|
| 345 |
-
env=os.environ.copy(),
|
| 346 |
-
start_new_session=True,
|
| 347 |
-
)
|
| 348 |
-
except Exception as e:
|
| 349 |
-
logger.debug("Upload spawn failed: %s", e)
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
def log_message(
|
| 353 |
-
session_id: str,
|
| 354 |
-
user_id: str,
|
| 355 |
-
model_name: str,
|
| 356 |
-
role: str,
|
| 357 |
-
content: str,
|
| 358 |
-
event_type: str = "message",
|
| 359 |
-
seq: int = 0,
|
| 360 |
-
extra: dict | None = None,
|
| 361 |
-
hf_user: str | None = None,
|
| 362 |
-
) -> None:
|
| 363 |
-
"""Log a single message/event permanently to dataset.
|
| 364 |
-
|
| 365 |
-
Fire-and-forget — all I/O is offloaded to a detached subprocess.
|
| 366 |
-
"""
|
| 367 |
-
token = _get_hf_token()
|
| 368 |
-
|
| 369 |
-
# Always write to local log first (backup)
|
| 370 |
-
_append_to_local_log(
|
| 371 |
-
session_id=session_id,
|
| 372 |
-
user_id=user_id,
|
| 373 |
-
model_name=model_name,
|
| 374 |
-
role=role,
|
| 375 |
-
content=content,
|
| 376 |
-
event_type=event_type,
|
| 377 |
-
seq=seq,
|
| 378 |
-
extra=extra,
|
| 379 |
-
)
|
| 380 |
-
|
| 381 |
-
if not token:
|
| 382 |
-
logger.debug("No HF token, skipping dataset upload")
|
| 383 |
-
return
|
| 384 |
-
|
| 385 |
-
row_data = {
|
| 386 |
-
"session_id": session_id,
|
| 387 |
-
"user_id": user_id,
|
| 388 |
-
"model_name": model_name,
|
| 389 |
-
"timestamp": _now_iso(),
|
| 390 |
-
"role": role,
|
| 391 |
-
"content": content,
|
| 392 |
-
"event_type": event_type,
|
| 393 |
-
"seq": seq,
|
| 394 |
-
"extra": json.dumps(extra) if extra else "{}",
|
| 395 |
-
}
|
| 396 |
-
|
| 397 |
-
# Upload to public/shared dataset
|
| 398 |
-
_spawn_uploader(row_data, PUBLIC_SESSIONS_DATASET, token, private=False)
|
| 399 |
-
|
| 400 |
-
# Upload to personal dataset if available
|
| 401 |
-
if hf_user and hf_user != "dev":
|
| 402 |
-
_spawn_uploader(row_data, f"{hf_user}/ml-intern-sessions", token, private=True)
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
def log_trajectory(
|
| 406 |
-
trajectory: dict,
|
| 407 |
-
user_id: str,
|
| 408 |
-
hf_user: str | None = None,
|
| 409 |
-
hf_token: str | None = None,
|
| 410 |
-
private: bool = False,
|
| 411 |
-
) -> Optional[str]:
|
| 412 |
-
"""Upload complete session trajectory to dataset (final flush)."""
|
| 413 |
-
session_id = trajectory.get("session_id", "unknown")
|
| 414 |
-
model_name = trajectory.get("model_name", "")
|
| 415 |
-
messages = trajectory.get("messages", [])
|
| 416 |
-
|
| 417 |
-
log_dir = Path(str(DEFAULT_LOG_DIR))
|
| 418 |
-
log_dir.mkdir(parents=True, exist_ok=True)
|
| 419 |
-
date_str = datetime.now().strftime("%Y-%m-%d")
|
| 420 |
-
filepath = log_dir / f"session_{session_id}_{date_str}.jsonl"
|
| 421 |
-
|
| 422 |
-
with open(filepath, "w") as f:
|
| 423 |
-
for msg in messages:
|
| 424 |
-
row = {
|
| 425 |
-
"session_id": session_id,
|
| 426 |
-
"user_id": user_id,
|
| 427 |
-
"model_name": model_name,
|
| 428 |
-
"timestamp": msg.get("timestamp") or _now_iso(),
|
| 429 |
-
"role": msg.get("role", "unknown"),
|
| 430 |
-
"content": msg.get("content", ""),
|
| 431 |
-
"event_type": "message",
|
| 432 |
-
"seq": msg.get("seq", 0),
|
| 433 |
-
"extra": json.dumps({
|
| 434 |
-
"tool_calls": msg.get("tool_calls"),
|
| 435 |
-
"tool_call_id": msg.get("tool_call_id"),
|
| 436 |
-
}) if any(k in msg for k in ("tool_calls", "tool_call_id")) else "{}",
|
| 437 |
-
}
|
| 438 |
-
f.write(json.dumps(row, default=str) + "\n")
|
| 439 |
-
|
| 440 |
-
events = trajectory.get("events", [])
|
| 441 |
-
for event in events:
|
| 442 |
-
row = {
|
| 443 |
-
"session_id": session_id,
|
| 444 |
-
"user_id": user_id,
|
| 445 |
-
"model_name": model_name,
|
| 446 |
-
"timestamp": event.get("timestamp", _now_iso()),
|
| 447 |
-
"role": "event",
|
| 448 |
-
"content": json.dumps(event.get("data", {})),
|
| 449 |
-
"event_type": event.get("event_type", "unknown"),
|
| 450 |
-
"seq": event.get("seq", 0),
|
| 451 |
-
"extra": "{}",
|
| 452 |
-
}
|
| 453 |
-
f.write(json.dumps(row, default=str) + "\n")
|
| 454 |
-
|
| 455 |
-
token = hf_token or _get_hf_token()
|
| 456 |
-
if token:
|
| 457 |
-
with open(filepath, "r") as f:
|
| 458 |
-
for line in f:
|
| 459 |
-
try:
|
| 460 |
-
row = json.loads(line.strip())
|
| 461 |
-
_spawn_uploader(row, PUBLIC_SESSIONS_DATASET, token, private)
|
| 462 |
-
if hf_user and hf_user != "dev":
|
| 463 |
-
_spawn_uploader(row, f"{hf_user}/ml-intern-sessions", token, True)
|
| 464 |
-
except Exception:
|
| 465 |
-
continue
|
| 466 |
-
|
| 467 |
-
return str(filepath)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/__init__.py
ADDED
|
File without changes
|
configs/cli_agent_config.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "zai-org/GLM-5.2:novita",
|
| 3 |
+
"save_sessions": true,
|
| 4 |
+
"session_dataset_repo": "smolagents/ml-intern-sessions",
|
| 5 |
+
"share_traces": true,
|
| 6 |
+
"personal_trace_repo_template": "{hf_user}/ml-intern-sessions",
|
| 7 |
+
"yolo_mode": false,
|
| 8 |
+
"confirm_cpu_jobs": true,
|
| 9 |
+
"auto_file_upload": true,
|
| 10 |
+
"tool_runtime": "local",
|
| 11 |
+
"messaging": {
|
| 12 |
+
"enabled": false,
|
| 13 |
+
"auto_event_types": ["approval_required", "error", "turn_complete"],
|
| 14 |
+
"destinations": {}
|
| 15 |
+
},
|
| 16 |
+
"mcpServers": {
|
| 17 |
+
"hf-mcp-server": {
|
| 18 |
+
"transport": "http",
|
| 19 |
+
"url": "https://huggingface.co/mcp?login"
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
}
|
configs/frontend_agent_config.json
CHANGED
|
@@ -1,19 +1,16 @@
|
|
| 1 |
{
|
| 2 |
-
"model_name": "
|
| 3 |
-
"api_base": "https://openrouter.ai/api/v1",
|
| 4 |
"save_sessions": true,
|
| 5 |
-
"
|
| 6 |
"share_traces": true,
|
|
|
|
| 7 |
"yolo_mode": false,
|
| 8 |
"confirm_cpu_jobs": true,
|
| 9 |
"auto_file_upload": true,
|
| 10 |
-
"auto_save_interval": 1,
|
| 11 |
-
"heartbeat_interval_s": 30,
|
| 12 |
-
"max_iterations": 300,
|
| 13 |
"mcpServers": {
|
| 14 |
"hf-mcp-server": {
|
| 15 |
"transport": "http",
|
| 16 |
-
"url": "https://
|
| 17 |
}
|
| 18 |
}
|
| 19 |
-
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"model_name": "${ML_INTERN_DEFAULT_MODEL_ID:-zai-org/GLM-5.2:novita}",
|
|
|
|
| 3 |
"save_sessions": true,
|
| 4 |
+
"session_dataset_repo": "smolagents/ml-intern-sessions",
|
| 5 |
"share_traces": true,
|
| 6 |
+
"personal_trace_repo_template": "{hf_user}/ml-intern-sessions",
|
| 7 |
"yolo_mode": false,
|
| 8 |
"confirm_cpu_jobs": true,
|
| 9 |
"auto_file_upload": true,
|
|
|
|
|
|
|
|
|
|
| 10 |
"mcpServers": {
|
| 11 |
"hf-mcp-server": {
|
| 12 |
"transport": "http",
|
| 13 |
+
"url": "https://huggingface.co/mcp?login"
|
| 14 |
}
|
| 15 |
}
|
| 16 |
+
}
|
fix_space_error_report.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"status": "rebuilding_with_deepseek_flash_fix",
|
| 3 |
-
"space": "bep40/ml-intern",
|
| 4 |
-
"space_url": "https://huggingface.co/spaces/bep40/ml-intern",
|
| 5 |
-
"root_cause": "DeepSeek V4 Flash model IDs were routed incorrectly: (1) GLM_52_MODEL_ID was set to 'openai/deepseek/deepseek-v4-flash' which the OpenRouter routing patch sends to OpenRouter, but OpenRouter model IDs do not include the 'openai/' prefix -- causing a 404 model-not-found. (2) The routing prefix check startswith('deepseek/') never matches 'deepseek-ai/' model IDs, so 'deepseek-ai/DeepSeek-V4-Flash' without a provider tag relied on HF Router auto-routing which could hit a provider lacking structured output support.",
|
| 6 |
-
"fix": "Changed GLM_52_MODEL_ID from 'openai/deepseek/deepseek-v4-flash' to 'deepseek-ai/DeepSeek-V4-Flash:deepinfra' (HF Router path with explicit provider tag). Also fixed the OpenRouter routing patch to strip the 'openai/' prefix before sending the model name to OpenRouter's API. Added ':novita' tag to DeepSeek V4 Pro for deterministic HF Router routing.",
|
| 7 |
-
"files_changed": ["patch_models.py"],
|
| 8 |
-
"model_routing": {
|
| 9 |
-
"deepseek-ai/DeepSeek-V4-Flash": "HF Inference Router (3 providers: novita, fireworks-ai, deepinfra) - all support tools, 1M context",
|
| 10 |
-
"deepseek-ai/DeepSeek-V4-Flash-0731": "HF Inference Router (5 providers) - all support tools, 1M context",
|
| 11 |
-
"deepseek/deepseek-v4-flash-0731": "OpenRouter - available via openrouter/deepseek/deepseek-v4-flash-0731"
|
| 12 |
-
},
|
| 13 |
-
"verification": "Space rebuild triggered. Monitor Space build logs. Test model selection for DeepSeek V4 Flash after rebuild completes."
|
| 14 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_agent_backend.py
DELETED
|
@@ -1,87 +0,0 @@
|
|
| 1 |
-
"""Patch: agent_loop.py task detection + auto-approve for free models."""
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
import re
|
| 5 |
-
import ast
|
| 6 |
-
|
| 7 |
-
AGENT_LOOP = "/app/agent/core/agent_loop.py"
|
| 8 |
-
AGENT_FILE = "/app/backend/routes/agent.py"
|
| 9 |
-
SESSION_FILE = "/app/agent/core/session.py"
|
| 10 |
-
|
| 11 |
-
# ===== PATCH 1: agent_loop.py - detect model stopped mid-task =====
|
| 12 |
-
with open(AGENT_LOOP) as f:
|
| 13 |
-
c = f.read()
|
| 14 |
-
|
| 15 |
-
# Add helper function before class Handlers
|
| 16 |
-
if "_unfinished_plan" not in c:
|
| 17 |
-
fn = (
|
| 18 |
-
"\n\ndef _unfinished_plan(s):\n"
|
| 19 |
-
" p = getattr(s, 'current_plan', None) or []\n"
|
| 20 |
-
" return [it for it in p if it.get('status') in ('pending', 'in_progress')]\n\n"
|
| 21 |
-
)
|
| 22 |
-
c = c.replace("class Handlers:", fn + "class Handlers:")
|
| 23 |
-
print("OK: Added _unfinished_plan()")
|
| 24 |
-
|
| 25 |
-
# Add detection: if model produced no tool calls but content still exists,
|
| 26 |
-
# and plan items are still pending/in_progress, send session_update event
|
| 27 |
-
if "ac_plan" not in c:
|
| 28 |
-
check = (
|
| 29 |
-
"\n # Auto-continue: detect model stopped mid-task\n"
|
| 30 |
-
" if not llm_result.tool_calls_acc and llm_result.content:\n"
|
| 31 |
-
" unfinished = _unfinished_plan(session)\n"
|
| 32 |
-
" if unfinished:\n"
|
| 33 |
-
" await session.send_event(Event(event_type='session_update', data={\n"
|
| 34 |
-
" 'ac_plan': unfinished,\n"
|
| 35 |
-
" }))\n"
|
| 36 |
-
)
|
| 37 |
-
c = c.replace(" # -- End of turn --", check + " # -- End of turn --")
|
| 38 |
-
print("OK: Added mid-task detection")
|
| 39 |
-
|
| 40 |
-
try:
|
| 41 |
-
ast.parse(c)
|
| 42 |
-
with open(AGENT_LOOP, "w") as f:
|
| 43 |
-
f.write(c)
|
| 44 |
-
print("OK: agent_loop.py patched")
|
| 45 |
-
except SyntaxError as e:
|
| 46 |
-
print(f"FAIL: agent_loop.py syntax: {e}")
|
| 47 |
-
|
| 48 |
-
# ===== PATCH 2: Enable auto_approval for ALL sessions by default =====
|
| 49 |
-
# So free models don't block on user approval.
|
| 50 |
-
with open(SESSION_FILE) as f:
|
| 51 |
-
c = f.read()
|
| 52 |
-
|
| 53 |
-
if "AUTO_APPROVE" not in c and "self.auto_approval" not in c:
|
| 54 |
-
auto_patch = (
|
| 55 |
-
"\n # === PATCH: Auto-approve for free models ===\n"
|
| 56 |
-
" self.auto_approval = {\n"
|
| 57 |
-
' "enabled": True,\n'
|
| 58 |
-
' "cost_cap_usd": None,\n'
|
| 59 |
-
' "estimated_spend_usd": 0.0,\n'
|
| 60 |
-
' "remaining_usd": None,\n'
|
| 61 |
-
" }\n"
|
| 62 |
-
)
|
| 63 |
-
|
| 64 |
-
# Insert as the FIRST STATEMENT of __init__ body: find the closing
|
| 65 |
-
# " ):" that terminates the def __init__(...) parameter list and
|
| 66 |
-
# insert right after it. (Inserting into the parameter list corrupts
|
| 67 |
-
# the file with invalid syntax.)
|
| 68 |
-
init_match = re.search(r"def __init__\(.*?\):\n", c, re.DOTALL)
|
| 69 |
-
if init_match:
|
| 70 |
-
insert_at = init_match.end()
|
| 71 |
-
c = c[:insert_at] + auto_patch + c[insert_at:]
|
| 72 |
-
print("OK: Added auto_approve default")
|
| 73 |
-
else:
|
| 74 |
-
print("WARN: could not locate __init__ signature in session.py")
|
| 75 |
-
|
| 76 |
-
try:
|
| 77 |
-
ast.parse(c)
|
| 78 |
-
with open(SESSION_FILE, "w") as f:
|
| 79 |
-
f.write(c)
|
| 80 |
-
print("OK: session.py patched & syntactically valid")
|
| 81 |
-
except SyntaxError as e:
|
| 82 |
-
print(f"FAIL: session.py syntax: {e}")
|
| 83 |
-
# Don't block on this patch
|
| 84 |
-
else:
|
| 85 |
-
print("OK: session.py already has auto_approval")
|
| 86 |
-
|
| 87 |
-
print("DONE: Backend patches applied")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_agent_loop.py
DELETED
|
@@ -1,74 +0,0 @@
|
|
| 1 |
-
"""Patch agent_loop.py - detect task incomplete and send event."""
|
| 2 |
-
|
| 3 |
-
AGENT_LOOP = "/app/agent/core/agent_loop.py"
|
| 4 |
-
|
| 5 |
-
def patch():
|
| 6 |
-
import os
|
| 7 |
-
if not os.path.exists(AGENT_LOOP):
|
| 8 |
-
print(f"SKIP: {AGENT_LOOP} not found")
|
| 9 |
-
return
|
| 10 |
-
|
| 11 |
-
with open(AGENT_LOOP, "r", encoding="utf-8") as f:
|
| 12 |
-
content = f.read()
|
| 13 |
-
|
| 14 |
-
# Add _check_task_incomplete function before class Handlers
|
| 15 |
-
check_func = '''
|
| 16 |
-
|
| 17 |
-
def _check_task_incomplete(session: Session, llm_result: LLMResult) -> bool:
|
| 18 |
-
"""Check if model stopped streaming but task is incomplete."""
|
| 19 |
-
if llm_result.tool_calls_acc:
|
| 20 |
-
return False # Has tool calls, task continues via tool execution
|
| 21 |
-
|
| 22 |
-
plan = getattr(session, "current_plan", None) or []
|
| 23 |
-
unfinished = [item for item in plan if item.get("status") in ("pending", "in_progress")]
|
| 24 |
-
|
| 25 |
-
return len(unfinished) > 0
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
|
| 29 |
-
"""Helper to get unfinished plan items."""
|
| 30 |
-
plan = getattr(session, "current_plan", None) or []
|
| 31 |
-
return [item for item in plan if item.get("status") in ("pending", "in_progress")]
|
| 32 |
-
|
| 33 |
-
'''
|
| 34 |
-
|
| 35 |
-
if "_check_task_incomplete" not in content:
|
| 36 |
-
content = content.replace(
|
| 37 |
-
"class Handlers:",
|
| 38 |
-
check_func + "\n\nclass Handlers:"
|
| 39 |
-
)
|
| 40 |
-
|
| 41 |
-
# Add the check after LLM response processing
|
| 42 |
-
check_call = '''
|
| 43 |
-
# === Check for incomplete task after LLM response ===
|
| 44 |
-
if not llm_result.tool_calls_acc and llm_result.content:
|
| 45 |
-
unfinished = _unfinished_plan_items(session)
|
| 46 |
-
if unfinished:
|
| 47 |
-
await session.send_event(
|
| 48 |
-
Event(
|
| 49 |
-
event_type="task_incomplete",
|
| 50 |
-
data={
|
| 51 |
-
"incomplete_plan": unfinished,
|
| 52 |
-
"message": "Model phản hồi đã dừng nhưng nhiệm vụ chưa hoàn thành."
|
| 53 |
-
}
|
| 54 |
-
)
|
| 55 |
-
)
|
| 56 |
-
'''
|
| 57 |
-
|
| 58 |
-
if "task_incomplete" not in content:
|
| 59 |
-
# Find a good insertion point - after tool calls processing
|
| 60 |
-
insert_marker = " # -- End of turn --"
|
| 61 |
-
if insert_marker in content:
|
| 62 |
-
content = content.replace(insert_marker, check_call + "\n" + insert_marker)
|
| 63 |
-
else:
|
| 64 |
-
# Try another location
|
| 65 |
-
result_marker = "final_response = llm_result.content or None"
|
| 66 |
-
if result_marker in content:
|
| 67 |
-
content = content.replace(result_marker, result_marker + "\n" + check_call)
|
| 68 |
-
|
| 69 |
-
with open(AGENT_LOOP, "w", encoding="utf-8") as f:
|
| 70 |
-
f.write(content)
|
| 71 |
-
print(f"OK: Patched {AGENT_LOOP}")
|
| 72 |
-
|
| 73 |
-
if __name__ == "__main__":
|
| 74 |
-
patch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_apodex_ling_beatapi.py
DELETED
|
@@ -1,145 +0,0 @@
|
|
| 1 |
-
"""Patch backend: add apodex-1.1-mini:free, ling-3.1-flash (OpenRouter) + BeatAPI space-bunny-alpha.
|
| 2 |
-
|
| 3 |
-
Two OpenRouter models (same `openai/` catch-all routing as the rest of the app):
|
| 4 |
-
- openai/apodex/apodex-1.1-mini:free
|
| 5 |
-
- openai/inclusionai/ling-3.1-flash
|
| 6 |
-
|
| 7 |
-
One BEATAPI model routed to a *different* provider entirely:
|
| 8 |
-
- beatapi/space-bunny-alpha
|
| 9 |
-
|
| 10 |
-
The BeatAPI id deliberately uses a `beatapi/` prefix (NOT `openai/`) so it can
|
| 11 |
-
never be captured by the OpenRouter catch-all branch in _resolve_llm_params.
|
| 12 |
-
See patch_beatapi_routing.py for the dedicated routing branch.
|
| 13 |
-
|
| 14 |
-
This runs AFTER patch_models.py / patch_new_models.py, which already injected
|
| 15 |
-
the `openai/`-prefixed constants and the LING_3_0_FLASH_FREE_MODEL_ID anchor.
|
| 16 |
-
Idempotent: re-running is a no-op.
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
import ast
|
| 20 |
-
import re
|
| 21 |
-
|
| 22 |
-
IDS_FILE = "/app/agent/core/model_ids.py"
|
| 23 |
-
AGENT_FILE = "/app/backend/routes/agent.py"
|
| 24 |
-
|
| 25 |
-
# ---------- 1. model_ids.py ----------
|
| 26 |
-
with open(IDS_FILE) as f:
|
| 27 |
-
ids = f.read()
|
| 28 |
-
|
| 29 |
-
new_constants = (
|
| 30 |
-
'APODEX_1_1_MINI_FREE_MODEL_ID = "openai/apodex/apodex-1.1-mini:free"\n'
|
| 31 |
-
'LING_3_1_FLASH_MODEL_ID = "openai/inclusionai/ling-3.1-flash"\n'
|
| 32 |
-
'# NOTE: BeatAPI model. NOT an OpenRouter id: `beatapi/` routes to\n'
|
| 33 |
-
'# https://api.beatapi.io/v1 and is authenticated with BEATAPI_API_KEY\n'
|
| 34 |
-
'# (see patch_beatapi_routing.py). Keep this prefix exclusive to BeatAPI.\n'
|
| 35 |
-
'BEATAPI_SPACE_BUNNY_ALPHA_MODEL_ID = "beatapi/space-bunny-alpha"\n'
|
| 36 |
-
)
|
| 37 |
-
|
| 38 |
-
missing = [
|
| 39 |
-
name
|
| 40 |
-
for name in (
|
| 41 |
-
"APODEX_1_1_MINI_FREE_MODEL_ID",
|
| 42 |
-
"LING_3_1_FLASH_MODEL_ID",
|
| 43 |
-
"BEATAPI_SPACE_BUNNY_ALPHA_MODEL_ID",
|
| 44 |
-
)
|
| 45 |
-
if name not in ids
|
| 46 |
-
]
|
| 47 |
-
|
| 48 |
-
if not missing:
|
| 49 |
-
print("OK: model_ids.py already has all 3 new constants (idempotent)")
|
| 50 |
-
else:
|
| 51 |
-
# Anchor: the LING_3_0_FLASH line injected by patch_new_models.py.
|
| 52 |
-
# Fall back to a generic OpenRouter constant line from patch_models.py.
|
| 53 |
-
anchors = [
|
| 54 |
-
'LING_3_0_FLASH_FREE_MODEL_ID = "openai/inclusionai/ling-3.0-flash:free"',
|
| 55 |
-
'NEX_N2_MINI_MODEL_ID = "openai/nex-agi/nex-n2-mini"',
|
| 56 |
-
]
|
| 57 |
-
inserted = False
|
| 58 |
-
for anchor in anchors:
|
| 59 |
-
if anchor in ids:
|
| 60 |
-
ids = ids.replace(anchor, anchor + "\n" + new_constants, 1)
|
| 61 |
-
print(f"OK: added constants to model_ids.py after {anchor.split(' =')[0]}")
|
| 62 |
-
inserted = True
|
| 63 |
-
break
|
| 64 |
-
if not inserted:
|
| 65 |
-
# Last resort: append right after the HOSTED_MODEL_IDS set closes, which
|
| 66 |
-
# is a stable upstream anchor present in every revision.
|
| 67 |
-
m = re.search(r"HOSTED_MODEL_IDS\s*=\s*\{[^}]*\}\n", ids)
|
| 68 |
-
if m:
|
| 69 |
-
ids = ids[: m.end()] + "\n" + new_constants + ids[m.end() :]
|
| 70 |
-
print("OK: added constants to model_ids.py after HOSTED_MODEL_IDS")
|
| 71 |
-
else:
|
| 72 |
-
print("FAIL: no anchor found in model_ids.py")
|
| 73 |
-
raise SystemExit(1)
|
| 74 |
-
|
| 75 |
-
with open(IDS_FILE, "w") as f:
|
| 76 |
-
f.write(ids)
|
| 77 |
-
ast.parse(ids)
|
| 78 |
-
print("OK: model_ids.py written & syntax OK")
|
| 79 |
-
|
| 80 |
-
# ---------- 2. agent.py : imports ----------
|
| 81 |
-
with open(AGENT_FILE) as f:
|
| 82 |
-
ac = f.read()
|
| 83 |
-
|
| 84 |
-
if "APODEX_1_1_MINI_FREE_MODEL_ID" not in ac:
|
| 85 |
-
# patch_models.py rewrote the import block to list every OpenRouter constant.
|
| 86 |
-
imp_anchor = " LING_3_0_FLASH_FREE_MODEL_ID,\n"
|
| 87 |
-
if imp_anchor in ac:
|
| 88 |
-
ac = ac.replace(
|
| 89 |
-
imp_anchor,
|
| 90 |
-
imp_anchor
|
| 91 |
-
+ " APODEX_1_1_MINI_FREE_MODEL_ID,\n"
|
| 92 |
-
+ " LING_3_1_FLASH_MODEL_ID,\n"
|
| 93 |
-
+ " BEATAPI_SPACE_BUNNY_ALPHA_MODEL_ID,\n",
|
| 94 |
-
1,
|
| 95 |
-
)
|
| 96 |
-
print("OK: added 3 model imports to agent.py")
|
| 97 |
-
else:
|
| 98 |
-
print("FAIL: import anchor not found in agent.py")
|
| 99 |
-
raise SystemExit(1)
|
| 100 |
-
|
| 101 |
-
# ---------- 3. agent.py : _available_models ----------
|
| 102 |
-
line_anchor = (
|
| 103 |
-
' {"id": SPACE_BUNNY_ALPHA_MODEL_ID, "label": "stealth/space-bunny-alpha", "recommended": True},\n'
|
| 104 |
-
)
|
| 105 |
-
new_lines = line_anchor + (
|
| 106 |
-
' # OpenRouter models (openai/ prefix -> OpenRouter)\n'
|
| 107 |
-
' {"id": APODEX_1_1_MINI_FREE_MODEL_ID, "label": "apodex/apodex-1.1-mini:free", "recommended": True},\n'
|
| 108 |
-
' {"id": LING_3_1_FLASH_MODEL_ID, "label": "inclusionai/ling-3.1-flash", "recommended": True},\n'
|
| 109 |
-
' # BeatAPI model (beatapi/ prefix -> https://api.beatapi.io/v1)\n'
|
| 110 |
-
' {"id": BEATAPI_SPACE_BUNNY_ALPHA_MODEL_ID, "label": "BeatAPI space-bunny-alpha", "recommended": True},\n'
|
| 111 |
-
)
|
| 112 |
-
|
| 113 |
-
if line_anchor in ac:
|
| 114 |
-
ac = ac.replace(line_anchor, new_lines, 1)
|
| 115 |
-
print("OK: added 3 new models to _available_models()")
|
| 116 |
-
else:
|
| 117 |
-
# The _available_models body was fully rewritten by patch_models.py; match
|
| 118 |
-
# on the generic recommended-Ling line so this stays robust.
|
| 119 |
-
alt = (
|
| 120 |
-
' {"id": LING_3_0_FLASH_FREE_MODEL_ID, "label": '
|
| 121 |
-
'"inclusionai/ling-3.0-flash:free", "recommended": True},\n'
|
| 122 |
-
)
|
| 123 |
-
if alt in ac:
|
| 124 |
-
ac = ac.replace(
|
| 125 |
-
alt,
|
| 126 |
-
alt
|
| 127 |
-
+ ' {"id": APODEX_1_1_MINI_FREE_MODEL_ID, "label": "apodex/apodex-1.1-mini:free", "recommended": True},\n'
|
| 128 |
-
+ ' {"id": LING_3_1_FLASH_MODEL_ID, "label": "inclusionai/ling-3.1-flash", "recommended": True},\n'
|
| 129 |
-
+ ' {"id": BEATAPI_SPACE_BUNNY_ALPHA_MODEL_ID, "label": "BeatAPI space-bunny-alpha", "recommended": True},\n',
|
| 130 |
-
1,
|
| 131 |
-
)
|
| 132 |
-
print("OK: added 3 new models to _available_models() (fallback anchor)")
|
| 133 |
-
else:
|
| 134 |
-
print("FAIL: _available_models() anchor not found in agent.py")
|
| 135 |
-
raise SystemExit(1)
|
| 136 |
-
|
| 137 |
-
with open(AGENT_FILE, "w") as f:
|
| 138 |
-
f.write(ac)
|
| 139 |
-
ast.parse(ac)
|
| 140 |
-
print("OK: agent.py written & syntax OK")
|
| 141 |
-
|
| 142 |
-
else:
|
| 143 |
-
print("OK: agent.py already patched (idempotent)")
|
| 144 |
-
|
| 145 |
-
print("DONE: apodex-1.1-mini:free, ling-3.1-flash, BeatAPI space-bunny-alpha registered")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue.py
DELETED
|
@@ -1,129 +0,0 @@
|
|
| 1 |
-
"""Patch script: Add auto-continue feature for free models in ml-intern.
|
| 2 |
-
|
| 3 |
-
Apply this script during Docker build to enable:
|
| 4 |
-
1. Detection when streaming ends but task incomplete
|
| 5 |
-
2. "Tự động tiếp tục (10s)" button - auto continues after 10s
|
| 6 |
-
3. "Tạm dừng" button - lets user send different content
|
| 7 |
-
|
| 8 |
-
Usage: Run this during Docker build after cloning source.
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
import os
|
| 12 |
-
import re
|
| 13 |
-
|
| 14 |
-
# Paths - adjust if needed
|
| 15 |
-
USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 16 |
-
SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 17 |
-
AGENT_LOOP = "/app/agent/core/agent_loop.py"
|
| 18 |
-
|
| 19 |
-
# =============================================================================
|
| 20 |
-
# Patch 1: useAgentChat.ts
|
| 21 |
-
# =============================================================================
|
| 22 |
-
def patch_use_agent_chat():
|
| 23 |
-
if not os.path.exists(USE_AGENT_CHAT):
|
| 24 |
-
print(f"SKIP: {USE_AGENT_CHAT} not found")
|
| 25 |
-
return
|
| 26 |
-
|
| 27 |
-
with open(USE_AGENT_CHAT, "r", encoding="utf-8") as f:
|
| 28 |
-
content = f.read()
|
| 29 |
-
|
| 30 |
-
# Add useState to imports if missing
|
| 31 |
-
if "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';" not in content:
|
| 32 |
-
content = content.replace(
|
| 33 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 34 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
# Add auto-continue state after callbacksRef
|
| 38 |
-
state_code = '''
|
| 39 |
-
// Auto-continue state for free models when task incomplete
|
| 40 |
-
const autoContinueTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
| 41 |
-
const [showAutoContinue, setShowAutoContinue] = useState(false);
|
| 42 |
-
const [taskIncompleteInfo, setTaskIncompleteInfo] = useState<{
|
| 43 |
-
incompletePlan: Array<{ id: string; content: string; status: string }>;
|
| 44 |
-
} | null>(null);
|
| 45 |
-
'''
|
| 46 |
-
if "autoContinueTimerRef" not in content and "callbacksRef.current = { onReady, onError, onSessionDead }" in content:
|
| 47 |
-
content = content.replace(
|
| 48 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 49 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };" + state_code
|
| 50 |
-
)
|
| 51 |
-
|
| 52 |
-
# Add onTaskIncomplete to SideChannelCallbacks interface
|
| 53 |
-
if "onTaskIncomplete" not in content:
|
| 54 |
-
content = content.replace(
|
| 55 |
-
"onInterrupted: () => void;",
|
| 56 |
-
"onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 57 |
-
)
|
| 58 |
-
|
| 59 |
-
with open(USE_AGENT_CHAT, "w", encoding="utf-8") as f:
|
| 60 |
-
f.write(content)
|
| 61 |
-
print(f"OK: Patched {USE_AGENT_CHAT}")
|
| 62 |
-
|
| 63 |
-
# =============================================================================
|
| 64 |
-
# Patch 2: sse-chat-transport.ts
|
| 65 |
-
# =============================================================================
|
| 66 |
-
def patch_sse_transport():
|
| 67 |
-
if not os.path.exists(SSE_TRANSPORT):
|
| 68 |
-
print(f"SKIP: {SSE_TRANSPORT} not found")
|
| 69 |
-
return
|
| 70 |
-
|
| 71 |
-
with open(SSE_TRANSPORT, "r", encoding="utf-8") as f:
|
| 72 |
-
content = f.read()
|
| 73 |
-
|
| 74 |
-
# Add task_incomplete case
|
| 75 |
-
if "case 'task_incomplete'" not in content:
|
| 76 |
-
task_incomplete_case = ''' case 'task_incomplete':
|
| 77 |
-
sideChannel.onTaskIncomplete(
|
| 78 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 79 |
-
);
|
| 80 |
-
break;
|
| 81 |
-
'''
|
| 82 |
-
# Insert before turn_complete case
|
| 83 |
-
content = content.replace(
|
| 84 |
-
"case 'turn_complete':",
|
| 85 |
-
task_incomplete_case + "\n case 'turn_complete':"
|
| 86 |
-
)
|
| 87 |
-
|
| 88 |
-
with open(SSE_TRANSPORT, "w", encoding="utf-8") as f:
|
| 89 |
-
f.write(content)
|
| 90 |
-
print(f"OK: Patched {SSE_TRANSPORT}")
|
| 91 |
-
|
| 92 |
-
# =============================================================================
|
| 93 |
-
# Patch 3: agent_loop.py
|
| 94 |
-
# =============================================================================
|
| 95 |
-
def patch_agent_loop():
|
| 96 |
-
if not os.path.exists(AGENT_LOOP):
|
| 97 |
-
print(f"SKIP: {AGENT_LOOP} not found")
|
| 98 |
-
return
|
| 99 |
-
|
| 100 |
-
with open(AGENT_LOOP, "r", encoding="utf-8") as f:
|
| 101 |
-
content = f.read()
|
| 102 |
-
|
| 103 |
-
# Add check function if not exists
|
| 104 |
-
check_func = '''
|
| 105 |
-
def _check_task_incomplete(session: Session, llm_result: LLMResult) -> bool:
|
| 106 |
-
"""Check if model stopped but task is incomplete (has unfinished plan items)."""
|
| 107 |
-
if llm_result.tool_calls_acc:
|
| 108 |
-
return False # Has tool calls, not incomplete
|
| 109 |
-
plan = getattr(session, "current_plan", None) or []
|
| 110 |
-
unfinished = [item for item in plan if item.get("status") in ("pending", "in_progress")]
|
| 111 |
-
return len(unfinished) > 0
|
| 112 |
-
'''
|
| 113 |
-
if "_check_task_incomplete" not in content:
|
| 114 |
-
# Insert after LLMResult dataclass
|
| 115 |
-
content = content.replace(
|
| 116 |
-
"class Handlers:",
|
| 117 |
-
check_func + "\n\nclass Handlers:"
|
| 118 |
-
)
|
| 119 |
-
|
| 120 |
-
with open(AGENT_LOOP, "w", encoding="utf-8") as f:
|
| 121 |
-
f.write(content)
|
| 122 |
-
print(f"OK: Patched {AGENT_LOOP}")
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
if __name__ == "__main__":
|
| 126 |
-
patch_use_agent_chat()
|
| 127 |
-
patch_sse_transport()
|
| 128 |
-
patch_agent_loop()
|
| 129 |
-
print("\nDONE: Auto-continue patches applied")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v10.py
DELETED
|
@@ -1,202 +0,0 @@
|
|
| 1 |
-
"""V10: Fixes scope issue with chat.sendMessage.
|
| 2 |
-
|
| 3 |
-
Uses: ref to store plan, useEffect + chatActionsRef to send."""
|
| 4 |
-
import os, re
|
| 5 |
-
|
| 6 |
-
events_ts = "/source/frontend/src/types/events.ts"
|
| 7 |
-
sse_ts = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 8 |
-
hook_ts = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 9 |
-
input_tsx = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 10 |
-
session_tsx = "/source/frontend/src/components/SessionChat.tsx"
|
| 11 |
-
agent_py = "/app/agent/core/agent_loop.py"
|
| 12 |
-
|
| 13 |
-
def patch_events():
|
| 14 |
-
with open(events_ts) as f: c = f.read()
|
| 15 |
-
if "'task_incomplete'" not in c:
|
| 16 |
-
c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
|
| 17 |
-
with open(events_ts, 'w') as f: f.write(c)
|
| 18 |
-
print("OK: events.ts")
|
| 19 |
-
|
| 20 |
-
def patch_sse():
|
| 21 |
-
with open(sse_ts) as f: c = f.read()
|
| 22 |
-
if "case 'task_incomplete'" not in c:
|
| 23 |
-
case_block = """ case 'task_incomplete':
|
| 24 |
-
(sideChannel as any).onTaskIncomplete(
|
| 25 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 26 |
-
);
|
| 27 |
-
break;
|
| 28 |
-
|
| 29 |
-
default:"""
|
| 30 |
-
c = c.replace("\n default:", "\n" + case_block)
|
| 31 |
-
with open(sse_ts, 'w') as f: f.write(c)
|
| 32 |
-
print("OK: sse.ts")
|
| 33 |
-
|
| 34 |
-
def patch_hook():
|
| 35 |
-
with open(hook_ts) as f:
|
| 36 |
-
lines = f.readlines()
|
| 37 |
-
|
| 38 |
-
# Find key insertion points by line number
|
| 39 |
-
result = []
|
| 40 |
-
state_added = False
|
| 41 |
-
handler_added = False
|
| 42 |
-
effect_added = False
|
| 43 |
-
cancel_added = False
|
| 44 |
-
return_added = False
|
| 45 |
-
|
| 46 |
-
for i, line in enumerate(lines):
|
| 47 |
-
# 1. Add useState import
|
| 48 |
-
if "import { useCallback, useEffect, useMemo, useRef } from 'react';" in line and "useState" not in line:
|
| 49 |
-
line = line.replace("useRef }", "useRef, useState }")
|
| 50 |
-
|
| 51 |
-
# 2. Add state after callbacksRef
|
| 52 |
-
if "callbacksRef.current = { onReady, onError, onSessionDead };" in line and not state_added:
|
| 53 |
-
result.append(line)
|
| 54 |
-
result.append(' // Auto-continue for free models\n')
|
| 55 |
-
result.append(' const [_showAc, _setShowAc] = useState(false);\n')
|
| 56 |
-
result.append(' const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n')
|
| 57 |
-
state_added = True
|
| 58 |
-
continue
|
| 59 |
-
|
| 60 |
-
# 3. Add handler in sideChannel's onSessionUpdate
|
| 61 |
-
if "onSessionUpdate: (data) => {" in line and not handler_added:
|
| 62 |
-
result.append(line)
|
| 63 |
-
result.append(' // Auto-continue: check for ac_plan\n')
|
| 64 |
-
result.append(' if ((data as any).ac_plan) {\n')
|
| 65 |
-
result.append(' _acRef.current = (data as any).ac_plan;\n')
|
| 66 |
-
result.append(' _setShowAc(true);\n')
|
| 67 |
-
result.append(' return;\n')
|
| 68 |
-
result.append(' }\n')
|
| 69 |
-
handler_added = True
|
| 70 |
-
continue
|
| 71 |
-
|
| 72 |
-
# 4. Add cancel function + effect before return
|
| 73 |
-
stripped = line.strip()
|
| 74 |
-
if stripped == "return {" and not cancel_added:
|
| 75 |
-
result.append(' // Auto-continue cancel\n')
|
| 76 |
-
result.append(' const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n')
|
| 77 |
-
cancel_added = True
|
| 78 |
-
# Add effect that uses chatActionsRef to send message
|
| 79 |
-
result.append('\n')
|
| 80 |
-
result.append(' // Auto-continue effect: 10s timer, sends via chatActionsRef\n')
|
| 81 |
-
result.append(' useEffect(() => {\n')
|
| 82 |
-
result.append(' if (!_showAc) return;\n')
|
| 83 |
-
result.append(' const plan = _acRef.current;\n')
|
| 84 |
-
result.append(' const timer = setTimeout(() => {\n')
|
| 85 |
-
result.append(' const s = chatActionsRef.current.setMessages;\n')
|
| 86 |
-
result.append(' const m = chatActionsRef.current.messages;\n')
|
| 87 |
-
result.append(' if (s && plan.length > 0) {\n')
|
| 88 |
-
result.append(" const t = plan.map(i => `- ${i.content}`).join('\\\\n');\n")
|
| 89 |
-
result.append(" s([...m, { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: `[TIẾP TỤC] Task chưa hoàn thành:\\\\n${t}` }], content: '' }]);\n")
|
| 90 |
-
result.append(' }\n')
|
| 91 |
-
result.append(' _setShowAc(false);\n')
|
| 92 |
-
result.append(' }, 10000);\n')
|
| 93 |
-
result.append(' return () => clearTimeout(timer);\n')
|
| 94 |
-
result.append(' }, [_showAc]);\n')
|
| 95 |
-
effect_added = True
|
| 96 |
-
|
| 97 |
-
# 5. Return values
|
| 98 |
-
if "refreshMessages," in line and not return_added:
|
| 99 |
-
result.append(line)
|
| 100 |
-
result.append(' _showAc,\n')
|
| 101 |
-
result.append(' _acCancel,\n')
|
| 102 |
-
return_added = True
|
| 103 |
-
continue
|
| 104 |
-
|
| 105 |
-
result.append(line)
|
| 106 |
-
|
| 107 |
-
with open(hook_ts, 'w') as f: f.writelines(result)
|
| 108 |
-
print(f"OK: useAgentChat.ts (added={state_added}/{handler_added}/{cancel_added}/{effect_added}/{return_added})")
|
| 109 |
-
|
| 110 |
-
def patch_chat_input():
|
| 111 |
-
with open(input_tsx) as f: c = f.read()
|
| 112 |
-
if "Button" not in c:
|
| 113 |
-
c = c.replace(" Tooltip,", " Tooltip,\n Button,")
|
| 114 |
-
|
| 115 |
-
if "_showAc" not in c:
|
| 116 |
-
old_iface = """interface ChatInputProps {
|
| 117 |
-
sessionId?: string;
|
| 118 |
-
initialModelPath?: string | null;
|
| 119 |
-
onSend: (text: string) => void;
|
| 120 |
-
onStop?: () => void;
|
| 121 |
-
onDatasetUploaded?: () => Promise<boolean> | boolean;
|
| 122 |
-
isProcessing?: boolean;
|
| 123 |
-
disabled?: boolean;
|
| 124 |
-
placeholder?: string;
|
| 125 |
-
}"""
|
| 126 |
-
new_iface = """interface ChatInputProps {
|
| 127 |
-
sessionId?: string;
|
| 128 |
-
initialModelPath?: string | null;
|
| 129 |
-
onSend: (text: string) => void;
|
| 130 |
-
onStop?: () => void;
|
| 131 |
-
onDatasetUploaded?: () => Promise<boolean> | boolean;
|
| 132 |
-
isProcessing?: boolean;
|
| 133 |
-
disabled?: boolean;
|
| 134 |
-
placeholder?: string;
|
| 135 |
-
_showAc?: boolean;
|
| 136 |
-
_acCancel?: () => void;
|
| 137 |
-
}"""
|
| 138 |
-
c = c.replace(old_iface, new_iface)
|
| 139 |
-
old_props = "placeholder = 'Ask anything...' }: ChatInputProps) {"
|
| 140 |
-
new_props = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
|
| 141 |
-
c = c.replace(old_props, new_props)
|
| 142 |
-
|
| 143 |
-
if "T\u1ea1m d\u1eebng" not in c:
|
| 144 |
-
btn = """
|
| 145 |
-
{_showAc && _acCancel && (
|
| 146 |
-
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
|
| 147 |
-
<Button variant="outlined" size="small" onClick={_acCancel}
|
| 148 |
-
sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
|
| 149 |
-
T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
|
| 150 |
-
</Button>
|
| 151 |
-
</Box>
|
| 152 |
-
)}
|
| 153 |
-
"""
|
| 154 |
-
if "<JobsUpgradeDialog" in c:
|
| 155 |
-
c = c.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
|
| 156 |
-
|
| 157 |
-
with open(input_tsx, 'w') as f: f.write(c)
|
| 158 |
-
print("OK: ChatInput.tsx")
|
| 159 |
-
|
| 160 |
-
def patch_session():
|
| 161 |
-
if not os.path.exists(session_tsx):
|
| 162 |
-
print(f"SKIP: {session_tsx}"); return
|
| 163 |
-
with open(session_tsx) as f: c = f.read()
|
| 164 |
-
if "_showAc" not in c:
|
| 165 |
-
c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
|
| 166 |
-
with open(session_tsx, 'w') as f: f.write(c)
|
| 167 |
-
print("OK: SessionChat.tsx")
|
| 168 |
-
|
| 169 |
-
def patch_agent():
|
| 170 |
-
if not os.path.exists(agent_py):
|
| 171 |
-
print(f"SKIP: {agent_py}"); return
|
| 172 |
-
with open(agent_py) as f: c = f.read()
|
| 173 |
-
|
| 174 |
-
if "_unfinished_plan" not in c:
|
| 175 |
-
fn = """
|
| 176 |
-
def _unfinished_plan(s: Session) -> list[dict[str, str]]:
|
| 177 |
-
p = getattr(s, "current_plan", None) or []
|
| 178 |
-
return [it for it in p if it.get("status") in ("pending", "in_progress")]
|
| 179 |
-
"""
|
| 180 |
-
c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
|
| 181 |
-
|
| 182 |
-
if "ac_plan" not in c:
|
| 183 |
-
check = """
|
| 184 |
-
if not llm_result.tool_calls_acc and llm_result.content:
|
| 185 |
-
unfinished = _unfinished_plan(session)
|
| 186 |
-
if unfinished:
|
| 187 |
-
current_ac = getattr(session, "auto_approval", None)
|
| 188 |
-
await session.send_event(Event(event_type="session_update", data={
|
| 189 |
-
"ac_plan": unfinished,
|
| 190 |
-
"auto_approval": current_ac,
|
| 191 |
-
}))
|
| 192 |
-
"""
|
| 193 |
-
marker = " # -- End of turn --"
|
| 194 |
-
if marker in c:
|
| 195 |
-
c = c.replace(marker, check + "\n" + marker)
|
| 196 |
-
|
| 197 |
-
with open(agent_py, 'w') as f: f.write(c)
|
| 198 |
-
print("OK: agent_loop.py")
|
| 199 |
-
|
| 200 |
-
if __name__ == "__main__":
|
| 201 |
-
patch_events(); patch_sse(); patch_hook(); patch_chat_input(); patch_session(); patch_agent()
|
| 202 |
-
print("\nDONE: V10")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v11.py
DELETED
|
@@ -1,185 +0,0 @@
|
|
| 1 |
-
"""V11: Auto-continue feature - clean single-file patch.
|
| 2 |
-
|
| 3 |
-
Backend: agent_loop.py detects model stopped mid-task, sends event.
|
| 4 |
-
Frontend: useAgentChat listens via onSessionUpdate, shows pause button.
|
| 5 |
-
All TS errors avoided via (as any) casts where needed.
|
| 6 |
-
"""
|
| 7 |
-
import os
|
| 8 |
-
|
| 9 |
-
ROOT = "/source/frontend/src"
|
| 10 |
-
EVENTS = f"{ROOT}/types/events.ts"
|
| 11 |
-
SSE = f"{ROOT}/lib/sse-chat-transport.ts"
|
| 12 |
-
HOOK = f"{ROOT}/hooks/useAgentChat.ts"
|
| 13 |
-
INPUT = f"{ROOT}/components/Chat/ChatInput.tsx"
|
| 14 |
-
SESS = f"{ROOT}/components/SessionChat.tsx"
|
| 15 |
-
AGENT_LOOP = "/app/agent/core/agent_loop.py"
|
| 16 |
-
|
| 17 |
-
def p(f, s):
|
| 18 |
-
print(f"OK: {f}" if s else f"FAIL: {f}")
|
| 19 |
-
|
| 20 |
-
# --- PATCH 1: events.ts ---
|
| 21 |
-
with open(EVENTS) as f: c = f.read()
|
| 22 |
-
r1 = "'task_incomplete'" not in c
|
| 23 |
-
if r1:
|
| 24 |
-
c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
|
| 25 |
-
with open(EVENTS, 'w') as f: f.write(c)
|
| 26 |
-
p("events.ts", r1)
|
| 27 |
-
|
| 28 |
-
# --- PATCH 2: sse-chat-transport.ts ---
|
| 29 |
-
with open(SSE) as f: c = f.read()
|
| 30 |
-
r2 = "case 'task_incomplete'" not in c
|
| 31 |
-
if r2:
|
| 32 |
-
block = """ case 'task_incomplete':
|
| 33 |
-
(sideChannel as any).onTaskIncomplete(
|
| 34 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 35 |
-
);
|
| 36 |
-
break;
|
| 37 |
-
|
| 38 |
-
default:"""
|
| 39 |
-
c = c.replace("\n default:", "\n" + block)
|
| 40 |
-
with open(SSE, 'w') as f: f.write(c)
|
| 41 |
-
p("sse.ts", r2)
|
| 42 |
-
|
| 43 |
-
# --- PATCH 3: useAgentChat.ts ---
|
| 44 |
-
with open(HOOK) as f: lines = f.readlines()
|
| 45 |
-
r3 = True
|
| 46 |
-
new_lines = []
|
| 47 |
-
state_done = False
|
| 48 |
-
handler_done = False
|
| 49 |
-
efx_done = False
|
| 50 |
-
ret_done = False
|
| 51 |
-
|
| 52 |
-
for line in lines:
|
| 53 |
-
if "import { useCallback, useEffect, useMemo, useRef } from 'react';" in line:
|
| 54 |
-
line = line.replace("useRef }", "useRef, useState }")
|
| 55 |
-
|
| 56 |
-
if "callbacksRef.current = { onReady, onError, onSessionDead };" in line and not state_done:
|
| 57 |
-
new_lines.append(line)
|
| 58 |
-
new_lines.append(" // Auto-continue state\n")
|
| 59 |
-
new_lines.append(" const [_showAc, _setShowAc] = useState(false);\n")
|
| 60 |
-
new_lines.append(" const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n")
|
| 61 |
-
state_done = True
|
| 62 |
-
continue
|
| 63 |
-
|
| 64 |
-
if "onSessionUpdate: (data) => {" in line and not handler_done:
|
| 65 |
-
new_lines.append(line)
|
| 66 |
-
new_lines.append(" // Auto-continue check\n")
|
| 67 |
-
new_lines.append(" if ((data as any).ac_plan) {\n")
|
| 68 |
-
new_lines.append(" _acRef.current = (data as any).ac_plan;\n")
|
| 69 |
-
new_lines.append(" _setShowAc(true);\n")
|
| 70 |
-
new_lines.append(" return;\n")
|
| 71 |
-
new_lines.append(" }\n")
|
| 72 |
-
handler_done = True
|
| 73 |
-
continue
|
| 74 |
-
|
| 75 |
-
stripped = line.strip()
|
| 76 |
-
if stripped == "return {" and not efx_done:
|
| 77 |
-
new_lines.append(" const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n")
|
| 78 |
-
new_lines.append(" useEffect(() => {\n")
|
| 79 |
-
new_lines.append(" if (!_showAc) return;\n")
|
| 80 |
-
new_lines.append(" const plan = _acRef.current;\n")
|
| 81 |
-
new_lines.append(" const timer = setTimeout(() => {\n")
|
| 82 |
-
new_lines.append(" const s = chatActionsRef.current.setMessages;\n")
|
| 83 |
-
new_lines.append(" const m = chatActionsRef.current.messages;\n")
|
| 84 |
-
new_lines.append(" if (s && plan.length > 0) {\n")
|
| 85 |
-
new_lines.append(" const t = plan.map(i => `- ${i.content}`).join('\\n');\n")
|
| 86 |
-
new_lines.append(" const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: `[TIEP TUC] Task chua hoan thanh:\\n${t}` }], content: '' };\n")
|
| 87 |
-
new_lines.append(" s([...m, nu]);\n")
|
| 88 |
-
new_lines.append(" }\n")
|
| 89 |
-
new_lines.append(" _setShowAc(false);\n")
|
| 90 |
-
new_lines.append(" }, 10000);\n")
|
| 91 |
-
new_lines.append(" return () => clearTimeout(timer);\n")
|
| 92 |
-
new_lines.append(" }, [_showAc]);\n")
|
| 93 |
-
efx_done = True
|
| 94 |
-
|
| 95 |
-
if "refreshMessages," in line and not ret_done:
|
| 96 |
-
new_lines.append(line)
|
| 97 |
-
new_lines.append(" _showAc,\n")
|
| 98 |
-
new_lines.append(" _acCancel,\n")
|
| 99 |
-
ret_done = True
|
| 100 |
-
continue
|
| 101 |
-
|
| 102 |
-
new_lines.append(line)
|
| 103 |
-
|
| 104 |
-
if not all([state_done, handler_done, efx_done, ret_done]):
|
| 105 |
-
r3 = False
|
| 106 |
-
print(f" MISSING: state={state_done} handler={handler_done} effect={efx_done} ret={ret_done}")
|
| 107 |
-
else:
|
| 108 |
-
with open(HOOK, 'w') as f: f.writelines(new_lines)
|
| 109 |
-
p("useAgentChat.ts", r3)
|
| 110 |
-
|
| 111 |
-
# --- PATCH 4: ChatInput.tsx ---
|
| 112 |
-
with open(INPUT) as f: c = f.read()
|
| 113 |
-
r4 = True
|
| 114 |
-
if "Button" not in c:
|
| 115 |
-
c = c.replace(" Tooltip,", " Tooltip,\n Button,")
|
| 116 |
-
|
| 117 |
-
if "_showAc" not in c:
|
| 118 |
-
old = "placeholder?: string;"
|
| 119 |
-
new = "placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;"
|
| 120 |
-
c = c.replace(old, new)
|
| 121 |
-
|
| 122 |
-
old2 = "placeholder = 'Ask anything...' }: ChatInputProps) {"
|
| 123 |
-
new2 = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
|
| 124 |
-
c = c.replace(old2, new2)
|
| 125 |
-
|
| 126 |
-
btn = """
|
| 127 |
-
{_showAc && _acCancel && (
|
| 128 |
-
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
|
| 129 |
-
<Button variant="outlined" size="small" onClick={_acCancel}
|
| 130 |
-
sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
|
| 131 |
-
Tam dung tu dong tiep tuc (10s)
|
| 132 |
-
</Button>
|
| 133 |
-
</Box>
|
| 134 |
-
)}
|
| 135 |
-
"""
|
| 136 |
-
c = c.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
|
| 137 |
-
else:
|
| 138 |
-
r4 = False
|
| 139 |
-
|
| 140 |
-
with open(INPUT, 'w') as f: f.write(c)
|
| 141 |
-
p("ChatInput.tsx", r4)
|
| 142 |
-
|
| 143 |
-
# --- PATCH 5: SessionChat.tsx ---
|
| 144 |
-
if os.path.exists(SESS):
|
| 145 |
-
with open(SESS) as f: c = f.read()
|
| 146 |
-
r5 = "_showAc" not in c
|
| 147 |
-
if r5:
|
| 148 |
-
c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
|
| 149 |
-
with open(SESS, 'w') as f: f.write(c)
|
| 150 |
-
p("SessionChat.tsx", r5)
|
| 151 |
-
else:
|
| 152 |
-
print("SKIP: SessionChat.tsx")
|
| 153 |
-
|
| 154 |
-
# --- PATCH 6: agent_loop.py ---
|
| 155 |
-
if os.path.exists(AGENT_LOOP):
|
| 156 |
-
with open(AGENT_LOOP) as f: c = f.read()
|
| 157 |
-
r6 = True
|
| 158 |
-
if "_unfinished_plan" not in c:
|
| 159 |
-
fn = """
|
| 160 |
-
def _unfinished_plan(s):
|
| 161 |
-
p = getattr(s, "current_plan", None) or []
|
| 162 |
-
return [it for it in p if it.get("status") in ("pending", "in_progress")]
|
| 163 |
-
"""
|
| 164 |
-
c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
|
| 165 |
-
|
| 166 |
-
if "ac_plan" not in c:
|
| 167 |
-
check = """
|
| 168 |
-
if not llm_result.tool_calls_acc and llm_result.content:
|
| 169 |
-
unfinished = _unfinished_plan(session)
|
| 170 |
-
if unfinished:
|
| 171 |
-
await session.send_event(Event(event_type="session_update", data={
|
| 172 |
-
"ac_plan": unfinished,
|
| 173 |
-
}))
|
| 174 |
-
"""
|
| 175 |
-
marker = " # -- End of turn --"
|
| 176 |
-
c = c.replace(marker, check + "\n" + marker)
|
| 177 |
-
else:
|
| 178 |
-
r6 = False
|
| 179 |
-
|
| 180 |
-
with open(AGENT_LOOP, 'w') as f: f.write(c)
|
| 181 |
-
p("agent_loop.py", r6)
|
| 182 |
-
else:
|
| 183 |
-
print("SKIP: agent_loop.py")
|
| 184 |
-
|
| 185 |
-
print("\nDONE: V11")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v12.py
DELETED
|
@@ -1,209 +0,0 @@
|
|
| 1 |
-
"""V12: Auto-continue feature - final fix.
|
| 2 |
-
|
| 3 |
-
Key fixes for V11 bugs:
|
| 4 |
-
1. Button import: check exact MUI import format
|
| 5 |
-
2. useAgentChat: match two-line `return {\n messages: chat.messages,` instead of bare `return {`
|
| 6 |
-
3. Use \u unicode escapes for Vietnamese text
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import os
|
| 10 |
-
|
| 11 |
-
ROOT = "/source/frontend/src"
|
| 12 |
-
EVENTS = f"{ROOT}/types/events.ts"
|
| 13 |
-
SSE = f"{ROOT}/lib/sse-chat-transport.ts"
|
| 14 |
-
HOOK = f"{ROOT}/hooks/useAgentChat.ts"
|
| 15 |
-
INPUT = f"{ROOT}/components/Chat/ChatInput.tsx"
|
| 16 |
-
SESS = f"{ROOT}/components/SessionChat.tsx"
|
| 17 |
-
AGENT = "/app/agent/core/agent_loop.py"
|
| 18 |
-
|
| 19 |
-
def ok(f):
|
| 20 |
-
print(f"OK: {f}")
|
| 21 |
-
|
| 22 |
-
# --- 1. events.ts: add task_incomplete ---
|
| 23 |
-
with open(EVENTS) as f:
|
| 24 |
-
c = f.read()
|
| 25 |
-
if "'task_incomplete'" not in c:
|
| 26 |
-
c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
|
| 27 |
-
with open(EVENTS, 'w') as f:
|
| 28 |
-
f.write(c)
|
| 29 |
-
ok("events.ts")
|
| 30 |
-
|
| 31 |
-
# --- 2. sse-chat-transport.ts: add case handler ---
|
| 32 |
-
with open(SSE) as f:
|
| 33 |
-
c = f.read()
|
| 34 |
-
if "case 'task_incomplete'" not in c:
|
| 35 |
-
block = """ case 'task_incomplete':
|
| 36 |
-
(sideChannel as any).onTaskIncomplete(
|
| 37 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 38 |
-
);
|
| 39 |
-
break;
|
| 40 |
-
|
| 41 |
-
default:"""
|
| 42 |
-
c = c.replace("\n default:", "\n" + block)
|
| 43 |
-
with open(SSE, 'w') as f:
|
| 44 |
-
f.write(c)
|
| 45 |
-
ok("sse.ts")
|
| 46 |
-
|
| 47 |
-
# --- 3. useAgentChat.ts: add state, handler, effect, return ---
|
| 48 |
-
with open(HOOK) as f:
|
| 49 |
-
content = f.read()
|
| 50 |
-
|
| 51 |
-
# Add useState to import
|
| 52 |
-
if "useState" not in content:
|
| 53 |
-
content = content.replace(
|
| 54 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 55 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 56 |
-
)
|
| 57 |
-
|
| 58 |
-
# Add state after callbacksRef line
|
| 59 |
-
state_block = """\
|
| 60 |
-
// Auto-continue state
|
| 61 |
-
const [_showAc, _setShowAc] = useState(false);
|
| 62 |
-
const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);
|
| 63 |
-
"""
|
| 64 |
-
if "_showAc" not in content:
|
| 65 |
-
content = content.replace(
|
| 66 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 67 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };\n" + state_block
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
# Add handler inside onSessionUpdate
|
| 71 |
-
handler_code = """\
|
| 72 |
-
// Auto-continue check
|
| 73 |
-
if ((data as any).ac_plan) {
|
| 74 |
-
_acRef.current = (data as any).ac_plan;
|
| 75 |
-
_setShowAc(true);
|
| 76 |
-
return;
|
| 77 |
-
}
|
| 78 |
-
"""
|
| 79 |
-
if "ac_plan" not in content:
|
| 80 |
-
content = content.replace(
|
| 81 |
-
"onSessionUpdate: (data) => {",
|
| 82 |
-
"onSessionUpdate: (data) => {\n" + handler_code
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
# Add cancel + effect BEFORE the two-line return marker
|
| 86 |
-
cancel_effect = """\
|
| 87 |
-
const _acCancel = useCallback(() => { _setShowAc(false); }, []);
|
| 88 |
-
useEffect(() => {
|
| 89 |
-
if (!_showAc) return;
|
| 90 |
-
const plan = _acRef.current;
|
| 91 |
-
const timer = setTimeout(() => {
|
| 92 |
-
const s = chatActionsRef.current.setMessages;
|
| 93 |
-
const m = chatActionsRef.current.messages;
|
| 94 |
-
if (s && plan.length > 0) {
|
| 95 |
-
const t = plan.map((i: { content: string }) => `- ${i.content}`).join('\\n');
|
| 96 |
-
const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: `[TIEP TUC] Task chua hoan thanh:\\n${t}` }], content: '' };
|
| 97 |
-
s([...m, nu]);
|
| 98 |
-
}
|
| 99 |
-
_setShowAc(false);
|
| 100 |
-
}, 10000);
|
| 101 |
-
return () => clearTimeout(timer);
|
| 102 |
-
}, [_showAc]);
|
| 103 |
-
|
| 104 |
-
"""
|
| 105 |
-
if "_acCancel" not in content:
|
| 106 |
-
# Match the EXACT two-line return pattern
|
| 107 |
-
content = content.replace(
|
| 108 |
-
" return {\n messages: chat.messages,",
|
| 109 |
-
cancel_effect + " return {\n messages: chat.messages,"
|
| 110 |
-
)
|
| 111 |
-
|
| 112 |
-
# Add return values
|
| 113 |
-
if "_showAc," not in content:
|
| 114 |
-
content = content.replace(
|
| 115 |
-
"refreshMessages,\n };",
|
| 116 |
-
"refreshMessages,\n _showAc,\n _acCancel,\n };"
|
| 117 |
-
)
|
| 118 |
-
|
| 119 |
-
with open(HOOK, 'w') as f:
|
| 120 |
-
f.write(content)
|
| 121 |
-
ok("useAgentChat.ts")
|
| 122 |
-
|
| 123 |
-
# --- 4. ChatInput.tsx: add Button + props + pause button ---
|
| 124 |
-
with open(INPUT) as f:
|
| 125 |
-
content = f.read()
|
| 126 |
-
|
| 127 |
-
# Add Button to MUI imports - match the actual format
|
| 128 |
-
if "Button" not in content:
|
| 129 |
-
content = content.replace(
|
| 130 |
-
" Tooltip,",
|
| 131 |
-
" Tooltip,\n Button,"
|
| 132 |
-
)
|
| 133 |
-
|
| 134 |
-
# Add props to interface
|
| 135 |
-
if "_showAc" not in content:
|
| 136 |
-
content = content.replace(
|
| 137 |
-
"placeholder?: string;",
|
| 138 |
-
"placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;"
|
| 139 |
-
)
|
| 140 |
-
|
| 141 |
-
# Add to destructured function params
|
| 142 |
-
if "_showAc" not in content or "_acCancel" not in content:
|
| 143 |
-
content = content.replace(
|
| 144 |
-
"placeholder = 'Ask anything...' }: ChatInputProps) {",
|
| 145 |
-
"placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
|
| 146 |
-
)
|
| 147 |
-
|
| 148 |
-
# Add pause button before JobsUpgradeDialog
|
| 149 |
-
if "Tam dung" not in content:
|
| 150 |
-
btn = """
|
| 151 |
-
{_showAc && _acCancel && (
|
| 152 |
-
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
|
| 153 |
-
<Button variant="outlined" size="small" onClick={_acCancel}
|
| 154 |
-
sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
|
| 155 |
-
T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
|
| 156 |
-
</Button>
|
| 157 |
-
</Box>
|
| 158 |
-
)}
|
| 159 |
-
"""
|
| 160 |
-
content = content.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
|
| 161 |
-
|
| 162 |
-
with open(INPUT, 'w') as f:
|
| 163 |
-
f.write(content)
|
| 164 |
-
ok("ChatInput.tsx")
|
| 165 |
-
|
| 166 |
-
# --- 5. SessionChat.tsx: pass props ---
|
| 167 |
-
if os.path.exists(SESS):
|
| 168 |
-
with open(SESS) as f:
|
| 169 |
-
c = f.read()
|
| 170 |
-
if "_showAc" not in c:
|
| 171 |
-
c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
|
| 172 |
-
with open(SESS, 'w') as f:
|
| 173 |
-
f.write(c)
|
| 174 |
-
ok("SessionChat.tsx")
|
| 175 |
-
|
| 176 |
-
# --- 6. agent_loop.py: detection ---
|
| 177 |
-
if os.path.exists(AGENT):
|
| 178 |
-
with open(AGENT) as f:
|
| 179 |
-
c = f.read()
|
| 180 |
-
|
| 181 |
-
if "_unfinished_plan" not in c:
|
| 182 |
-
fn = """
|
| 183 |
-
|
| 184 |
-
def _unfinished_plan(s):
|
| 185 |
-
p = getattr(s, "current_plan", None) or []
|
| 186 |
-
return [it for it in p if it.get("status") in ("pending", "in_progress")]
|
| 187 |
-
"""
|
| 188 |
-
c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
|
| 189 |
-
|
| 190 |
-
if "ac_plan" not in c:
|
| 191 |
-
check = """
|
| 192 |
-
if not llm_result.tool_calls_acc and llm_result.content:
|
| 193 |
-
unfinished = _unfinished_plan(session)
|
| 194 |
-
if unfinished:
|
| 195 |
-
await session.send_event(Event(event_type="session_update", data={
|
| 196 |
-
"ac_plan": unfinished,
|
| 197 |
-
}))
|
| 198 |
-
"""
|
| 199 |
-
marker = " # -- End of turn --"
|
| 200 |
-
if marker in c:
|
| 201 |
-
c = c.replace(marker, check + marker)
|
| 202 |
-
|
| 203 |
-
with open(AGENT, 'w') as f:
|
| 204 |
-
f.write(c)
|
| 205 |
-
ok("agent_loop.py")
|
| 206 |
-
else:
|
| 207 |
-
print("SKIP: agent_loop.py (backend)")
|
| 208 |
-
|
| 209 |
-
print("\nDONE: V12")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v13.py
DELETED
|
@@ -1,197 +0,0 @@
|
|
| 1 |
-
"""V13: Auto-continue feature - fixed SyntaxError, no raw \u in Python source"""
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
|
| 5 |
-
ROOT = "/source/frontend/src"
|
| 6 |
-
EVENTS = f"{ROOT}/types/events.ts"
|
| 7 |
-
SSE = f"{ROOT}/lib/sse-chat-transport.ts"
|
| 8 |
-
HOOK = f"{ROOT}/hooks/useAgentChat.ts"
|
| 9 |
-
INPUT = f"{ROOT}/components/Chat/ChatInput.tsx"
|
| 10 |
-
SESS = f"{ROOT}/components/SessionChat.tsx"
|
| 11 |
-
AGENT = "/app/agent/core/agent_loop.py"
|
| 12 |
-
|
| 13 |
-
# Vietnamese text as hex to avoid \u issues in Python source
|
| 14 |
-
PAUSE_TEXT = "T\\u1ea1m d\\u1eebng t\\u1ef1 \\u0111\\u1ed9ng ti\\u1ebfp t\\u1ee5c (10s)"
|
| 15 |
-
CONTINUE_TEXT = "[TIEP TUC] Task chua hoan thanh:"
|
| 16 |
-
|
| 17 |
-
def ok(f):
|
| 18 |
-
print(f"OK: {f}")
|
| 19 |
-
|
| 20 |
-
# --- 1. events.ts ---
|
| 21 |
-
with open(EVENTS) as f:
|
| 22 |
-
c = f.read()
|
| 23 |
-
if "'task_incomplete'" not in c:
|
| 24 |
-
c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
|
| 25 |
-
with open(EVENTS, 'w') as f:
|
| 26 |
-
f.write(c)
|
| 27 |
-
ok("events.ts")
|
| 28 |
-
|
| 29 |
-
# --- 2. sse.ts ---
|
| 30 |
-
with open(SSE) as f:
|
| 31 |
-
c = f.read()
|
| 32 |
-
if "case 'task_incomplete'" not in c:
|
| 33 |
-
block = """ case 'task_incomplete':
|
| 34 |
-
(sideChannel as any).onTaskIncomplete(
|
| 35 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 36 |
-
);
|
| 37 |
-
break;
|
| 38 |
-
|
| 39 |
-
default:"""
|
| 40 |
-
c = c.replace("\n default:", "\n" + block)
|
| 41 |
-
with open(SSE, 'w') as f:
|
| 42 |
-
f.write(c)
|
| 43 |
-
ok("sse.ts")
|
| 44 |
-
|
| 45 |
-
# --- 3. useAgentChat.ts ---
|
| 46 |
-
with open(HOOK) as f:
|
| 47 |
-
content = f.read()
|
| 48 |
-
|
| 49 |
-
if "useState" not in content:
|
| 50 |
-
content = content.replace(
|
| 51 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 52 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 53 |
-
)
|
| 54 |
-
|
| 55 |
-
state_block = """\
|
| 56 |
-
// Auto-continue state
|
| 57 |
-
const [_showAc, _setShowAc] = useState(false);
|
| 58 |
-
const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);
|
| 59 |
-
"""
|
| 60 |
-
if "_showAc" not in content:
|
| 61 |
-
content = content.replace(
|
| 62 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 63 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };\n" + state_block
|
| 64 |
-
)
|
| 65 |
-
|
| 66 |
-
handler_code = """\
|
| 67 |
-
// Auto-continue check
|
| 68 |
-
if ((data as any).ac_plan) {
|
| 69 |
-
_acRef.current = (data as any).ac_plan;
|
| 70 |
-
_setShowAc(true);
|
| 71 |
-
return;
|
| 72 |
-
}
|
| 73 |
-
"""
|
| 74 |
-
if "ac_plan" not in content:
|
| 75 |
-
content = content.replace(
|
| 76 |
-
"onSessionUpdate: (data) => {",
|
| 77 |
-
"onSessionUpdate: (data) => {\n" + handler_code
|
| 78 |
-
)
|
| 79 |
-
|
| 80 |
-
cancel_effect = """\
|
| 81 |
-
const _acCancel = useCallback(() => { _setShowAc(false); }, []);
|
| 82 |
-
useEffect(() => {
|
| 83 |
-
if (!_showAc) return;
|
| 84 |
-
const plan = _acRef.current;
|
| 85 |
-
const timer = setTimeout(() => {
|
| 86 |
-
const s = chatActionsRef.current.setMessages;
|
| 87 |
-
const m = chatActionsRef.current.messages;
|
| 88 |
-
if (s && plan.length > 0) {
|
| 89 |
-
const t = plan.map((i: { content: string }) => '- ' + i.content).join('\\\\n');
|
| 90 |
-
const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: '""" + CONTINUE_TEXT + "\\\\n' + t }], content: '' };\n"
|
| 91 |
-
cancel_effect += """\
|
| 92 |
-
s([...m, nu]);
|
| 93 |
-
}
|
| 94 |
-
_setShowAc(false);
|
| 95 |
-
}, 10000);
|
| 96 |
-
return () => clearTimeout(timer);
|
| 97 |
-
}, [_showAc]);
|
| 98 |
-
|
| 99 |
-
"""
|
| 100 |
-
if "_acCancel" not in content:
|
| 101 |
-
content = content.replace(
|
| 102 |
-
" return {\n messages: chat.messages,",
|
| 103 |
-
cancel_effect + " return {\n messages: chat.messages,"
|
| 104 |
-
)
|
| 105 |
-
|
| 106 |
-
if "_showAc," not in content:
|
| 107 |
-
content = content.replace(
|
| 108 |
-
"refreshMessages,\n };",
|
| 109 |
-
"refreshMessages,\n _showAc,\n _acCancel,\n };"
|
| 110 |
-
)
|
| 111 |
-
|
| 112 |
-
with open(HOOK, 'w') as f:
|
| 113 |
-
f.write(content)
|
| 114 |
-
ok("useAgentChat.ts")
|
| 115 |
-
|
| 116 |
-
# --- 4. ChatInput.tsx ---
|
| 117 |
-
with open(INPUT) as f:
|
| 118 |
-
content = f.read()
|
| 119 |
-
|
| 120 |
-
if "Button" not in content:
|
| 121 |
-
content = content.replace(
|
| 122 |
-
" Tooltip,",
|
| 123 |
-
" Tooltip,\n Button,"
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
if "_showAc" not in content:
|
| 127 |
-
content = content.replace(
|
| 128 |
-
"placeholder?: string;",
|
| 129 |
-
"placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;"
|
| 130 |
-
)
|
| 131 |
-
content = content.replace(
|
| 132 |
-
"placeholder = 'Ask anything...' }: ChatInputProps) {",
|
| 133 |
-
"placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
|
| 134 |
-
)
|
| 135 |
-
|
| 136 |
-
pause_marker = "TAM_DUNG"
|
| 137 |
-
if pause_marker not in content:
|
| 138 |
-
btn = """
|
| 139 |
-
{_showAc && _acCancel && (
|
| 140 |
-
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
|
| 141 |
-
<Button variant="outlined" size="small" onClick={_acCancel}
|
| 142 |
-
sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
|
| 143 |
-
""" + PAUSE_TEXT + """
|
| 144 |
-
</Button>
|
| 145 |
-
</Box>
|
| 146 |
-
)}
|
| 147 |
-
"""
|
| 148 |
-
content = content.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
|
| 149 |
-
|
| 150 |
-
with open(INPUT, 'w') as f:
|
| 151 |
-
f.write(content)
|
| 152 |
-
ok("ChatInput.tsx")
|
| 153 |
-
|
| 154 |
-
# --- 5. SessionChat.tsx ---
|
| 155 |
-
if os.path.exists(SESS):
|
| 156 |
-
with open(SESS) as f:
|
| 157 |
-
c = f.read()
|
| 158 |
-
if "_showAc" not in c:
|
| 159 |
-
c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
|
| 160 |
-
with open(SESS, 'w') as f:
|
| 161 |
-
f.write(c)
|
| 162 |
-
ok("SessionChat.tsx")
|
| 163 |
-
|
| 164 |
-
# --- 6. agent_loop.py ---
|
| 165 |
-
if os.path.exists(AGENT):
|
| 166 |
-
with open(AGENT) as f:
|
| 167 |
-
c = f.read()
|
| 168 |
-
|
| 169 |
-
if "_unfinished_plan" not in c:
|
| 170 |
-
fn = """
|
| 171 |
-
|
| 172 |
-
def _unfinished_plan(s):
|
| 173 |
-
p = getattr(s, "current_plan", None) or []
|
| 174 |
-
return [it for it in p if it.get("status") in ("pending", "in_progress")]
|
| 175 |
-
"""
|
| 176 |
-
c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
|
| 177 |
-
|
| 178 |
-
if "ac_plan" not in c:
|
| 179 |
-
check = """
|
| 180 |
-
if not llm_result.tool_calls_acc and llm_result.content:
|
| 181 |
-
unfinished = _unfinished_plan(session)
|
| 182 |
-
if unfinished:
|
| 183 |
-
await session.send_event(Event(event_type="session_update", data={
|
| 184 |
-
"ac_plan": unfinished,
|
| 185 |
-
}))
|
| 186 |
-
"""
|
| 187 |
-
marker = " # -- End of turn --"
|
| 188 |
-
if marker in c:
|
| 189 |
-
c = c.replace(marker, check + marker)
|
| 190 |
-
|
| 191 |
-
with open(AGENT, 'w') as f:
|
| 192 |
-
f.write(c)
|
| 193 |
-
ok("agent_loop.py")
|
| 194 |
-
else:
|
| 195 |
-
print("SKIP: agent_loop.py")
|
| 196 |
-
|
| 197 |
-
print("\nDONE: V13")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v14.py
DELETED
|
@@ -1,166 +0,0 @@
|
|
| 1 |
-
"""V14: Auto-continue. Use ASCII-only text for reliable build."""
|
| 2 |
-
|
| 3 |
-
import os, sys
|
| 4 |
-
|
| 5 |
-
ROOT = "/source/frontend/src"
|
| 6 |
-
|
| 7 |
-
FILES = {
|
| 8 |
-
"events.ts": ROOT + "/types/events.ts",
|
| 9 |
-
"sse.ts": ROOT + "/lib/sse-chat-transport.ts",
|
| 10 |
-
"hook.ts": ROOT + "/hooks/useAgentChat.ts",
|
| 11 |
-
"input.tsx": ROOT + "/components/Chat/ChatInput.tsx",
|
| 12 |
-
"sess.tsx": ROOT + "/components/SessionChat.tsx",
|
| 13 |
-
"agent.py": "/app/agent/core/agent_loop.py",
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
# 1. events.ts
|
| 17 |
-
c = open(FILES["events.ts"]).read()
|
| 18 |
-
if "'task_incomplete'" not in c:
|
| 19 |
-
c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
|
| 20 |
-
open(FILES["events.ts"], 'w').write(c)
|
| 21 |
-
print("OK: events.ts")
|
| 22 |
-
|
| 23 |
-
# 2. sse.ts
|
| 24 |
-
c = open(FILES["sse.ts"]).read()
|
| 25 |
-
if "case 'task_incomplete'" not in c:
|
| 26 |
-
block = """ case 'task_incomplete':
|
| 27 |
-
(sideChannel as any).onTaskIncomplete(
|
| 28 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 29 |
-
);
|
| 30 |
-
break;
|
| 31 |
-
|
| 32 |
-
default:"""
|
| 33 |
-
c = c.replace("\n default:", "\n" + block)
|
| 34 |
-
open(FILES["sse.ts"], 'w').write(c)
|
| 35 |
-
print("OK: sse.ts")
|
| 36 |
-
|
| 37 |
-
# 3. hook.ts - useAgentChat.ts
|
| 38 |
-
c = open(FILES["hook.ts"]).read()
|
| 39 |
-
|
| 40 |
-
# useState import
|
| 41 |
-
if "useState" not in c:
|
| 42 |
-
c = c.replace(
|
| 43 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 44 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
# State after callbacksRef
|
| 48 |
-
if "_showAc" not in c:
|
| 49 |
-
state_block = (
|
| 50 |
-
" // Auto-continue state\n"
|
| 51 |
-
" const [_showAc, _setShowAc] = useState(false);\n"
|
| 52 |
-
" const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n"
|
| 53 |
-
)
|
| 54 |
-
c = c.replace(
|
| 55 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 56 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };\n" + state_block
|
| 57 |
-
)
|
| 58 |
-
|
| 59 |
-
# Handler in onSessionUpdate
|
| 60 |
-
if "ac_plan" not in c:
|
| 61 |
-
handler = (
|
| 62 |
-
" // Auto-continue check\n"
|
| 63 |
-
" if ((data as any).ac_plan) {\n"
|
| 64 |
-
" _acRef.current = (data as any).ac_plan;\n"
|
| 65 |
-
" _setShowAc(true);\n"
|
| 66 |
-
" return;\n"
|
| 67 |
-
" }\n"
|
| 68 |
-
)
|
| 69 |
-
c = c.replace("onSessionUpdate: (data) => {", "onSessionUpdate: (data) => {\n" + handler)
|
| 70 |
-
|
| 71 |
-
# Cancel + effect before return
|
| 72 |
-
if "_acCancel" not in c:
|
| 73 |
-
cancel = " const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n"
|
| 74 |
-
effect = (
|
| 75 |
-
" useEffect(() => {\n"
|
| 76 |
-
" if (!_showAc) return;\n"
|
| 77 |
-
" const plan = _acRef.current;\n"
|
| 78 |
-
" const timer = setTimeout(() => {\n"
|
| 79 |
-
" const s = chatActionsRef.current.setMessages;\n"
|
| 80 |
-
" const m = chatActionsRef.current.messages;\n"
|
| 81 |
-
" if (s && plan.length > 0) {\n"
|
| 82 |
-
" const t = plan.map((i: { content: string }) => '- ' + i.content).join('\\\\n');\n"
|
| 83 |
-
" const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: '[TIEP TUC] Task chua hoan thanh:\\\\n' + t }], content: '' };\n"
|
| 84 |
-
" s([...m, nu]);\n"
|
| 85 |
-
" }\n"
|
| 86 |
-
" _setShowAc(false);\n"
|
| 87 |
-
" }, 10000);\n"
|
| 88 |
-
" return () => clearTimeout(timer);\n"
|
| 89 |
-
" }, [_showAc]);\n\n"
|
| 90 |
-
)
|
| 91 |
-
c = c.replace(
|
| 92 |
-
" return {\n messages: chat.messages,",
|
| 93 |
-
cancel + effect + " return {\n messages: chat.messages,"
|
| 94 |
-
)
|
| 95 |
-
|
| 96 |
-
# Return values
|
| 97 |
-
if "_showAc," not in c:
|
| 98 |
-
c = c.replace("refreshMessages,\n };", "refreshMessages,\n _showAc,\n _acCancel,\n };")
|
| 99 |
-
|
| 100 |
-
open(FILES["hook.ts"], 'w').write(c)
|
| 101 |
-
print("OK: useAgentChat.ts")
|
| 102 |
-
|
| 103 |
-
# 4. input.tsx - ChatInput.tsx
|
| 104 |
-
c = open(FILES["input.tsx"]).read()
|
| 105 |
-
|
| 106 |
-
if "Button" not in c:
|
| 107 |
-
# Match the exact MUI import block
|
| 108 |
-
c = c.replace("import {\n Alert,\n Box,\n TextField,\n IconButton,\n CircularProgress,\n Typography,\n Menu,\n MenuItem,\n ListItemIcon,\n ListItemText,\n Chip,\n LinearProgress,\n Snackbar,\n Tooltip,\n} from '@mui/material';",
|
| 109 |
-
"import {\n Alert,\n Box,\n TextField,\n IconButton,\n CircularProgress,\n Typography,\n Menu,\n MenuItem,\n ListItemIcon,\n ListItemText,\n Chip,\n LinearProgress,\n Snackbar,\n Tooltip,\n Button,\n} from '@mui/material';")
|
| 110 |
-
|
| 111 |
-
if "_showAc" not in c:
|
| 112 |
-
c = c.replace("placeholder?: string;", "placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;")
|
| 113 |
-
c = c.replace("placeholder = 'Ask anything...' }: ChatInputProps) {",
|
| 114 |
-
"placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {")
|
| 115 |
-
|
| 116 |
-
if "TAM_DUNG" not in c:
|
| 117 |
-
btn = (
|
| 118 |
-
" {_showAc && _acCancel && (\n"
|
| 119 |
-
" <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>\n"
|
| 120 |
-
" <Button variant=\"outlined\" size=\"small\" onClick={_acCancel}\n"
|
| 121 |
-
" sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>\n"
|
| 122 |
-
" TAM_DUNG_TU_DONG_TIEP_TUC\n"
|
| 123 |
-
" </Button>\n"
|
| 124 |
-
" </Box>\n"
|
| 125 |
-
" )}\n"
|
| 126 |
-
)
|
| 127 |
-
c = c.replace("<JobsUpgradeDialog", btn + " <JobsUpgradeDialog")
|
| 128 |
-
|
| 129 |
-
open(FILES["input.tsx"], 'w').write(c)
|
| 130 |
-
print("OK: ChatInput.tsx")
|
| 131 |
-
|
| 132 |
-
# 5. sess.tsx - SessionChat.tsx
|
| 133 |
-
if os.path.exists(FILES["sess.tsx"]):
|
| 134 |
-
c = open(FILES["sess.tsx"]).read()
|
| 135 |
-
if "_showAc" not in c:
|
| 136 |
-
c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
|
| 137 |
-
open(FILES["sess.tsx"], 'w').write(c)
|
| 138 |
-
print("OK: SessionChat.tsx")
|
| 139 |
-
|
| 140 |
-
# 6. agent.py
|
| 141 |
-
if os.path.exists(FILES["agent.py"]):
|
| 142 |
-
c = open(FILES["agent.py"]).read()
|
| 143 |
-
|
| 144 |
-
if "_unfinished_plan" not in c:
|
| 145 |
-
fn = "\n\ndef _unfinished_plan(s):\n p = getattr(s, 'current_plan', None) or []\n return [it for it in p if it.get('status') in ('pending', 'in_progress')]\n"
|
| 146 |
-
c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
|
| 147 |
-
|
| 148 |
-
if "ac_plan" not in c:
|
| 149 |
-
check = (
|
| 150 |
-
"\n if not llm_result.tool_calls_acc and llm_result.content:\n"
|
| 151 |
-
" unfinished = _unfinished_plan(session)\n"
|
| 152 |
-
" if unfinished:\n"
|
| 153 |
-
" await session.send_event(Event(event_type='session_update', data={\n"
|
| 154 |
-
" 'ac_plan': unfinished,\n"
|
| 155 |
-
" }))\n"
|
| 156 |
-
)
|
| 157 |
-
marker = " # -- End of turn --"
|
| 158 |
-
if marker in c:
|
| 159 |
-
c = c.replace(marker, check + marker)
|
| 160 |
-
|
| 161 |
-
open(FILES["agent.py"], 'w').write(c)
|
| 162 |
-
print("OK: agent_loop.py")
|
| 163 |
-
else:
|
| 164 |
-
print("SKIP: agent_loop.py")
|
| 165 |
-
|
| 166 |
-
print("\nDONE: V14")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v15.py
DELETED
|
@@ -1,167 +0,0 @@
|
|
| 1 |
-
"""V15: Auto-continue - fix Button import detection + return matching."""
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
|
| 5 |
-
ROOT = "/source/frontend/src"
|
| 6 |
-
EVENTS = ROOT + "/types/events.ts"
|
| 7 |
-
SSE = ROOT + "/lib/sse-chat-transport.ts"
|
| 8 |
-
HOOK = ROOT + "/hooks/useAgentChat.ts"
|
| 9 |
-
INPUT = ROOT + "/components/Chat/ChatInput.tsx"
|
| 10 |
-
SESS = ROOT + "/components/SessionChat.tsx"
|
| 11 |
-
AGENT = "/app/agent/core/agent_loop.py"
|
| 12 |
-
|
| 13 |
-
def ok(f): print(f"OK: {f}")
|
| 14 |
-
|
| 15 |
-
# 1. events.ts
|
| 16 |
-
with open(EVENTS) as f: c = f.read()
|
| 17 |
-
if "'task_incomplete'" not in c:
|
| 18 |
-
c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
|
| 19 |
-
with open(EVENTS, 'w') as f: f.write(c)
|
| 20 |
-
ok("events.ts")
|
| 21 |
-
|
| 22 |
-
# 2. sse.ts
|
| 23 |
-
with open(SSE) as f: c = f.read()
|
| 24 |
-
if "case 'task_incomplete'" not in c:
|
| 25 |
-
block = """ case 'task_incomplete':
|
| 26 |
-
(sideChannel as any).onTaskIncomplete(
|
| 27 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 28 |
-
);
|
| 29 |
-
break;
|
| 30 |
-
|
| 31 |
-
default:"""
|
| 32 |
-
c = c.replace("\n default:", "\n" + block)
|
| 33 |
-
with open(SSE, 'w') as f: f.write(c)
|
| 34 |
-
ok("sse.ts")
|
| 35 |
-
|
| 36 |
-
# 3. useAgentChat.ts
|
| 37 |
-
with open(HOOK) as f:
|
| 38 |
-
lines = f.readlines()
|
| 39 |
-
|
| 40 |
-
new_lines = []
|
| 41 |
-
state_ok = hdl_ok = efx_ok = ret_ok = False
|
| 42 |
-
|
| 43 |
-
for line in lines:
|
| 44 |
-
# Import useState
|
| 45 |
-
if "import { useCallback, useEffect, useMemo, useRef } from 'react';" in line:
|
| 46 |
-
line = line.replace("useRef }", "useRef, useState }")
|
| 47 |
-
|
| 48 |
-
# State after callbacksRef
|
| 49 |
-
if "callbacksRef.current = { onReady, onError, onSessionDead };" in line and not state_ok:
|
| 50 |
-
new_lines.append(line)
|
| 51 |
-
new_lines.append(" const [_showAc, _setShowAc] = useState(false);\n")
|
| 52 |
-
new_lines.append(" const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n")
|
| 53 |
-
state_ok = True
|
| 54 |
-
continue
|
| 55 |
-
|
| 56 |
-
# Handler
|
| 57 |
-
if "onSessionUpdate: (data) => {" in line and not hdl_ok:
|
| 58 |
-
new_lines.append(line)
|
| 59 |
-
new_lines.append(" if ((data as any).ac_plan) {\n")
|
| 60 |
-
new_lines.append(" _acRef.current = (data as any).ac_plan;\n")
|
| 61 |
-
new_lines.append(" _setShowAc(true);\n")
|
| 62 |
-
new_lines.append(" return;\n")
|
| 63 |
-
new_lines.append(" }\n")
|
| 64 |
-
hdl_ok = True
|
| 65 |
-
continue
|
| 66 |
-
|
| 67 |
-
# Cancel + effect BEFORE 'return {'
|
| 68 |
-
stripped = line.strip()
|
| 69 |
-
if stripped == "return {" and not efx_ok:
|
| 70 |
-
new_lines.append(" const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n")
|
| 71 |
-
new_lines.append(" useEffect(() => {\n")
|
| 72 |
-
new_lines.append(" if (!_showAc) return;\n")
|
| 73 |
-
new_lines.append(" const plan = _acRef.current;\n")
|
| 74 |
-
new_lines.append(" const timer = setTimeout(() => {\n")
|
| 75 |
-
new_lines.append(" const s = chatActionsRef.current.setMessages;\n")
|
| 76 |
-
new_lines.append(" const m = chatActionsRef.current.messages;\n")
|
| 77 |
-
new_lines.append(" if (s && plan.length > 0) {\n")
|
| 78 |
-
new_lines.append(" const t = plan.map((i: { content: string }) => '- ' + i.content).join('\\\\n');\n")
|
| 79 |
-
new_lines.append(" const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: '[TIEP TUC] Task chua hoan thanh:\\\\n' + t }], content: '' };\n")
|
| 80 |
-
new_lines.append(" s([...m, nu]);\n")
|
| 81 |
-
new_lines.append(" }\n")
|
| 82 |
-
new_lines.append(" _setShowAc(false);\n")
|
| 83 |
-
new_lines.append(" }, 10000);\n")
|
| 84 |
-
new_lines.append(" return () => clearTimeout(timer);\n")
|
| 85 |
-
new_lines.append(" }, [_showAc]);\n\n")
|
| 86 |
-
efx_ok = True
|
| 87 |
-
|
| 88 |
-
# Add return values after "refreshMessages,"
|
| 89 |
-
if "refreshMessages," in line and not ret_ok:
|
| 90 |
-
new_lines.append(line)
|
| 91 |
-
new_lines.append(" _showAc,\n")
|
| 92 |
-
new_lines.append(" _acCancel,\n")
|
| 93 |
-
ret_ok = True
|
| 94 |
-
continue
|
| 95 |
-
|
| 96 |
-
new_lines.append(line)
|
| 97 |
-
|
| 98 |
-
with open(HOOK, 'w') as f:
|
| 99 |
-
f.writelines(new_lines)
|
| 100 |
-
ok(f"useAgentChat.ts (s={state_ok} h={hdl_ok} e={efx_ok} r={ret_ok})")
|
| 101 |
-
|
| 102 |
-
# 4. ChatInput.tsx
|
| 103 |
-
with open(INPUT) as f:
|
| 104 |
-
content = f.read()
|
| 105 |
-
|
| 106 |
-
# Check for exact Button in MUI import (not "IconButton" which contains "Button")
|
| 107 |
-
if " Button,\n" not in content and " Button\n" not in content:
|
| 108 |
-
content = content.replace(
|
| 109 |
-
" Tooltip,",
|
| 110 |
-
" Tooltip,\n Button,"
|
| 111 |
-
)
|
| 112 |
-
|
| 113 |
-
# Add props to interface
|
| 114 |
-
if "_showAc" not in content:
|
| 115 |
-
content = content.replace("placeholder?: string;", "placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;")
|
| 116 |
-
content = content.replace("placeholder = 'Ask anything...' }: ChatInputProps) {",
|
| 117 |
-
"placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {")
|
| 118 |
-
|
| 119 |
-
# Add pause button before <JobsUpgradeDialog
|
| 120 |
-
btn = (
|
| 121 |
-
" {_showAc && _acCancel && (\n"
|
| 122 |
-
" <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>\n"
|
| 123 |
-
" <Button variant=\"outlined\" size=\"small\" onClick={_acCancel}\n"
|
| 124 |
-
" sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>\n"
|
| 125 |
-
" TAM_DUNG_TU_DONG_TIEP_TUC (10s)\n"
|
| 126 |
-
" </Button>\n"
|
| 127 |
-
" </Box>\n"
|
| 128 |
-
" )}\n"
|
| 129 |
-
)
|
| 130 |
-
content = content.replace("<JobsUpgradeDialog", btn + " <JobsUpgradeDialog")
|
| 131 |
-
|
| 132 |
-
with open(INPUT, 'w') as f:
|
| 133 |
-
f.write(content)
|
| 134 |
-
ok("ChatInput.tsx")
|
| 135 |
-
|
| 136 |
-
# 5. SessionChat.tsx
|
| 137 |
-
if os.path.exists(SESS):
|
| 138 |
-
with open(SESS) as f: c = f.read()
|
| 139 |
-
if "_showAc" not in c:
|
| 140 |
-
c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
|
| 141 |
-
with open(SESS, 'w') as f: f.write(c)
|
| 142 |
-
ok("SessionChat.tsx")
|
| 143 |
-
|
| 144 |
-
# 6. agent_loop.py
|
| 145 |
-
if os.path.exists(AGENT):
|
| 146 |
-
with open(AGENT) as f: c = f.read()
|
| 147 |
-
if "_unfinished_plan" not in c:
|
| 148 |
-
fn = "\n\ndef _unfinished_plan(s):\n p = getattr(s, 'current_plan', None) or []\n return [it for it in p if it.get('status') in ('pending', 'in_progress')]\n"
|
| 149 |
-
c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
|
| 150 |
-
if "ac_plan" not in c:
|
| 151 |
-
check = (
|
| 152 |
-
"\n if not llm_result.tool_calls_acc and llm_result.content:\n"
|
| 153 |
-
" unfinished = _unfinished_plan(session)\n"
|
| 154 |
-
" if unfinished:\n"
|
| 155 |
-
" await session.send_event(Event(event_type='session_update', data={\n"
|
| 156 |
-
" 'ac_plan': unfinished,\n"
|
| 157 |
-
" }))\n"
|
| 158 |
-
)
|
| 159 |
-
marker = " # -- End of turn --"
|
| 160 |
-
if marker in c:
|
| 161 |
-
c = c.replace(marker, check + marker)
|
| 162 |
-
with open(AGENT, 'w') as f: f.write(c)
|
| 163 |
-
ok("agent_loop.py")
|
| 164 |
-
else:
|
| 165 |
-
print("SKIP: agent_loop.py")
|
| 166 |
-
|
| 167 |
-
print("\nDONE: V15")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v16.py
DELETED
|
@@ -1,142 +0,0 @@
|
|
| 1 |
-
"""V16: Auto-continue - match exact 'return {\n messages: chat.messages,' (only one in file)"""
|
| 2 |
-
|
| 3 |
-
import os, sys
|
| 4 |
-
|
| 5 |
-
F = lambda n: "/source/frontend/src/" + n
|
| 6 |
-
EVENTS = F("types/events.ts")
|
| 7 |
-
SSE = F("lib/sse-chat-transport.ts")
|
| 8 |
-
HOOK = F("hooks/useAgentChat.ts")
|
| 9 |
-
INPUT = F("components/Chat/ChatInput.tsx")
|
| 10 |
-
SESS = F("components/SessionChat.tsx")
|
| 11 |
-
AGENT = "/app/agent/core/agent_loop.py"
|
| 12 |
-
|
| 13 |
-
def ok(f): print(f"OK: {f}")
|
| 14 |
-
|
| 15 |
-
# --- events.ts ---
|
| 16 |
-
with open(EVENTS) as f: c = f.read()
|
| 17 |
-
if "'task_incomplete'" not in c:
|
| 18 |
-
c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
|
| 19 |
-
open(EVENTS, 'w').write(c)
|
| 20 |
-
ok("events.ts")
|
| 21 |
-
|
| 22 |
-
# --- sse.ts ---
|
| 23 |
-
with open(SSE) as f: c = f.read()
|
| 24 |
-
if "case 'task_incomplete'" not in c:
|
| 25 |
-
c = c.replace("\n default:", "\n case 'task_incomplete':\n (sideChannel as any).onTaskIncomplete(\n (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],\n );\n break;\n\n default:")
|
| 26 |
-
open(SSE, 'w').write(c)
|
| 27 |
-
ok("sse.ts")
|
| 28 |
-
|
| 29 |
-
# --- useAgentChat.ts ---
|
| 30 |
-
with open(HOOK) as f: content = f.read()
|
| 31 |
-
|
| 32 |
-
# useState import
|
| 33 |
-
if "useState" not in content:
|
| 34 |
-
content = content.replace("useRef }", "useRef, useState }")
|
| 35 |
-
|
| 36 |
-
# State after callbacksRef (insert once)
|
| 37 |
-
if "_showAc" not in content:
|
| 38 |
-
s = (" // Auto-continue state\n"
|
| 39 |
-
" const [_showAc, _setShowAc] = useState(false);\n"
|
| 40 |
-
" const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n")
|
| 41 |
-
content = content.replace(
|
| 42 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 43 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };\n" + s
|
| 44 |
-
)
|
| 45 |
-
|
| 46 |
-
# Handler in onSessionUpdate
|
| 47 |
-
if "ac_plan" not in content:
|
| 48 |
-
h = (" // Auto-continue check\n"
|
| 49 |
-
" if ((data as any).ac_plan) {\n"
|
| 50 |
-
" _acRef.current = (data as any).ac_plan;\n"
|
| 51 |
-
" _setShowAc(true);\n"
|
| 52 |
-
" return;\n"
|
| 53 |
-
" }\n")
|
| 54 |
-
content = content.replace("onSessionUpdate: (data) => {", "onSessionUpdate: (data) => {\n" + h)
|
| 55 |
-
|
| 56 |
-
# CRITICAL: Only match the exact two-line return that has messages: chat.messages
|
| 57 |
-
# Only ONE place in the file has this exact pattern
|
| 58 |
-
marker = " return {\n messages: chat.messages,"
|
| 59 |
-
if marker in content and "_acCancel" not in content:
|
| 60 |
-
cancel_effect = (
|
| 61 |
-
" const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n"
|
| 62 |
-
" useEffect(() => {\n"
|
| 63 |
-
" if (!_showAc) return;\n"
|
| 64 |
-
" const plan = _acRef.current;\n"
|
| 65 |
-
" const timer = setTimeout(() => {\n"
|
| 66 |
-
" const s = chatActionsRef.current.setMessages;\n"
|
| 67 |
-
" const m = chatActionsRef.current.messages;\n"
|
| 68 |
-
" if (s && plan.length > 0) {\n"
|
| 69 |
-
" const t = plan.map((i: { content: string }) => '- ' + i.content).join('\\\\n');\n"
|
| 70 |
-
" const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: '[TIEP TUC] Task chua hoan thanh:\\\\n' + t }], content: '' };\n"
|
| 71 |
-
" s([...m, nu]);\n"
|
| 72 |
-
" }\n"
|
| 73 |
-
" _setShowAc(false);\n"
|
| 74 |
-
" }, 10000);\n"
|
| 75 |
-
" return () => clearTimeout(timer);\n"
|
| 76 |
-
" }, [_showAc]);\n\n"
|
| 77 |
-
)
|
| 78 |
-
content = content.replace(marker, cancel_effect + marker)
|
| 79 |
-
|
| 80 |
-
# Add to return values
|
| 81 |
-
content = content.replace(
|
| 82 |
-
"refreshMessages,\n };",
|
| 83 |
-
"refreshMessages,\n _showAc,\n _acCancel,\n };"
|
| 84 |
-
)
|
| 85 |
-
|
| 86 |
-
with open(HOOK, 'w').write(content)
|
| 87 |
-
ok("useAgentChat.ts")
|
| 88 |
-
|
| 89 |
-
# --- ChatInput.tsx ---
|
| 90 |
-
with open(INPUT) as f: content = f.read()
|
| 91 |
-
|
| 92 |
-
# Button: check for standalone ", Button" or " Button" NOT "IconButton"
|
| 93 |
-
if ", Button,\n" not in content and ", Button\n" not in content:
|
| 94 |
-
content = content.replace(
|
| 95 |
-
" Tooltip,",
|
| 96 |
-
" Tooltip,\n Button,"
|
| 97 |
-
)
|
| 98 |
-
|
| 99 |
-
if "_showAc" not in content:
|
| 100 |
-
content = content.replace("placeholder?: string;", "placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;")
|
| 101 |
-
content = content.replace("placeholder = 'Ask anything...' }: ChatInputProps) {",
|
| 102 |
-
"placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {")
|
| 103 |
-
btn = (" {_showAc && _acCancel && (\n"
|
| 104 |
-
" <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>\n"
|
| 105 |
-
" <Button variant=\"outlined\" size=\"small\" onClick={_acCancel}\n"
|
| 106 |
-
" sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>\n"
|
| 107 |
-
" TAM_DUNG_TU_DONG_TIEP_TUC (10s)\n"
|
| 108 |
-
" </Button>\n"
|
| 109 |
-
" </Box>\n"
|
| 110 |
-
" )}\n")
|
| 111 |
-
content = content.replace("<JobsUpgradeDialog", btn + " <JobsUpgradeDialog")
|
| 112 |
-
|
| 113 |
-
open(INPUT, 'w').write((content).read() if hasattr(content, 'read') else content)
|
| 114 |
-
ok("ChatInput.tsx")
|
| 115 |
-
|
| 116 |
-
# --- SessionChat.tsx ---
|
| 117 |
-
if os.path.exists(SESS):
|
| 118 |
-
with open(SESS) as f: c = f.read()
|
| 119 |
-
if "_showAc" not in c:
|
| 120 |
-
c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
|
| 121 |
-
open(SESS, 'w').write(c)
|
| 122 |
-
ok("SessionChat.tsx")
|
| 123 |
-
|
| 124 |
-
# --- agent_loop.py ---
|
| 125 |
-
if os.path.exists(AGENT):
|
| 126 |
-
with open(AGENT) as f: c = f.read()
|
| 127 |
-
if "_unfinished_plan" not in c:
|
| 128 |
-
c = c.replace("class Handlers:", "\n\ndef _unfinished_plan(s):\n p = getattr(s, 'current_plan', None) or []\n return [it for it in p if it.get('status') in ('pending', 'in_progress')]\n\n\nclass Handlers:")
|
| 129 |
-
if "ac_plan" not in c:
|
| 130 |
-
check = ("\n if not llm_result.tool_calls_acc and llm_result.content:\n"
|
| 131 |
-
" unfinished = _unfinished_plan(session)\n"
|
| 132 |
-
" if unfinished:\n"
|
| 133 |
-
" await session.send_event(Event(event_type='session_update', data={\n"
|
| 134 |
-
" 'ac_plan': unfinished,\n"
|
| 135 |
-
" }))\n")
|
| 136 |
-
c = c.replace(" # -- End of turn --", check + " # -- End of turn --")
|
| 137 |
-
open(AGENT, 'w').write(c)
|
| 138 |
-
ok("agent_loop.py")
|
| 139 |
-
else:
|
| 140 |
-
print("SKIP: agent_loop.py")
|
| 141 |
-
|
| 142 |
-
print("\nDONE: V16")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v17.py
DELETED
|
@@ -1,187 +0,0 @@
|
|
| 1 |
-
"""V17: Auto-continue - perfect Python, exact marker matching"""
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
|
| 5 |
-
EVENTS = "/source/frontend/src/types/events.ts"
|
| 6 |
-
SSE = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 7 |
-
HOOK = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 8 |
-
INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 9 |
-
SESS = "/source/frontend/src/components/SessionChat.tsx"
|
| 10 |
-
AGENT = "/app/agent/core/agent_loop.py"
|
| 11 |
-
|
| 12 |
-
print("Patching auto-continue V17...")
|
| 13 |
-
|
| 14 |
-
# --- events.ts ---
|
| 15 |
-
with open(EVENTS) as f:
|
| 16 |
-
c = f.read()
|
| 17 |
-
if "'task_incomplete'" not in c:
|
| 18 |
-
c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
|
| 19 |
-
with open(EVENTS, 'w') as f:
|
| 20 |
-
f.write(c)
|
| 21 |
-
print("OK: events.ts")
|
| 22 |
-
|
| 23 |
-
# --- sse.ts ---
|
| 24 |
-
with open(SSE) as f:
|
| 25 |
-
c = f.read()
|
| 26 |
-
if "case 'task_incomplete'" not in c:
|
| 27 |
-
block = (
|
| 28 |
-
" case 'task_incomplete':\n"
|
| 29 |
-
" (sideChannel as any).onTaskIncomplete(\n"
|
| 30 |
-
" (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],\n"
|
| 31 |
-
" );\n"
|
| 32 |
-
" break;\n\n"
|
| 33 |
-
" default:"
|
| 34 |
-
)
|
| 35 |
-
c = c.replace("\n default:", "\n" + block)
|
| 36 |
-
with open(SSE, 'w') as f:
|
| 37 |
-
f.write(c)
|
| 38 |
-
print("OK: sse.ts")
|
| 39 |
-
|
| 40 |
-
# --- useAgentChat.ts ---
|
| 41 |
-
with open(HOOK) as f:
|
| 42 |
-
content = f.read()
|
| 43 |
-
|
| 44 |
-
# Add useState import
|
| 45 |
-
if "useState" not in content:
|
| 46 |
-
content = content.replace(
|
| 47 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 48 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
# Add state after callbacksRef
|
| 52 |
-
if "_showAc" not in content:
|
| 53 |
-
state_code = (
|
| 54 |
-
" // Auto-continue state\n"
|
| 55 |
-
" const [_showAc, _setShowAc] = useState(false);\n"
|
| 56 |
-
" const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n"
|
| 57 |
-
)
|
| 58 |
-
content = content.replace(
|
| 59 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 60 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };\n" + state_code
|
| 61 |
-
)
|
| 62 |
-
|
| 63 |
-
# Add handler in onSessionUpdate
|
| 64 |
-
if "ac_plan" not in content:
|
| 65 |
-
h = (
|
| 66 |
-
" // Auto-continue check\n"
|
| 67 |
-
" if ((data as any).ac_plan) {\n"
|
| 68 |
-
" _acRef.current = (data as any).ac_plan;\n"
|
| 69 |
-
" _setShowAc(true);\n"
|
| 70 |
-
" return;\n"
|
| 71 |
-
" }\n"
|
| 72 |
-
)
|
| 73 |
-
content = content.replace("onSessionUpdate: (data) => {", "onSessionUpdate: (data) => {\n" + h)
|
| 74 |
-
|
| 75 |
-
# ONLY match the unique two-line pattern: " return {\n messages: chat.messages,"
|
| 76 |
-
UNIQUE_MARKER = " return {\n messages: chat.messages,"
|
| 77 |
-
if UNIQUE_MARKER in content and "_acCancel" not in content:
|
| 78 |
-
insert = (
|
| 79 |
-
" const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n"
|
| 80 |
-
" useEffect(() => {\n"
|
| 81 |
-
" if (!_showAc) return;\n"
|
| 82 |
-
" const plan = _acRef.current;\n"
|
| 83 |
-
" const timer = setTimeout(() => {\n"
|
| 84 |
-
" const s = chatActionsRef.current.setMessages;\n"
|
| 85 |
-
" const m = chatActionsRef.current.messages;\n"
|
| 86 |
-
" if (s && plan.length > 0) {\n"
|
| 87 |
-
" const t = plan.map((i: { content: string }) => '- ' + i.content).join('\\\\n');\n"
|
| 88 |
-
" const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: '[TIEP TUC] Task chua hoan thanh:\\\\n' + t }], content: '' };\n"
|
| 89 |
-
" s([...m, nu]);\n"
|
| 90 |
-
" }\n"
|
| 91 |
-
" _setShowAc(false);\n"
|
| 92 |
-
" }, 10000);\n"
|
| 93 |
-
" return () => clearTimeout(timer);\n"
|
| 94 |
-
" }, [_showAc]);\n\n"
|
| 95 |
-
)
|
| 96 |
-
content = content.replace(UNIQUE_MARKER, insert + UNIQUE_MARKER)
|
| 97 |
-
|
| 98 |
-
# Add to return
|
| 99 |
-
content = content.replace(
|
| 100 |
-
"refreshMessages,\n };",
|
| 101 |
-
"refreshMessages,\n _showAc,\n _acCancel,\n };"
|
| 102 |
-
)
|
| 103 |
-
|
| 104 |
-
with open(HOOK, 'w') as f:
|
| 105 |
-
f.write(content)
|
| 106 |
-
print("OK: useAgentChat.ts")
|
| 107 |
-
|
| 108 |
-
# --- ChatInput.tsx ---
|
| 109 |
-
with open(INPUT) as f:
|
| 110 |
-
content = f.read()
|
| 111 |
-
|
| 112 |
-
# Add Button (check exact substring, not "IconButton")
|
| 113 |
-
if " Button,\n" not in content and " Button\n" not in content:
|
| 114 |
-
content = content.replace(
|
| 115 |
-
" Tooltip,",
|
| 116 |
-
" Tooltip,\n Button,"
|
| 117 |
-
)
|
| 118 |
-
|
| 119 |
-
if "_showAc" not in content:
|
| 120 |
-
content = content.replace(
|
| 121 |
-
"placeholder?: string;",
|
| 122 |
-
"placeholder?: string;\n _showAc?: boolean;\n _acCancel?: () => void;"
|
| 123 |
-
)
|
| 124 |
-
content = content.replace(
|
| 125 |
-
"placeholder = 'Ask anything...' }: ChatInputProps) {",
|
| 126 |
-
"placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
|
| 127 |
-
)
|
| 128 |
-
btn = (
|
| 129 |
-
" {_showAc && _acCancel && (\n"
|
| 130 |
-
" <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>\n"
|
| 131 |
-
" <Button variant=\"outlined\" size=\"small\" onClick={_acCancel}\n"
|
| 132 |
-
" sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>\n"
|
| 133 |
-
" TAM_DUNG_TU_DONG_TIEP_TUC (10s)\n"
|
| 134 |
-
" </Button>\n"
|
| 135 |
-
" </Box>\n"
|
| 136 |
-
" )}\n"
|
| 137 |
-
)
|
| 138 |
-
content = content.replace("<JobsUpgradeDialog", btn + " <JobsUpgradeDialog")
|
| 139 |
-
|
| 140 |
-
with open(INPUT, 'w') as f:
|
| 141 |
-
f.write(content)
|
| 142 |
-
print("OK: ChatInput.tsx")
|
| 143 |
-
|
| 144 |
-
# --- SessionChat.tsx ---
|
| 145 |
-
if os.path.exists(SESS):
|
| 146 |
-
with open(SESS) as f:
|
| 147 |
-
c = f.read()
|
| 148 |
-
if "_showAc" not in c:
|
| 149 |
-
c = c.replace(
|
| 150 |
-
"<ChatInput ",
|
| 151 |
-
"<ChatInput _showAc={_showAc} _acCancel={_acCancel} "
|
| 152 |
-
)
|
| 153 |
-
with open(SESS, 'w') as f:
|
| 154 |
-
f.write(c)
|
| 155 |
-
print("OK: SessionChat.tsx")
|
| 156 |
-
|
| 157 |
-
# --- agent_loop.py ---
|
| 158 |
-
if os.path.exists(AGENT):
|
| 159 |
-
with open(AGENT) as f:
|
| 160 |
-
c = f.read()
|
| 161 |
-
|
| 162 |
-
if "_unfinished_plan" not in c:
|
| 163 |
-
fn = (
|
| 164 |
-
"\n\ndef _unfinished_plan(s):\n"
|
| 165 |
-
" p = getattr(s, 'current_plan', None) or []\n"
|
| 166 |
-
" return [it for it in p if it.get('status') in ('pending', 'in_progress')]\n\n"
|
| 167 |
-
)
|
| 168 |
-
c = c.replace("class Handlers:", fn + "class Handlers:")
|
| 169 |
-
|
| 170 |
-
if "ac_plan" not in c:
|
| 171 |
-
check = (
|
| 172 |
-
"\n if not llm_result.tool_calls_acc and llm_result.content:\n"
|
| 173 |
-
" unfinished = _unfinished_plan(session)\n"
|
| 174 |
-
" if unfinished:\n"
|
| 175 |
-
" await session.send_event(Event(event_type='session_update', data={\n"
|
| 176 |
-
" 'ac_plan': unfinished,\n"
|
| 177 |
-
" }))\n"
|
| 178 |
-
)
|
| 179 |
-
c = c.replace(" # -- End of turn --", check + " # -- End of turn --")
|
| 180 |
-
|
| 181 |
-
with open(AGENT, 'w') as f:
|
| 182 |
-
f.write(c)
|
| 183 |
-
print("OK: agent_loop.py")
|
| 184 |
-
else:
|
| 185 |
-
print("SKIP: agent_loop.py")
|
| 186 |
-
|
| 187 |
-
print("DONE: V17")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v2.py
DELETED
|
@@ -1,307 +0,0 @@
|
|
| 1 |
-
"""V2: Fix all TypeScript errors for auto-continue feature.
|
| 2 |
-
|
| 3 |
-
Errors fixed:
|
| 4 |
-
1. 'startAutoContinue' used before declaration → use refs
|
| 5 |
-
2. 'showAutoContinue'/'taskIncompleteInfo' never read → they're in return
|
| 6 |
-
3. 'incompletePlan' implicit any → add type
|
| 7 |
-
4. EventType missing 'task_incomplete' → add to events.ts
|
| 8 |
-
5. DatasetUploadResponse not found → fix import path
|
| 9 |
-
6. onTaskIncomplete not on SideChannelCallbacks → fix in SSE transport
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
import os
|
| 13 |
-
import re
|
| 14 |
-
|
| 15 |
-
# =====================================================================
|
| 16 |
-
# PATCH 1: events.ts - Add 'task_incomplete' to EventType
|
| 17 |
-
# =====================================================================
|
| 18 |
-
EVENTS_FILE = "/source/frontend/src/types/events.ts"
|
| 19 |
-
|
| 20 |
-
def patch_events():
|
| 21 |
-
if not os.path.exists(EVENTS_FILE):
|
| 22 |
-
print(f"SKIP: {EVENTS_FILE} not found")
|
| 23 |
-
return
|
| 24 |
-
with open(EVENTS_FILE, "r") as f:
|
| 25 |
-
content = f.read()
|
| 26 |
-
if "'task_incomplete'" not in content:
|
| 27 |
-
content = content.replace(
|
| 28 |
-
" | 'plan_update';",
|
| 29 |
-
" | 'plan_update'\n | 'task_incomplete';"
|
| 30 |
-
)
|
| 31 |
-
with open(EVENTS_FILE, "w") as f:
|
| 32 |
-
f.write(content)
|
| 33 |
-
print("OK: Added 'task_incomplete' to EventType")
|
| 34 |
-
else:
|
| 35 |
-
print("OK: EventType already has 'task_incomplete'")
|
| 36 |
-
|
| 37 |
-
# =====================================================================
|
| 38 |
-
# PATCH 2: useAgentChat.ts - Fix hoisting + type issues
|
| 39 |
-
# =====================================================================
|
| 40 |
-
USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 41 |
-
|
| 42 |
-
def patch_use_agent_chat():
|
| 43 |
-
if not os.path.exists(USE_AGENT_CHAT):
|
| 44 |
-
print(f"SKIP: {USE_AGENT_CHAT} not found")
|
| 45 |
-
return
|
| 46 |
-
|
| 47 |
-
with open(USE_AGENT_CHAT, "r") as f:
|
| 48 |
-
content = f.read()
|
| 49 |
-
|
| 50 |
-
# 1. Add useState to imports
|
| 51 |
-
if "useState" not in content:
|
| 52 |
-
content = content.replace(
|
| 53 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 54 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 55 |
-
)
|
| 56 |
-
|
| 57 |
-
# 2. Add state + refs after callbacksRef
|
| 58 |
-
state_code = '''
|
| 59 |
-
// Auto-continue state for free models when task incomplete
|
| 60 |
-
const autoContinueTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
| 61 |
-
const [showAutoContinue, setShowAutoContinue] = useState(false);
|
| 62 |
-
const [taskIncompleteInfo, setTaskIncompleteInfo] = useState<{
|
| 63 |
-
incompletePlan: Array<{ id: string; content: string; status: string }>;
|
| 64 |
-
} | null>(null);
|
| 65 |
-
const startAutoContinueRef = useRef<() => void>(() => {});
|
| 66 |
-
const cancelAutoContinueRef = useRef<() => void>(() => {});
|
| 67 |
-
'''
|
| 68 |
-
if "autoContinueTimerRef" not in content:
|
| 69 |
-
content = content.replace(
|
| 70 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 71 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };" + state_code
|
| 72 |
-
)
|
| 73 |
-
|
| 74 |
-
# 3. Add onTaskIncomplete to SideChannelCallbacks interface
|
| 75 |
-
if "onTaskIncomplete:" not in content and "onInterrupted:" in content:
|
| 76 |
-
content = content.replace(
|
| 77 |
-
" onInterrupted: () => void;",
|
| 78 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
# 4. Replace onTaskIncomplete handler to use refs
|
| 82 |
-
old_handler = "onTaskIncomplete: (incompletePlan) => {"
|
| 83 |
-
new_handler = "onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {"
|
| 84 |
-
if old_handler in content:
|
| 85 |
-
content = content.replace(old_handler, new_handler)
|
| 86 |
-
|
| 87 |
-
# Fix the timer callback to use ref instead of direct call
|
| 88 |
-
old_timer = "autoContinueTimerRef.current = setTimeout(() => {\n startAutoContinue();\n }, 10000);"
|
| 89 |
-
new_timer = "autoContinueTimerRef.current = setTimeout(() => {\n startAutoContinueRef.current();\n }, 10000);"
|
| 90 |
-
if old_timer in content:
|
| 91 |
-
content = content.replace(old_timer, new_timer)
|
| 92 |
-
|
| 93 |
-
# 5. Add auto-continue functions BEFORE return (but using refs pattern)
|
| 94 |
-
# Remove old startAutoContinue/cancelAutoContinue if they exist
|
| 95 |
-
old_funcs_start = "const startAutoContinue = useCallback"
|
| 96 |
-
if old_funcs_start in content:
|
| 97 |
-
# Find and remove the old function blocks
|
| 98 |
-
start_idx = content.find("const startAutoContinue = useCallback")
|
| 99 |
-
cancel_idx = content.find("const cancelAutoContinue = useCallback")
|
| 100 |
-
cleanup_idx = content.find("// Cleanup timer on unmount")
|
| 101 |
-
|
| 102 |
-
# Remove from startAutoContinue to end of cleanup
|
| 103 |
-
if start_idx > 0 and cleanup_idx > start_idx:
|
| 104 |
-
# Find the end of cleanup effect
|
| 105 |
-
end_cleanup = content.find("\n", cleanup_idx)
|
| 106 |
-
# Find the closing of the useEffect
|
| 107 |
-
close_idx = content.find("}, []);", end_cleanup)
|
| 108 |
-
if close_idx > 0:
|
| 109 |
-
close_idx += len("}, []);")
|
| 110 |
-
content = content[:start_idx] + content[close_idx:]
|
| 111 |
-
|
| 112 |
-
# 5b. Add new functions using ref pattern, before return
|
| 113 |
-
new_funcs = '''
|
| 114 |
-
// -- Auto-continue for free models when task incomplete -----------------
|
| 115 |
-
const startAutoContinue = useCallback(() => {
|
| 116 |
-
setShowAutoContinue(false);
|
| 117 |
-
setTaskIncompleteInfo(null);
|
| 118 |
-
if (autoContinueTimerRef.current) {
|
| 119 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 120 |
-
autoContinueTimerRef.current = null;
|
| 121 |
-
}
|
| 122 |
-
const incompleteItems = taskIncompleteInfo?.incompletePlan || [];
|
| 123 |
-
const continuationText = incompleteItems.length > 0
|
| 124 |
-
? `Tiếp tục từ các task chưa hoàn thành:\\n${incompleteItems.map(i => `- ${i.content}`).join('\\n')}`
|
| 125 |
-
: 'Tiếp tục nhiệm vụ.';
|
| 126 |
-
chat.sendMessage({
|
| 127 |
-
text: `[TỰ ĐỘNG TIẾP TỤC] ${continuationText}`,
|
| 128 |
-
metadata: { createdAt: new Date().toISOString() },
|
| 129 |
-
});
|
| 130 |
-
}, [taskIncompleteInfo, chat]);
|
| 131 |
-
|
| 132 |
-
const cancelAutoContinue = useCallback(() => {
|
| 133 |
-
setShowAutoContinue(false);
|
| 134 |
-
setTaskIncompleteInfo(null);
|
| 135 |
-
if (autoContinueTimerRef.current) {
|
| 136 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 137 |
-
autoContinueTimerRef.current = null;
|
| 138 |
-
}
|
| 139 |
-
}, []);
|
| 140 |
-
|
| 141 |
-
// Keep refs in sync
|
| 142 |
-
startAutoContinueRef.current = startAutoContinue;
|
| 143 |
-
cancelAutoContinueRef.current = cancelAutoContinue;
|
| 144 |
-
|
| 145 |
-
// Cleanup timer on unmount
|
| 146 |
-
useEffect(() => {
|
| 147 |
-
return () => {
|
| 148 |
-
if (autoContinueTimerRef.current) {
|
| 149 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 150 |
-
}
|
| 151 |
-
};
|
| 152 |
-
}, []);
|
| 153 |
-
'''
|
| 154 |
-
|
| 155 |
-
if "startAutoContinueRef.current" not in content:
|
| 156 |
-
content = content.replace(
|
| 157 |
-
'\n return {\n messages: chat.messages,',
|
| 158 |
-
new_funcs + '\n return {\n messages: chat.messages,'
|
| 159 |
-
)
|
| 160 |
-
|
| 161 |
-
# 6. Update return to include new values
|
| 162 |
-
if "showAutoContinue," not in content:
|
| 163 |
-
content = content.replace(
|
| 164 |
-
"refreshMessages,\n };",
|
| 165 |
-
"refreshMessages,\n showAutoContinue,\n taskIncompleteInfo,\n startAutoContinue,\n cancelAutoContinue,\n };"
|
| 166 |
-
)
|
| 167 |
-
|
| 168 |
-
with open(USE_AGENT_CHAT, "w") as f:
|
| 169 |
-
f.write(content)
|
| 170 |
-
print(f"OK: Patched {USE_AGENT_CHAT}")
|
| 171 |
-
|
| 172 |
-
# =====================================================================
|
| 173 |
-
# PATCH 3: ChatInput.tsx - Fix DatasetUploadResponse import
|
| 174 |
-
# =====================================================================
|
| 175 |
-
CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 176 |
-
|
| 177 |
-
def patch_chat_input():
|
| 178 |
-
if not os.path.exists(CHAT_INPUT):
|
| 179 |
-
print(f"SKIP: {CHAT_INPUT} not found")
|
| 180 |
-
return
|
| 181 |
-
|
| 182 |
-
with open(CHAT_INPUT, "r") as f:
|
| 183 |
-
content = f.read()
|
| 184 |
-
|
| 185 |
-
# Fix DatasetUploadResponse import - try correct path
|
| 186 |
-
if "DatasetUploadResponse" in content:
|
| 187 |
-
# Check if it's imported from agent.ts
|
| 188 |
-
wrong_import = "import type { DatasetUploadResponse } from '@/types/agent';"
|
| 189 |
-
if wrong_import in content:
|
| 190 |
-
# Try to find the correct import - it's probably in the same file or from backend
|
| 191 |
-
# Replace with inline type or remove
|
| 192 |
-
content = content.replace(wrong_import, "")
|
| 193 |
-
# Add inline type
|
| 194 |
-
content = content.replace(
|
| 195 |
-
"import { apiFetch } from '@/utils/api';",
|
| 196 |
-
"import { apiFetch } from '@/utils/api';\n\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };"
|
| 197 |
-
)
|
| 198 |
-
print("OK: Fixed DatasetUploadResponse import")
|
| 199 |
-
|
| 200 |
-
# Add Button to MUI imports
|
| 201 |
-
if "Button" not in content:
|
| 202 |
-
content = content.replace(
|
| 203 |
-
"import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
|
| 204 |
-
"import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
|
| 205 |
-
)
|
| 206 |
-
|
| 207 |
-
# Check if interface already has our props (from previous patch)
|
| 208 |
-
if "showAutoContinue" not in content:
|
| 209 |
-
# Find interface and replace with complete version
|
| 210 |
-
interface_match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', content, re.DOTALL)
|
| 211 |
-
if interface_match:
|
| 212 |
-
props_code = '''
|
| 213 |
-
interface ChatInputProps {
|
| 214 |
-
sessionId: string;
|
| 215 |
-
initialModelPath: string | null | undefined;
|
| 216 |
-
onSend: (text: string) => Promise<void>;
|
| 217 |
-
onStop: () => void;
|
| 218 |
-
onDatasetUploaded: () => Promise<boolean>;
|
| 219 |
-
isProcessing: boolean;
|
| 220 |
-
disabled: boolean;
|
| 221 |
-
placeholder?: string;
|
| 222 |
-
showAutoContinue?: boolean;
|
| 223 |
-
taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
|
| 224 |
-
startAutoContinue?: () => void;
|
| 225 |
-
cancelAutoContinue?: () => void;
|
| 226 |
-
}
|
| 227 |
-
'''
|
| 228 |
-
content = content.replace(interface_match.group(0), props_code)
|
| 229 |
-
print("OK: Updated ChatInputProps interface")
|
| 230 |
-
|
| 231 |
-
# Add auto-continue buttons
|
| 232 |
-
buttons_code = '''
|
| 233 |
-
{/* Auto-continue controls for free models */}
|
| 234 |
-
{showAutoContinue && taskIncompleteInfo && (
|
| 235 |
-
<Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
|
| 236 |
-
<Button
|
| 237 |
-
variant="contained"
|
| 238 |
-
color="primary"
|
| 239 |
-
size="small"
|
| 240 |
-
onClick={startAutoContinue}
|
| 241 |
-
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
|
| 242 |
-
>
|
| 243 |
-
Tự động tiếp tục (10s)
|
| 244 |
-
</Button>
|
| 245 |
-
<Button
|
| 246 |
-
variant="outlined"
|
| 247 |
-
color="secondary"
|
| 248 |
-
size="small"
|
| 249 |
-
onClick={cancelAutoContinue}
|
| 250 |
-
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
|
| 251 |
-
>
|
| 252 |
-
Tạm dừng
|
| 253 |
-
</Button>
|
| 254 |
-
</Stack>
|
| 255 |
-
)}
|
| 256 |
-
'''
|
| 257 |
-
|
| 258 |
-
if "Tự động tiếp tục" not in content:
|
| 259 |
-
content = content.replace(
|
| 260 |
-
"<Box sx={{ flex: 1 ",
|
| 261 |
-
buttons_code + " <Box sx={{ flex: 1 "
|
| 262 |
-
)
|
| 263 |
-
print("OK: Added auto-continue buttons")
|
| 264 |
-
|
| 265 |
-
with open(CHAT_INPUT, "w") as f:
|
| 266 |
-
f.write(content)
|
| 267 |
-
print(f"OK: Patched {CHAT_INPUT}")
|
| 268 |
-
|
| 269 |
-
# =====================================================================
|
| 270 |
-
# PATCH 4: sse-chat-transport.ts - Fix onTaskIncomplete reference
|
| 271 |
-
# =====================================================================
|
| 272 |
-
SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 273 |
-
|
| 274 |
-
def patch_sse_transport():
|
| 275 |
-
if not os.path.exists(SSE_TRANSPORT):
|
| 276 |
-
print(f"SKIP: {SSE_TRANSPORT} not found")
|
| 277 |
-
return
|
| 278 |
-
|
| 279 |
-
with open(SSE_TRANSPORT, "r") as f:
|
| 280 |
-
content = f.read()
|
| 281 |
-
|
| 282 |
-
# Add onTaskIncomplete to SideChannelCallbacks interface
|
| 283 |
-
if "onTaskIncomplete:" not in content and "onInterrupted:" in content:
|
| 284 |
-
content = content.replace(
|
| 285 |
-
" onInterrupted: () => void;",
|
| 286 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 287 |
-
)
|
| 288 |
-
|
| 289 |
-
# Use @ts-ignore for the case since EventType union doesn't include 'task_incomplete'
|
| 290 |
-
# Or better: add the case with a type assertion
|
| 291 |
-
if "case 'task_incomplete'" in content:
|
| 292 |
-
# Already added, make sure it compiles
|
| 293 |
-
old_case = "case 'task_incomplete':\n sideChannel.onTaskIncomplete("
|
| 294 |
-
new_case = "case 'task_incomplete' as const:\n sideChannel.onTaskIncomplete("
|
| 295 |
-
content = content.replace(old_case, new_case)
|
| 296 |
-
|
| 297 |
-
with open(SSE_TRANSPORT, "w") as f:
|
| 298 |
-
f.write(content)
|
| 299 |
-
print(f"OK: Patched {SSE_TRANSPORT}")
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
if __name__ == "__main__":
|
| 303 |
-
patch_events()
|
| 304 |
-
patch_use_agent_chat()
|
| 305 |
-
patch_chat_input()
|
| 306 |
-
patch_sse_transport()
|
| 307 |
-
print("\nDONE: All V2 patches applied")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v3.py
DELETED
|
@@ -1,229 +0,0 @@
|
|
| 1 |
-
"""V3: FINAL - Fix remaining TS errors for auto-continue feature.
|
| 2 |
-
|
| 3 |
-
Errors fixed:
|
| 4 |
-
1. TS6133: 'showAutoContinue'/'taskIncompleteInfo' never read → prefix with _
|
| 5 |
-
2. TS2741: Property 'onTaskIncomplete' missing in sideChannel object → add implementation
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import os
|
| 9 |
-
import re
|
| 10 |
-
|
| 11 |
-
USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 12 |
-
EVENTS_FILE = "/source/frontend/src/types/events.ts"
|
| 13 |
-
CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 14 |
-
SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 15 |
-
|
| 16 |
-
def patch_events():
|
| 17 |
-
if not os.path.exists(EVENTS_FILE):
|
| 18 |
-
print(f"SKIP: {EVENTS_FILE} not found")
|
| 19 |
-
return
|
| 20 |
-
with open(EVENTS_FILE, "r") as f:
|
| 21 |
-
content = f.read()
|
| 22 |
-
if "'task_incomplete'" not in content:
|
| 23 |
-
content = content.replace(
|
| 24 |
-
" | 'plan_update';",
|
| 25 |
-
" | 'plan_update'\n | 'task_incomplete';"
|
| 26 |
-
)
|
| 27 |
-
with open(EVENTS_FILE, "w") as f:
|
| 28 |
-
f.write(content)
|
| 29 |
-
print("OK: Added 'task_incomplete' to EventType")
|
| 30 |
-
|
| 31 |
-
def patch_use_agent_chat():
|
| 32 |
-
if not os.path.exists(USE_AGENT_CHAT):
|
| 33 |
-
print(f"SKIP: {USE_AGENT_CHAT} not found")
|
| 34 |
-
return
|
| 35 |
-
|
| 36 |
-
with open(USE_AGENT_CHAT, "r") as f:
|
| 37 |
-
content = f.read()
|
| 38 |
-
|
| 39 |
-
# 1. Add useState import
|
| 40 |
-
if "useState" not in content:
|
| 41 |
-
content = content.replace(
|
| 42 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 43 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 44 |
-
)
|
| 45 |
-
|
| 46 |
-
# 2. Add state after callbacksRef
|
| 47 |
-
state_block = '''
|
| 48 |
-
// Auto-continue state for free models when task incomplete
|
| 49 |
-
const autoContinueTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
| 50 |
-
const [_showAutoContinue, _setShowAutoContinue] = useState(false);
|
| 51 |
-
const [_taskIncompleteInfo, _setTaskIncompleteInfo] = useState<{
|
| 52 |
-
incompletePlan: Array<{ id: string; content: string; status: string }>;
|
| 53 |
-
} | null>(null);
|
| 54 |
-
'''
|
| 55 |
-
if "autoContinueTimerRef" not in content:
|
| 56 |
-
content = content.replace(
|
| 57 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 58 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };" + state_block
|
| 59 |
-
)
|
| 60 |
-
|
| 61 |
-
# 3. Add onTaskIncomplete to interface
|
| 62 |
-
if "onTaskIncomplete:" not in content and "onInterrupted: () => void;" in content:
|
| 63 |
-
content = content.replace(
|
| 64 |
-
" onInterrupted: () => void;",
|
| 65 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 66 |
-
)
|
| 67 |
-
|
| 68 |
-
# 4. Add onTaskIncomplete handler in sideChannel (BEFORE onInterrupted)
|
| 69 |
-
handler_block = ''' onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
|
| 70 |
-
_setTaskIncompleteInfo({ incompletePlan });
|
| 71 |
-
_setShowAutoContinue(true);
|
| 72 |
-
if (autoContinueTimerRef.current) {
|
| 73 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 74 |
-
}
|
| 75 |
-
autoContinueTimerRef.current = setTimeout(() => {
|
| 76 |
-
_setShowAutoContinue(false);
|
| 77 |
-
_setTaskIncompleteInfo(null);
|
| 78 |
-
const plan = incompletePlan.map(i => `- ${i.content}`).join('\\n');
|
| 79 |
-
chat.sendMessage({
|
| 80 |
-
text: `[TỰ ĐỘNG TIẾP TỤC] Tiếp tục từ các task chưa hoàn thành:\\n${plan}`,
|
| 81 |
-
metadata: { createdAt: new Date().toISOString() },
|
| 82 |
-
});
|
| 83 |
-
}, 10000);
|
| 84 |
-
},
|
| 85 |
-
onInterrupted: () => { /* no-op */ },'''
|
| 86 |
-
|
| 87 |
-
if "onTaskIncomplete: (incompletePlan" not in content:
|
| 88 |
-
old_interrupted = ' onInterrupted: () => { /* no-op — handled by stop() caller */ },'
|
| 89 |
-
if old_interrupted in content:
|
| 90 |
-
content = content.replace(old_interrupted, handler_block)
|
| 91 |
-
else:
|
| 92 |
-
# Try the simpler version
|
| 93 |
-
simple_interrupted = ' onInterrupted: () => { /* no-op */ },'
|
| 94 |
-
if simple_interrupted in content:
|
| 95 |
-
content = content.replace(simple_interrupted, handler_block)
|
| 96 |
-
|
| 97 |
-
# 5. Add cleanup timer before return
|
| 98 |
-
cleanup = '''
|
| 99 |
-
// Cleanup auto-continue timer
|
| 100 |
-
useEffect(() => {
|
| 101 |
-
return () => {
|
| 102 |
-
if (autoContinueTimerRef.current) {
|
| 103 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 104 |
-
}
|
| 105 |
-
};
|
| 106 |
-
}, []);
|
| 107 |
-
'''
|
| 108 |
-
if "autoContinueTimerRef.current" in content and "Cleanup timer" not in content:
|
| 109 |
-
content = content.replace(
|
| 110 |
-
'\n return {\n messages: chat.messages,',
|
| 111 |
-
cleanup + '\n return {\n messages: chat.messages,'
|
| 112 |
-
)
|
| 113 |
-
|
| 114 |
-
with open(USE_AGENT_CHAT, "w") as f:
|
| 115 |
-
f.write(content)
|
| 116 |
-
print(f"OK: Patched {USE_AGENT_CHAT}")
|
| 117 |
-
|
| 118 |
-
def patch_chat_input():
|
| 119 |
-
if not os.path.exists(CHAT_INPUT):
|
| 120 |
-
print(f"SKIP: {CHAT_INPUT} not found")
|
| 121 |
-
return
|
| 122 |
-
|
| 123 |
-
with open(CHAT_INPUT, "r") as f:
|
| 124 |
-
content = f.read()
|
| 125 |
-
|
| 126 |
-
# Fix DatasetUploadResponse - define inline type
|
| 127 |
-
if "DatasetUploadResponse" in content and "type DatasetUploadResponse" not in content:
|
| 128 |
-
wrong = "import type { DatasetUploadResponse } from '@/types/agent';"
|
| 129 |
-
if wrong in content:
|
| 130 |
-
content = content.replace(wrong, "")
|
| 131 |
-
# Add inline type after apiFetch import
|
| 132 |
-
content = content.replace(
|
| 133 |
-
"import { apiFetch } from '@/utils/api';",
|
| 134 |
-
"import { apiFetch } from '@/utils/api';\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };"
|
| 135 |
-
)
|
| 136 |
-
|
| 137 |
-
# Add Button import
|
| 138 |
-
if "Button" not in content:
|
| 139 |
-
content = content.replace(
|
| 140 |
-
"import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
|
| 141 |
-
"import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
|
| 142 |
-
)
|
| 143 |
-
|
| 144 |
-
# Fix interface - replace with complete version
|
| 145 |
-
if "_showAutoContinue" not in content:
|
| 146 |
-
interface_match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', content, re.DOTALL)
|
| 147 |
-
if interface_match:
|
| 148 |
-
props_code = '''
|
| 149 |
-
interface ChatInputProps {
|
| 150 |
-
sessionId: string;
|
| 151 |
-
initialModelPath: string | null | undefined;
|
| 152 |
-
onSend: (text: string) => Promise<void>;
|
| 153 |
-
onStop: () => void;
|
| 154 |
-
onDatasetUploaded: () => Promise<boolean>;
|
| 155 |
-
isProcessing: boolean;
|
| 156 |
-
disabled: boolean;
|
| 157 |
-
placeholder?: string;
|
| 158 |
-
_showAutoContinue?: boolean;
|
| 159 |
-
_taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
|
| 160 |
-
_onCancelAutoContinue?: () => void;
|
| 161 |
-
}
|
| 162 |
-
'''
|
| 163 |
-
content = content.replace(interface_match.group(0), props_code)
|
| 164 |
-
|
| 165 |
-
# Add cancel button
|
| 166 |
-
buttons = '''
|
| 167 |
-
{/* Auto-continue cancel button for free models */}
|
| 168 |
-
{_showAutoContinue && _taskIncompleteInfo && _onCancelAutoContinue && (
|
| 169 |
-
<Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
|
| 170 |
-
<Button
|
| 171 |
-
variant="outlined"
|
| 172 |
-
color="secondary"
|
| 173 |
-
size="small"
|
| 174 |
-
onClick={_onCancelAutoContinue}
|
| 175 |
-
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
|
| 176 |
-
>
|
| 177 |
-
Tạm dừng (để nhập nội dung khác)
|
| 178 |
-
</Button>
|
| 179 |
-
</Stack>
|
| 180 |
-
)}
|
| 181 |
-
'''
|
| 182 |
-
if "Tạm dừng" not in content:
|
| 183 |
-
content = content.replace(
|
| 184 |
-
"<Box sx={{ flex: 1 ",
|
| 185 |
-
buttons + "\n <Box sx={{ flex: 1 "
|
| 186 |
-
)
|
| 187 |
-
|
| 188 |
-
with open(CHAT_INPUT, "w") as f:
|
| 189 |
-
f.write(content)
|
| 190 |
-
print(f"OK: Patched {CHAT_INPUT}")
|
| 191 |
-
|
| 192 |
-
def patch_sse_transport():
|
| 193 |
-
if not os.path.exists(SSE_TRANSPORT):
|
| 194 |
-
print(f"SKIP: {SSE_TRANSPORT} not found")
|
| 195 |
-
return
|
| 196 |
-
|
| 197 |
-
with open(SSE_TRANSPORT, "r") as f:
|
| 198 |
-
content = f.read()
|
| 199 |
-
|
| 200 |
-
# Add onTaskIncomplete to SideChannelCallbacks interface
|
| 201 |
-
if "onTaskIncomplete:" not in content and "onInterrupted: () => void;" in content:
|
| 202 |
-
content = content.replace(
|
| 203 |
-
" onInterrupted: () => void;",
|
| 204 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 205 |
-
)
|
| 206 |
-
|
| 207 |
-
# Add case 'task_incomplete'
|
| 208 |
-
if "case 'task_incomplete'" not in content:
|
| 209 |
-
task_case = ''' case 'task_incomplete' as const:
|
| 210 |
-
sideChannel.onTaskIncomplete(
|
| 211 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 212 |
-
);
|
| 213 |
-
break;
|
| 214 |
-
'''
|
| 215 |
-
content = content.replace(
|
| 216 |
-
"case 'turn_complete':",
|
| 217 |
-
task_case + "\n case 'turn_complete':"
|
| 218 |
-
)
|
| 219 |
-
|
| 220 |
-
with open(SSE_TRANSPORT, "w") as f:
|
| 221 |
-
f.write(content)
|
| 222 |
-
print(f"OK: Patched {SSE_TRANSPORT}")
|
| 223 |
-
|
| 224 |
-
if __name__ == "__main__":
|
| 225 |
-
patch_events()
|
| 226 |
-
patch_use_agent_chat()
|
| 227 |
-
patch_chat_input()
|
| 228 |
-
patch_sse_transport()
|
| 229 |
-
print("\nDONE: V3 patches applied")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v4.py
DELETED
|
@@ -1,264 +0,0 @@
|
|
| 1 |
-
"""V4: FINAL WORKING - Fix all TS errors for auto-continue feature.
|
| 2 |
-
|
| 3 |
-
Strategy:
|
| 4 |
-
- useAgentChat: return _showAutoContinue + _cancelAutoContinue (TS _ prefix suppresses "unused")
|
| 5 |
-
- ChatInput.tsx: receive props and render "Tạm dừng" button
|
| 6 |
-
- SessionChat.tsx: pass props from hook to ChatInput
|
| 7 |
-
- events.ts: add 'task_incomplete' to EventType union
|
| 8 |
-
- sse-chat-transport.ts: add onTaskIncomplete + case handler
|
| 9 |
-
- agent_loop.py: send task_incomplete event
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
import os
|
| 13 |
-
import re
|
| 14 |
-
|
| 15 |
-
EVENTS = "/source/frontend/src/types/events.ts"
|
| 16 |
-
USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 17 |
-
SSE = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 18 |
-
CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 19 |
-
SESSION_CHAT = "/source/frontend/src/components/SessionChat.tsx"
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def patch_events():
|
| 23 |
-
with open(EVENTS, "r") as f:
|
| 24 |
-
c = f.read()
|
| 25 |
-
if "'task_incomplete'" not in c:
|
| 26 |
-
c = c.replace(" | 'plan_update';", " | 'plan_update'\n | 'task_incomplete';")
|
| 27 |
-
with open(EVENTS, "w") as f: f.write(c)
|
| 28 |
-
print("OK: events.ts")
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def patch_sse():
|
| 32 |
-
with open(SSE, "r") as f:
|
| 33 |
-
c = f.read()
|
| 34 |
-
|
| 35 |
-
# Add onTaskIncomplete to interface
|
| 36 |
-
if "onTaskIncomplete:" not in c:
|
| 37 |
-
c = c.replace(
|
| 38 |
-
" onInterrupted: () => void;",
|
| 39 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 40 |
-
)
|
| 41 |
-
|
| 42 |
-
# Add case handler
|
| 43 |
-
if "case 'task_incomplete'" not in c:
|
| 44 |
-
case = """ case 'task_incomplete' as const:
|
| 45 |
-
sideChannel.onTaskIncomplete(
|
| 46 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 47 |
-
);
|
| 48 |
-
break;
|
| 49 |
-
"""
|
| 50 |
-
c = c.replace("case 'turn_complete':", case + "\n case 'turn_complete':")
|
| 51 |
-
|
| 52 |
-
with open(SSE, "w") as f: f.write(c)
|
| 53 |
-
print("OK: sse-chat-transport.ts")
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def patch_use_agent_chat():
|
| 57 |
-
with open(USE_AGENT_CHAT, "r") as f:
|
| 58 |
-
c = f.read()
|
| 59 |
-
|
| 60 |
-
# 1. Import useState
|
| 61 |
-
if "useState" not in c:
|
| 62 |
-
c = c.replace(
|
| 63 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 64 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 65 |
-
)
|
| 66 |
-
|
| 67 |
-
# 2. State (TS _ prefix = unused ok)
|
| 68 |
-
state_block = '''
|
| 69 |
-
// Auto-continue for free models
|
| 70 |
-
const _autoContinueTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
| 71 |
-
const [_showAutoContinue, _setShowAutoContinue] = useState(false);
|
| 72 |
-
'''
|
| 73 |
-
if "_autoContinueTimer" not in c:
|
| 74 |
-
c = c.replace(
|
| 75 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 76 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };" + state_block
|
| 77 |
-
)
|
| 78 |
-
|
| 79 |
-
# 3. onTaskIncomplete in interface
|
| 80 |
-
if "onTaskIncomplete:" not in c:
|
| 81 |
-
c = c.replace(
|
| 82 |
-
" onInterrupted: () => void;",
|
| 83 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 84 |
-
)
|
| 85 |
-
|
| 86 |
-
# 4. Cancel ref
|
| 87 |
-
cancel_ref = ' const _cancelAutoContinue = useRef(() => {});\n'
|
| 88 |
-
if "_cancelAutoContinue" not in c:
|
| 89 |
-
# Insert after callbacksRef.current = { ... } area
|
| 90 |
-
c = c.replace(
|
| 91 |
-
" isActiveRef.current = isActive;",
|
| 92 |
-
" isActiveRef.current = isActive;\n" + cancel_ref
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
# 5. Handler in sideChannel - inline all logic
|
| 96 |
-
handler = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
|
| 97 |
-
_setShowAutoContinue(true);
|
| 98 |
-
if (_autoContinueTimer.current) clearTimeout(_autoContinueTimer.current);
|
| 99 |
-
_autoContinueTimer.current = setTimeout(() => {
|
| 100 |
-
_setShowAutoContinue(false);
|
| 101 |
-
const plan = incompletePlan.map(i => `- ${i.content}`).join('\\\\n');
|
| 102 |
-
chat.sendMessage({
|
| 103 |
-
text: `[TỰ ĐỘNG TIẾP TỤC] Tiếp tục từ các task chưa hoàn thành:\\\\n${plan}`,
|
| 104 |
-
metadata: { createdAt: new Date().toISOString() },
|
| 105 |
-
});
|
| 106 |
-
}, 10000);
|
| 107 |
-
},
|
| 108 |
-
onInterrupted: () => { /* no-op */ },"""
|
| 109 |
-
|
| 110 |
-
if "onTaskIncomplete: (incompletePlan" not in c:
|
| 111 |
-
old = ' onInterrupted: () => { /* no-op — handled by stop() caller */ },'
|
| 112 |
-
if old in c:
|
| 113 |
-
c = c.replace(old, handler)
|
| 114 |
-
|
| 115 |
-
# 6. Set cancel ref function + cleanup
|
| 116 |
-
cancel_func = """
|
| 117 |
-
_cancelAutoContinue.current = () => {
|
| 118 |
-
_setShowAutoContinue(false);
|
| 119 |
-
if (_autoContinueTimer.current) {
|
| 120 |
-
clearTimeout(_autoContinueTimer.current);
|
| 121 |
-
_autoContinueTimer.current = null;
|
| 122 |
-
}
|
| 123 |
-
};
|
| 124 |
-
"""
|
| 125 |
-
if "_cancelAutoContinue.current = ()" not in c:
|
| 126 |
-
# Insert before return
|
| 127 |
-
c = c.replace(
|
| 128 |
-
"\n return {\n messages: chat.messages,",
|
| 129 |
-
cancel_func + "\n return {\n messages: chat.messages,"
|
| 130 |
-
)
|
| 131 |
-
|
| 132 |
-
# 7. Return values
|
| 133 |
-
if "_showAutoContinue," not in c:
|
| 134 |
-
c = c.replace(
|
| 135 |
-
"refreshMessages,\n };",
|
| 136 |
-
"refreshMessages,\n _showAutoContinue,\n _cancelAutoContinue,\n };"
|
| 137 |
-
)
|
| 138 |
-
|
| 139 |
-
with open(USE_AGENT_CHAT, "w") as f: f.write(c)
|
| 140 |
-
print("OK: useAgentChat.ts")
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
def patch_chat_input():
|
| 144 |
-
with open(CHAT_INPUT, "r") as f:
|
| 145 |
-
c = f.read()
|
| 146 |
-
|
| 147 |
-
# Fix DatasetUploadResponse
|
| 148 |
-
if "DatasetUploadResponse" in c and "inline" not in c:
|
| 149 |
-
wrong = "import type { DatasetUploadResponse } from '@/types/agent';"
|
| 150 |
-
if wrong in c:
|
| 151 |
-
c = c.replace(wrong, "")
|
| 152 |
-
c = c.replace(
|
| 153 |
-
"import { apiFetch } from '@/utils/api';",
|
| 154 |
-
"import { apiFetch } from '@/utils/api';\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };"
|
| 155 |
-
)
|
| 156 |
-
|
| 157 |
-
# Add Button import
|
| 158 |
-
if "Button" not in c:
|
| 159 |
-
c = c.replace(
|
| 160 |
-
"import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
|
| 161 |
-
"import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
|
| 162 |
-
)
|
| 163 |
-
|
| 164 |
-
# Add props to interface
|
| 165 |
-
if "_showAutoContinue" not in c:
|
| 166 |
-
match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', c, re.DOTALL)
|
| 167 |
-
if match:
|
| 168 |
-
new_iface = """interface ChatInputProps {
|
| 169 |
-
sessionId: string;
|
| 170 |
-
initialModelPath: string | null | undefined;
|
| 171 |
-
onSend: (text: string) => Promise<void>;
|
| 172 |
-
onStop: () => void;
|
| 173 |
-
onDatasetUploaded: () => Promise<boolean>;
|
| 174 |
-
isProcessing: boolean;
|
| 175 |
-
disabled: boolean;
|
| 176 |
-
placeholder?: string;
|
| 177 |
-
_showAutoContinue?: boolean;
|
| 178 |
-
_cancelAutoContinue?: React.RefObject<() => void>;
|
| 179 |
-
}"""
|
| 180 |
-
c = c.replace(match.group(0), new_iface)
|
| 181 |
-
|
| 182 |
-
# Add pause button
|
| 183 |
-
btn = """
|
| 184 |
-
{/* Pause auto-continue for free models */}
|
| 185 |
-
{_showAutoContinue && _cancelAutoContinue?.current && (
|
| 186 |
-
<Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
|
| 187 |
-
<Button
|
| 188 |
-
variant="outlined"
|
| 189 |
-
color="secondary"
|
| 190 |
-
size="small"
|
| 191 |
-
onClick={_cancelAutoContinue.current}
|
| 192 |
-
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
|
| 193 |
-
>
|
| 194 |
-
Tạm dừng (để nhập nội dung khác)
|
| 195 |
-
</Button>
|
| 196 |
-
</Stack>
|
| 197 |
-
)}
|
| 198 |
-
"""
|
| 199 |
-
if "Tạm dừng" not in c:
|
| 200 |
-
c = c.replace("<Box sx={{ flex: 1 ", btn + "\n <Box sx={{ flex: 1 ")
|
| 201 |
-
|
| 202 |
-
with open(CHAT_INPUT, "w") as f: f.write(c)
|
| 203 |
-
print("OK: ChatInput.tsx")
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
def patch_session_chat():
|
| 207 |
-
with open(SESSION_CHAT, "r") as f:
|
| 208 |
-
c = f.read()
|
| 209 |
-
|
| 210 |
-
# Find the ChatInput usage and add new props
|
| 211 |
-
if "_showAutoContinue" not in c:
|
| 212 |
-
chat_input_pattern = r'(<ChatInput[^>]*sessionId={sessionId}[^>]*initialModelPath={initialModelPath}[^>]*)(\s*/>)'
|
| 213 |
-
new_props = r'\1 _showAutoContinue={_showAutoContinue} _cancelAutoContinue={_cancelAutoContinue}\2'
|
| 214 |
-
c = re.sub(chat_input_pattern, new_props, c)
|
| 215 |
-
|
| 216 |
-
with open(SESSION_CHAT, "w") as f: f.write(c)
|
| 217 |
-
print("OK: SessionChat.tsx")
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
def patch_agent_loop():
|
| 221 |
-
agent_loop = "/app/agent/core/agent_loop.py"
|
| 222 |
-
if not os.path.exists(agent_loop):
|
| 223 |
-
print(f"SKIP: {agent_loop} not found (backend)")
|
| 224 |
-
return
|
| 225 |
-
|
| 226 |
-
with open(agent_loop, "r") as f:
|
| 227 |
-
c = f.read()
|
| 228 |
-
|
| 229 |
-
if "_check_task_incomplete" not in c:
|
| 230 |
-
func = '''
|
| 231 |
-
|
| 232 |
-
def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
|
| 233 |
-
plan = getattr(session, "current_plan", None) or []
|
| 234 |
-
return [item for item in plan if item.get("status") in ("pending", "in_progress")]
|
| 235 |
-
|
| 236 |
-
'''
|
| 237 |
-
c = c.replace("class Handlers:", func + "\n\nclass Handlers:")
|
| 238 |
-
|
| 239 |
-
if "task_incomplete" not in c:
|
| 240 |
-
check = '''
|
| 241 |
-
# === Auto-continue: if model stopped but plan incomplete ===
|
| 242 |
-
if not llm_result.tool_calls_acc and llm_result.content:
|
| 243 |
-
unfinished = _unfinished_plan_items(session)
|
| 244 |
-
if unfinished:
|
| 245 |
-
await session.send_event(
|
| 246 |
-
Event(event_type="task_incomplete", data={"incomplete_plan": unfinished})
|
| 247 |
-
)
|
| 248 |
-
'''
|
| 249 |
-
marker = " # -- End of turn --"
|
| 250 |
-
if marker in c:
|
| 251 |
-
c = c.replace(marker, check + "\n" + marker)
|
| 252 |
-
|
| 253 |
-
with open(agent_loop, "w") as f: f.write(c)
|
| 254 |
-
print("OK: agent_loop.py")
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
if __name__ == "__main__":
|
| 258 |
-
patch_events()
|
| 259 |
-
patch_sse()
|
| 260 |
-
patch_use_agent_chat()
|
| 261 |
-
patch_chat_input()
|
| 262 |
-
patch_session_chat()
|
| 263 |
-
patch_agent_loop()
|
| 264 |
-
print("\nDONE: V4 - everything applied")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v5.py
DELETED
|
@@ -1,252 +0,0 @@
|
|
| 1 |
-
"""V5: SIMPLEST approach - just show pause button with 10s auto-continue.
|
| 2 |
-
|
| 3 |
-
Architecture:
|
| 4 |
-
1. Agent loop detects model stopped mid-task → sends 'task_incomplete' event
|
| 5 |
-
2. SSE transport receives it → calls sideChannel.onTaskIncomplete()
|
| 6 |
-
3. useAgentChat: onTaskIncomplete sets _showAutoContinue=true, sends continue msg
|
| 7 |
-
4. ChatInput shows "Tạm dừng" button if _showAutoContinue
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
import os, re
|
| 11 |
-
|
| 12 |
-
EVENTS = "/source/frontend/src/types/events.ts"
|
| 13 |
-
SSE = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 14 |
-
USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 15 |
-
CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 16 |
-
AGENT_LOOP = "/app/agent/core/agent_loop.py"
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def patch_events():
|
| 20 |
-
with open(EVENTS, "r") as f:
|
| 21 |
-
c = f.read()
|
| 22 |
-
if "'task_incomplete'" not in c:
|
| 23 |
-
c = c.replace(" | 'plan_update';", " | 'plan_update'\n | 'task_incomplete';")
|
| 24 |
-
with open(EVENTS, "w") as f:
|
| 25 |
-
f.write(c)
|
| 26 |
-
print("OK: events.ts")
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def patch_sse():
|
| 30 |
-
with open(SSE, "r") as f:
|
| 31 |
-
c = f.read()
|
| 32 |
-
|
| 33 |
-
if "onTaskIncomplete:" not in c:
|
| 34 |
-
c = c.replace(
|
| 35 |
-
" onInterrupted: () => void;",
|
| 36 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 37 |
-
)
|
| 38 |
-
|
| 39 |
-
if "case 'task_incomplete'" not in c:
|
| 40 |
-
case_block = """ case 'task_incomplete' as const:
|
| 41 |
-
sideChannel.onTaskIncomplete(
|
| 42 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 43 |
-
);
|
| 44 |
-
break;
|
| 45 |
-
"""
|
| 46 |
-
c = c.replace("case 'turn_complete':", case_block + "\n case 'turn_complete':")
|
| 47 |
-
|
| 48 |
-
with open(SSE, "w") as f:
|
| 49 |
-
f.write(c)
|
| 50 |
-
print("OK: sse.ts")
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def patch_use_agent():
|
| 54 |
-
with open(USE_AGENT_CHAT, "r") as f:
|
| 55 |
-
c = f.read()
|
| 56 |
-
|
| 57 |
-
# 1. Import useState
|
| 58 |
-
if "useState" not in c:
|
| 59 |
-
c = c.replace(
|
| 60 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 61 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 62 |
-
)
|
| 63 |
-
|
| 64 |
-
# 2. State (prefixed with _ = TS allows unused)
|
| 65 |
-
state = '''
|
| 66 |
-
// Auto-continue state for free models
|
| 67 |
-
const _acTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
| 68 |
-
const [_showAc, _setShowAc] = useState(false);'''
|
| 69 |
-
if "_acTimer" not in c:
|
| 70 |
-
c = c.replace(
|
| 71 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 72 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };" + state
|
| 73 |
-
)
|
| 74 |
-
|
| 75 |
-
# 3. Add onTaskIncomplete to interface
|
| 76 |
-
if "onTaskIncomplete:" not in c:
|
| 77 |
-
c = c.replace(
|
| 78 |
-
" onInterrupted: () => void;",
|
| 79 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 80 |
-
)
|
| 81 |
-
|
| 82 |
-
# 4. Add onTaskIncomplete handler - uses chatActionsRef ref instead of chat directly
|
| 83 |
-
handler = ''' onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
|
| 84 |
-
_setShowAc(true);
|
| 85 |
-
if (_acTimer.current) clearTimeout(_acTimer.current);
|
| 86 |
-
_acTimer.current = setTimeout(() => {
|
| 87 |
-
_setShowAc(false);
|
| 88 |
-
// Auto-continue after 10s via chatActionsRef
|
| 89 |
-
const chatRef = chatActionsRef.current;
|
| 90 |
-
const sendMsg = (chatRef as any)?.sendMessage;
|
| 91 |
-
if (sendMsg) {
|
| 92 |
-
const planStr = incompletePlan.map(i => `- ${i.content}`).join('\\\\n');
|
| 93 |
-
sendMsg({ text: `[TIẾP TỤC] Tiếp tục task chưa hoàn thành:\\\\n${planStr}`, metadata: { createdAt: new Date().toISOString() } });
|
| 94 |
-
}
|
| 95 |
-
}, 10000);
|
| 96 |
-
},
|
| 97 |
-
onInterrupted: () => { /* no-op */ },'''
|
| 98 |
-
|
| 99 |
-
if "onTaskIncomplete: (incompletePlan" not in c:
|
| 100 |
-
old = ' onInterrupted: () => { /* no-op — handled by stop() caller */ },'
|
| 101 |
-
if old in c:
|
| 102 |
-
c = c.replace(old, handler)
|
| 103 |
-
else:
|
| 104 |
-
old2 = ' onInterrupted: () => { /* no-op */ },'
|
| 105 |
-
c = c.replace(old2, handler)
|
| 106 |
-
|
| 107 |
-
# 5. Cancel ref + return values
|
| 108 |
-
if "_showAc" not in content:
|
| 109 |
-
# Insert cancel before return
|
| 110 |
-
cancel = '''
|
| 111 |
-
// Expose auto-continue state
|
| 112 |
-
const _acCancel = useCallback(() => {
|
| 113 |
-
_setShowAc(false);
|
| 114 |
-
if (_acTimer.current) { clearTimeout(_acTimer.current); _acTimer.current = null; }
|
| 115 |
-
}, []);
|
| 116 |
-
'''
|
| 117 |
-
if "_acCancel" not in c:
|
| 118 |
-
c = c.replace('\n return {\n messages: chat.messages,', cancel + '\n return {\n messages: chat.messages,')
|
| 119 |
-
|
| 120 |
-
if "_showAc," not in c:
|
| 121 |
-
c = c.replace(
|
| 122 |
-
"refreshMessages,\n };",
|
| 123 |
-
"refreshMessages,\n _showAc,\n _acCancel,\n };"
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
with open(USE_AGENT_CHAT, "w") as f:
|
| 127 |
-
f.write(c)
|
| 128 |
-
print("OK: useAgentChat.ts")
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
def patch_chat_input():
|
| 132 |
-
with open(CHAT_INPUT, "r") as f:
|
| 133 |
-
c = f.read()
|
| 134 |
-
|
| 135 |
-
# DatasetUploadResponse fix
|
| 136 |
-
if "DatasetUploadResponse" in c and "type DatasetUploadResponse" not in c:
|
| 137 |
-
wrong = "import type { DatasetUploadResponse } from '@/types/agent';"
|
| 138 |
-
if wrong in c:
|
| 139 |
-
c = c.replace(wrong, "")
|
| 140 |
-
c = c.replace(
|
| 141 |
-
"import { apiFetch } from '@/utils/api';",
|
| 142 |
-
"import { apiFetch } from '@/utils/api';\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };"
|
| 143 |
-
)
|
| 144 |
-
|
| 145 |
-
# Add Button
|
| 146 |
-
if "Button" not in c:
|
| 147 |
-
c = c.replace(
|
| 148 |
-
"import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
|
| 149 |
-
"import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
|
| 150 |
-
)
|
| 151 |
-
|
| 152 |
-
# Update interface
|
| 153 |
-
if "_showAc" not in c:
|
| 154 |
-
m = re.search(r'interface ChatInputProps\s*\{[^}]*\}', c, re.DOTALL)
|
| 155 |
-
if m:
|
| 156 |
-
iface = """interface ChatInputProps {
|
| 157 |
-
sessionId: string;
|
| 158 |
-
initialModelPath: string | null | undefined;
|
| 159 |
-
onSend: (text: string) => Promise<void>;
|
| 160 |
-
onStop: () => void;
|
| 161 |
-
onDatasetUploaded: () => Promise<boolean>;
|
| 162 |
-
isProcessing: boolean;
|
| 163 |
-
disabled: boolean;
|
| 164 |
-
placeholder?: string;
|
| 165 |
-
_showAc?: boolean;
|
| 166 |
-
_acCancel?: () => void;
|
| 167 |
-
}"""
|
| 168 |
-
c = c.replace(m.group(0), iface)
|
| 169 |
-
|
| 170 |
-
# Add button
|
| 171 |
-
btn = """
|
| 172 |
-
{/* Auto-continue pause button for free models */}
|
| 173 |
-
{_showAc && _acCancel && (
|
| 174 |
-
<Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
|
| 175 |
-
<Button
|
| 176 |
-
variant="outlined"
|
| 177 |
-
color="secondary"
|
| 178 |
-
size="small"
|
| 179 |
-
onClick={_acCancel}
|
| 180 |
-
sx={{ textTransform: 'none', fontSize: '0.7rem' }}
|
| 181 |
-
>
|
| 182 |
-
Tạm dừng tự động tiếp tục (10s)
|
| 183 |
-
</Button>
|
| 184 |
-
</Stack>
|
| 185 |
-
)}
|
| 186 |
-
"""
|
| 187 |
-
if "Tạm dừng" not in c:
|
| 188 |
-
c = c.replace("<Box sx={{ flex: 1 ", btn + "\n <Box sx={{ flex: 1 ")
|
| 189 |
-
|
| 190 |
-
with open(CHAT_INPUT, "w") as f:
|
| 191 |
-
f.write(c)
|
| 192 |
-
print("OK: ChatInput.tsx")
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
def patch_session_chat():
|
| 196 |
-
sess = "/source/frontend/src/components/SessionChat.tsx"
|
| 197 |
-
if not os.path.exists(sess):
|
| 198 |
-
print(f"SKIP: {sess}")
|
| 199 |
-
return
|
| 200 |
-
with open(sess, "r") as f:
|
| 201 |
-
c = f.read()
|
| 202 |
-
if "_showAc" not in c:
|
| 203 |
-
c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
|
| 204 |
-
with open(sess, "w") as f:
|
| 205 |
-
f.write(c)
|
| 206 |
-
print("OK: SessionChat.tsx")
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
def patch_agent_loop():
|
| 210 |
-
if not os.path.exists(AGENT_LOOP):
|
| 211 |
-
print(f"SKIP: {AGENT_LOOP}")
|
| 212 |
-
return
|
| 213 |
-
with open(AGENT_LOOP, "r") as f:
|
| 214 |
-
c = f.read()
|
| 215 |
-
|
| 216 |
-
if "_unfinished_plan" not in c:
|
| 217 |
-
fn = '''
|
| 218 |
-
|
| 219 |
-
def _unfinished_plan(session: Session) -> list[dict[str, str]]:
|
| 220 |
-
plan = getattr(session, "current_plan", None) or []
|
| 221 |
-
return [item for item in plan if item.get("status") in ("pending", "in_progress")]
|
| 222 |
-
|
| 223 |
-
'''
|
| 224 |
-
c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
|
| 225 |
-
|
| 226 |
-
if "task_incomplete" not in c:
|
| 227 |
-
check = '''
|
| 228 |
-
# === Auto-continue detection ===
|
| 229 |
-
if not llm_result.tool_calls_acc and llm_result.content:
|
| 230 |
-
unfinished = _unfinished_plan(session)
|
| 231 |
-
if unfinished:
|
| 232 |
-
await session.send_event(
|
| 233 |
-
Event(event_type="task_incomplete", data={"incomplete_plan": unfinished})
|
| 234 |
-
)
|
| 235 |
-
'''
|
| 236 |
-
marker = " # -- End of turn --"
|
| 237 |
-
if marker in c:
|
| 238 |
-
c = c.replace(marker, check + "\n" + marker)
|
| 239 |
-
|
| 240 |
-
with open(AGENT_LOOP, "w") as f:
|
| 241 |
-
f.write(c)
|
| 242 |
-
print("OK: agent_loop.py")
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
if __name__ == "__main__":
|
| 246 |
-
patch_events()
|
| 247 |
-
patch_sse()
|
| 248 |
-
patch_use_agent()
|
| 249 |
-
patch_chat_input()
|
| 250 |
-
patch_session_chat()
|
| 251 |
-
patch_agent_loop()
|
| 252 |
-
print("\nDONE: V5 - simple approach applied")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v6.py
DELETED
|
@@ -1,243 +0,0 @@
|
|
| 1 |
-
"""V6: FIXED - Auto-continue with pause button for free models."""
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
import re
|
| 5 |
-
|
| 6 |
-
EVENTS_FILE = "/source/frontend/src/types/events.ts"
|
| 7 |
-
SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 8 |
-
HOOK_FILE = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 9 |
-
CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 10 |
-
SESSION_CHAT = "/source/frontend/src/components/SessionChat.tsx"
|
| 11 |
-
AGENT_LOOP = "/app/agent/core/agent_loop.py"
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
def fix_events():
|
| 15 |
-
with open(EVENTS_FILE, "r") as f:
|
| 16 |
-
content = f.read()
|
| 17 |
-
if "'task_incomplete'" not in content:
|
| 18 |
-
content = content.replace(" | 'plan_update';", " | 'plan_update'\n | 'task_incomplete';")
|
| 19 |
-
with open(EVENTS_FILE, "w") as f:
|
| 20 |
-
f.write(content)
|
| 21 |
-
print("OK: events.ts")
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def fix_sse():
|
| 25 |
-
with open(SSE_TRANSPORT, "r") as f:
|
| 26 |
-
content = f.read()
|
| 27 |
-
|
| 28 |
-
# Add onTaskIncomplete to interface
|
| 29 |
-
if "onTaskIncomplete:" not in content and "onInterrupted: () => void;" in content:
|
| 30 |
-
content = content.replace(
|
| 31 |
-
" onInterrupted: () => void;",
|
| 32 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 33 |
-
)
|
| 34 |
-
|
| 35 |
-
# Add case handler
|
| 36 |
-
if "case 'task_incomplete'" not in content:
|
| 37 |
-
case_code = """ case 'task_incomplete' as const:
|
| 38 |
-
sideChannel.onTaskIncomplete(
|
| 39 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 40 |
-
);
|
| 41 |
-
break;
|
| 42 |
-
"""
|
| 43 |
-
content = content.replace("case 'turn_complete':", case_code + "\n case 'turn_complete':")
|
| 44 |
-
|
| 45 |
-
with open(SSE_TRANSPORT, "w") as f:
|
| 46 |
-
f.write(content)
|
| 47 |
-
print("OK: sse-chat-transport.ts")
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def fix_hook():
|
| 51 |
-
with open(HOOK_FILE, "r") as f:
|
| 52 |
-
content = f.read()
|
| 53 |
-
|
| 54 |
-
# 1. Import useState
|
| 55 |
-
if "useState" not in content:
|
| 56 |
-
content = content.replace(
|
| 57 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 58 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 59 |
-
)
|
| 60 |
-
|
| 61 |
-
# 2. Add state after callbacksRef block
|
| 62 |
-
state_block = """
|
| 63 |
-
// Auto-continue for free models
|
| 64 |
-
const _acTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
| 65 |
-
const [_showAc, _setShowAc] = useState(false);
|
| 66 |
-
const _acCancel = useCallback(() => {
|
| 67 |
-
_setShowAc(false);
|
| 68 |
-
if (_acTimer.current) { clearTimeout(_acTimer.current); _acTimer.current = null; }
|
| 69 |
-
}, []);"""
|
| 70 |
-
|
| 71 |
-
if "_acTimer" not in content:
|
| 72 |
-
content = content.replace(
|
| 73 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 74 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };" + state_block
|
| 75 |
-
)
|
| 76 |
-
|
| 77 |
-
# 3. Add onTaskIncomplete to interface
|
| 78 |
-
if "onTaskIncomplete:" not in content and "onInterrupted: () => void;" in content:
|
| 79 |
-
content = content.replace(
|
| 80 |
-
" onInterrupted: () => void;",
|
| 81 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 82 |
-
)
|
| 83 |
-
|
| 84 |
-
# 4. Add handler in sideChannel - uses chatActionsRef to avoid hoisting issues
|
| 85 |
-
handler_code = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
|
| 86 |
-
_setShowAc(true);
|
| 87 |
-
if (_acTimer.current) clearTimeout(_acTimer.current);
|
| 88 |
-
_acTimer.current = setTimeout(() => {
|
| 89 |
-
_setShowAc(false);
|
| 90 |
-
const planStr = incompletePlan.map(i => `- ${i.content}`).join('\\\\n');
|
| 91 |
-
const msg = { text: `[TIẾP TỤC] Task chưa hoàn thành:\\\\n${planStr}`, metadata: { createdAt: new Date().toISOString() } };
|
| 92 |
-
const setMsgs = chatActionsRef.current.setMessages;
|
| 93 |
-
if (setMsgs) {
|
| 94 |
-
chatActionsRef.current.messages = [...chatActionsRef.current.messages, { id: 'ac-continue', role: 'user', parts: [{ type: 'text', text: msg.text }], content: msg.text }];
|
| 95 |
-
}
|
| 96 |
-
}, 10000);
|
| 97 |
-
},
|
| 98 |
-
onInterrupted: () => { /* no-op - handled by stop() */ },"""
|
| 99 |
-
|
| 100 |
-
if "onTaskIncomplete: (incompletePlan" not in content:
|
| 101 |
-
old_handler = ' onInterrupted: () => { /* no-op \u2014 handled by stop() caller */ },'
|
| 102 |
-
if old_handler in content:
|
| 103 |
-
content = content.replace(old_handler, handler_code)
|
| 104 |
-
else:
|
| 105 |
-
old_handler2 = ' onInterrupted: () => { /* no-op */ },'
|
| 106 |
-
content = content.replace(old_handler2, handler_code)
|
| 107 |
-
|
| 108 |
-
# 5. Return values
|
| 109 |
-
if "_showAc" not in content:
|
| 110 |
-
# The _showAc and _acCancel need to be in the return
|
| 111 |
-
return_marker = "refreshMessages,\n };"
|
| 112 |
-
if return_marker in content:
|
| 113 |
-
content = content.replace(
|
| 114 |
-
return_marker,
|
| 115 |
-
"refreshMessages,\n _showAc,\n _acCancel,\n };"
|
| 116 |
-
)
|
| 117 |
-
|
| 118 |
-
with open(HOOK_FILE, "w") as f:
|
| 119 |
-
f.write(content)
|
| 120 |
-
print("OK: useAgentChat.ts")
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
def fix_chat_input():
|
| 124 |
-
with open(CHAT_INPUT, "r") as f:
|
| 125 |
-
content = f.read()
|
| 126 |
-
|
| 127 |
-
# Fix DatasetUploadResponse
|
| 128 |
-
wrong_import = "import type { DatasetUploadResponse } from '@/types/agent';"
|
| 129 |
-
if wrong_import in content:
|
| 130 |
-
content = content.replace(wrong_import, "")
|
| 131 |
-
content = content.replace(
|
| 132 |
-
"import { apiFetch } from '@/utils/api';",
|
| 133 |
-
"import { apiFetch } from '@/utils/api';\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };"
|
| 134 |
-
)
|
| 135 |
-
|
| 136 |
-
# Add Button import
|
| 137 |
-
if "Button" not in content:
|
| 138 |
-
content = content.replace(
|
| 139 |
-
"import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
|
| 140 |
-
"import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
|
| 141 |
-
)
|
| 142 |
-
|
| 143 |
-
# Fix interface
|
| 144 |
-
if "_showAc" not in content:
|
| 145 |
-
iface_match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', content, re.DOTALL)
|
| 146 |
-
if iface_match:
|
| 147 |
-
new_iface = """interface ChatInputProps {
|
| 148 |
-
sessionId: string;
|
| 149 |
-
initialModelPath: string | null | undefined;
|
| 150 |
-
onSend: (text: string) => Promise<void>;
|
| 151 |
-
onStop: () => void;
|
| 152 |
-
onDatasetUploaded: () => Promise<boolean>;
|
| 153 |
-
isProcessing: boolean;
|
| 154 |
-
disabled: boolean;
|
| 155 |
-
placeholder?: string;
|
| 156 |
-
_showAc?: boolean;
|
| 157 |
-
_acCancel?: () => void;
|
| 158 |
-
}"""
|
| 159 |
-
content = content.replace(iface_match.group(0), new_iface)
|
| 160 |
-
|
| 161 |
-
# Add pause button
|
| 162 |
-
btn_code = """
|
| 163 |
-
{/* Auto-continue pause for free models */}
|
| 164 |
-
{_showAc && _acCancel && (
|
| 165 |
-
<Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
|
| 166 |
-
<Button
|
| 167 |
-
variant="outlined"
|
| 168 |
-
color="secondary"
|
| 169 |
-
size="small"
|
| 170 |
-
onClick={_acCancel}
|
| 171 |
-
sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.8 }}
|
| 172 |
-
>
|
| 173 |
-
T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
|
| 174 |
-
</Button>
|
| 175 |
-
</Stack>
|
| 176 |
-
)}"""
|
| 177 |
-
|
| 178 |
-
if "T\u1ea1m d\u1eebng" not in content and "free models" not in content:
|
| 179 |
-
content = content.replace("<Box sx={{ flex: 1 ", btn_code + "\n <Box sx={{ flex: 1 ")
|
| 180 |
-
|
| 181 |
-
with open(CHAT_INPUT, "w") as f:
|
| 182 |
-
f.write(content)
|
| 183 |
-
print("OK: ChatInput.tsx")
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
def fix_session_chat():
|
| 187 |
-
with open(SESSION_CHAT, "r") as f:
|
| 188 |
-
content = f.read()
|
| 189 |
-
|
| 190 |
-
# Add props to ChatInput
|
| 191 |
-
if "_showAc" not in content:
|
| 192 |
-
content = content.replace(
|
| 193 |
-
"<ChatInput ",
|
| 194 |
-
"<ChatInput _showAc={_showAc} _acCancel={_acCancel} "
|
| 195 |
-
)
|
| 196 |
-
|
| 197 |
-
with open(SESSION_CHAT, "w") as f:
|
| 198 |
-
f.write(content)
|
| 199 |
-
print("OK: SessionChat.tsx")
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
def fix_agent_loop():
|
| 203 |
-
if not os.path.exists(AGENT_LOOP):
|
| 204 |
-
print(f"SKIP: {AGENT_LOOP}")
|
| 205 |
-
return
|
| 206 |
-
with open(AGENT_LOOP, "r") as f:
|
| 207 |
-
content = f.read()
|
| 208 |
-
|
| 209 |
-
if "_unfinished_plan" not in content:
|
| 210 |
-
fn_block = '''
|
| 211 |
-
|
| 212 |
-
def _unfinished_plan(s: Session) -> list[dict[str, str]]:
|
| 213 |
-
p = getattr(s, "current_plan", None) or []
|
| 214 |
-
return [it for it in p if it.get("status") in ("pending", "in_progress")]
|
| 215 |
-
|
| 216 |
-
'''
|
| 217 |
-
content = content.replace("class Handlers:", fn_block + "\n\nclass Handlers:")
|
| 218 |
-
|
| 219 |
-
if "task_incomplete" not in content:
|
| 220 |
-
check_block = '''
|
| 221 |
-
# Auto-continue detect
|
| 222 |
-
if not llm_result.tool_calls_acc and llm_result.content:
|
| 223 |
-
unfinished = _unfinished_plan(session)
|
| 224 |
-
if unfinished:
|
| 225 |
-
await session.send_event(Event(event_type="task_incomplete", data={"incomplete_plan": unfinished}))
|
| 226 |
-
'''
|
| 227 |
-
marker = " # -- End of turn --"
|
| 228 |
-
if marker in content:
|
| 229 |
-
content = content.replace(marker, check_block + "\n" + marker)
|
| 230 |
-
|
| 231 |
-
with open(AGENT_LOOP, "w") as f:
|
| 232 |
-
f.write(content)
|
| 233 |
-
print("OK: agent_loop.py")
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
if __name__ == "__main__":
|
| 237 |
-
fix_events()
|
| 238 |
-
fix_sse()
|
| 239 |
-
fix_hook()
|
| 240 |
-
fix_chat_input()
|
| 241 |
-
fix_session_chat()
|
| 242 |
-
fix_agent_loop()
|
| 243 |
-
print("\nDONE: V6 - all fixed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v7.py
DELETED
|
@@ -1,255 +0,0 @@
|
|
| 1 |
-
"""V7: PRECISE patch - exact string matching with source code.
|
| 2 |
-
|
| 3 |
-
Adds auto-continue pause button for free models. All patches use exact
|
| 4 |
-
string content from the original source files for reliable replacement.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import os, sys
|
| 8 |
-
|
| 9 |
-
# ========== PATCH 1: events.ts - add 'task_incomplete' to EventType ==========
|
| 10 |
-
events_ts = "/source/frontend/src/types/events.ts"
|
| 11 |
-
def patch_events():
|
| 12 |
-
with open(events_ts) as f:
|
| 13 |
-
c = f.read()
|
| 14 |
-
if "'task_incomplete'" not in c:
|
| 15 |
-
c = c.replace(
|
| 16 |
-
" | 'interrupted'",
|
| 17 |
-
" | 'interrupted'\n | 'task_incomplete'"
|
| 18 |
-
)
|
| 19 |
-
with open(events_ts, 'w') as f: f.write(c)
|
| 20 |
-
print("OK: events.ts")
|
| 21 |
-
|
| 22 |
-
# ========== PATCH 2: SSE transport - add onTaskIncomplete to interface + handler ==========
|
| 23 |
-
sse_ts = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 24 |
-
def patch_sse():
|
| 25 |
-
with open(sse_ts) as f:
|
| 26 |
-
c = f.read()
|
| 27 |
-
|
| 28 |
-
# Add to interface
|
| 29 |
-
if "onTaskIncomplete:" not in c:
|
| 30 |
-
c = c.replace(
|
| 31 |
-
" onInterrupted: () => void;",
|
| 32 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 33 |
-
)
|
| 34 |
-
|
| 35 |
-
# Add case handler before default
|
| 36 |
-
if "case 'task_incomplete'" not in c:
|
| 37 |
-
case_handler = """ case 'task_incomplete':
|
| 38 |
-
sideChannel.onTaskIncomplete(
|
| 39 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 40 |
-
);
|
| 41 |
-
break;
|
| 42 |
-
|
| 43 |
-
default:"""
|
| 44 |
-
c = c.replace("\n default:", "\n" + case_handler)
|
| 45 |
-
|
| 46 |
-
with open(sse_ts, 'w') as f: f.write(c)
|
| 47 |
-
print("OK: sse-chat-transport.ts")
|
| 48 |
-
|
| 49 |
-
# ========== PATCH 3: useAgentChat.ts - add state + handler + return ==========
|
| 50 |
-
hook_ts = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 51 |
-
def patch_hook():
|
| 52 |
-
with open(hook_ts) as f:
|
| 53 |
-
c = f.read()
|
| 54 |
-
|
| 55 |
-
# 1. Add useState to imports
|
| 56 |
-
if "useState" not in c:
|
| 57 |
-
c = c.replace(
|
| 58 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 59 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 60 |
-
)
|
| 61 |
-
|
| 62 |
-
# 2. Add state variables after callbacksRef
|
| 63 |
-
state_block = """
|
| 64 |
-
// Auto-continue state for free models
|
| 65 |
-
const _acTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
| 66 |
-
const [_showAc, _setShowAc] = useState(false);"""
|
| 67 |
-
if "_acTimerRef" not in c:
|
| 68 |
-
c = c.replace(
|
| 69 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 70 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };" + state_block
|
| 71 |
-
)
|
| 72 |
-
|
| 73 |
-
# 3. Add onTaskIncomplete to interface
|
| 74 |
-
if "onTaskIncomplete:" not in c:
|
| 75 |
-
c = c.replace(
|
| 76 |
-
" onInterrupted: () => void;",
|
| 77 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 78 |
-
)
|
| 79 |
-
|
| 80 |
-
# 4. Add onTaskIncomplete handler in sideChannel object
|
| 81 |
-
# This inserts BEFORE the existing onInterrupted handler
|
| 82 |
-
handler = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
|
| 83 |
-
_setShowAc(true);
|
| 84 |
-
if (_acTimerRef.current) clearTimeout(_acTimerRef.current);
|
| 85 |
-
_acTimerRef.current = setTimeout(() => {
|
| 86 |
-
_setShowAc(false);
|
| 87 |
-
const planStr = incompletePlan.map(i => `- ${i.content}`).join('\\\\n');
|
| 88 |
-
const setMsgs = chatActionsRef.current.setMessages;
|
| 89 |
-
if (setMsgs) {
|
| 90 |
-
const msgs = chatActionsRef.current.messages;
|
| 91 |
-
setMsgs([...msgs, { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: `[TIẾP TỤC] Task chưa hoàn thành:\\\\n${planStr}` }], content: '' }]);
|
| 92 |
-
}
|
| 93 |
-
}, 10000);
|
| 94 |
-
},
|
| 95 |
-
onInterrupted: () => { /* no-op */ },"""
|
| 96 |
-
|
| 97 |
-
if "onTaskIncomplete: (incompletePlan" not in c:
|
| 98 |
-
old_interrupted = ' onInterrupted: () => { /* no-op \u2014 handled by stop() caller */ },'
|
| 99 |
-
if old_interrupted in c:
|
| 100 |
-
c = c.replace(old_interrupted, handler)
|
| 101 |
-
else:
|
| 102 |
-
# Try simpler version
|
| 103 |
-
old2 = ' onInterrupted: () => { /* no-op */ },'
|
| 104 |
-
c = c.replace(old2, handler)
|
| 105 |
-
|
| 106 |
-
# 5. Add _acCancel function + return values
|
| 107 |
-
cancel_func = """
|
| 108 |
-
const _acCancel = useCallback(() => {
|
| 109 |
-
_setShowAc(false);
|
| 110 |
-
if (_acTimerRef.current) { clearTimeout(_acTimerRef.current); _acTimerRef.current = null; }
|
| 111 |
-
}, []);"""
|
| 112 |
-
if "_acCancel" not in c:
|
| 113 |
-
c = c.replace("\n return {\n messages: chat.messages,", cancel_func + "\n return {\n messages: chat.messages,")
|
| 114 |
-
|
| 115 |
-
# 6. Return values
|
| 116 |
-
if "_showAc," not in c:
|
| 117 |
-
c = c.replace(
|
| 118 |
-
"refreshMessages,\n };",
|
| 119 |
-
"refreshMessages,\n _showAc,\n _acCancel,\n };"
|
| 120 |
-
)
|
| 121 |
-
|
| 122 |
-
with open(hook_ts, 'w') as f: f.write(c)
|
| 123 |
-
print("OK: useAgentChat.ts")
|
| 124 |
-
|
| 125 |
-
# ========== PATCH 4: ChatInput.tsx - add pause button ==========
|
| 126 |
-
input_tsx = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 127 |
-
def patch_chat_input():
|
| 128 |
-
with open(input_tsx) as f:
|
| 129 |
-
c = f.read()
|
| 130 |
-
|
| 131 |
-
# 1. Add Button import (it's missing from the original imports)
|
| 132 |
-
if "Button" not in c:
|
| 133 |
-
c = c.replace(
|
| 134 |
-
" Tooltip,",
|
| 135 |
-
" Tooltip,\n Button,"
|
| 136 |
-
)
|
| 137 |
-
|
| 138 |
-
# 2. Add props to interface
|
| 139 |
-
if "_showAc" not in c:
|
| 140 |
-
old_interface_end = """interface ChatInputProps {
|
| 141 |
-
sessionId?: string;
|
| 142 |
-
initialModelPath?: string | null;
|
| 143 |
-
onSend: (text: string) => void;
|
| 144 |
-
onStop?: () => void;
|
| 145 |
-
onDatasetUploaded?: () => Promise<boolean> | boolean;
|
| 146 |
-
isProcessing?: boolean;
|
| 147 |
-
disabled?: boolean;
|
| 148 |
-
placeholder?: string;
|
| 149 |
-
}"""
|
| 150 |
-
new_interface = """interface ChatInputProps {
|
| 151 |
-
sessionId?: string;
|
| 152 |
-
initialModelPath?: string | null;
|
| 153 |
-
onSend: (text: string) => void;
|
| 154 |
-
onStop?: () => void;
|
| 155 |
-
onDatasetUploaded?: () => Promise<boolean> | boolean;
|
| 156 |
-
isProcessing?: boolean;
|
| 157 |
-
disabled?: boolean;
|
| 158 |
-
placeholder?: string;
|
| 159 |
-
_showAc?: boolean;
|
| 160 |
-
_acCancel?: () => void;
|
| 161 |
-
}"""
|
| 162 |
-
c = c.replace(old_interface_end, new_interface)
|
| 163 |
-
|
| 164 |
-
# 3. Add destructured props in function signature
|
| 165 |
-
if "_showAc" not in c:
|
| 166 |
-
old_props = "placeholder = 'Ask anything...' }: ChatInputProps) {"
|
| 167 |
-
new_props = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
|
| 168 |
-
if old_props not in c:
|
| 169 |
-
old_props2 = "placeholder = 'Ask anything...' }: ChatInputProps) {"
|
| 170 |
-
new_props2 = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
|
| 171 |
-
c = c.replace(old_props2, new_props2)
|
| 172 |
-
else:
|
| 173 |
-
c = c.replace(old_props, new_props)
|
| 174 |
-
|
| 175 |
-
# 4. Add pause button after the model badge area, before <JobsUpgradeDialog>
|
| 176 |
-
if "T\u1ea1m d\u1eebng" not in c:
|
| 177 |
-
btn = """
|
| 178 |
-
{/* Auto-continue pause for free models */}
|
| 179 |
-
{_showAc && _acCancel && (
|
| 180 |
-
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
|
| 181 |
-
<Button
|
| 182 |
-
variant="outlined"
|
| 183 |
-
size="small"
|
| 184 |
-
onClick={_acCancel}
|
| 185 |
-
sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}
|
| 186 |
-
>
|
| 187 |
-
T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
|
| 188 |
-
</Button>
|
| 189 |
-
</Box>
|
| 190 |
-
)}
|
| 191 |
-
"""
|
| 192 |
-
if "<JobsUpgradeDialog" in c:
|
| 193 |
-
c = c.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
|
| 194 |
-
|
| 195 |
-
with open(input_tsx, 'w') as f: f.write(c)
|
| 196 |
-
print("OK: ChatInput.tsx")
|
| 197 |
-
|
| 198 |
-
# ========== PATCH 5: SessionChat.tsx - pass props down ==========
|
| 199 |
-
session_tsx = "/source/frontend/src/components/SessionChat.tsx"
|
| 200 |
-
def patch_session():
|
| 201 |
-
if not os.path.exists(session_tsx):
|
| 202 |
-
print(f"SKIP: {session_tsx}")
|
| 203 |
-
return
|
| 204 |
-
with open(session_tsx) as f:
|
| 205 |
-
c = f.read()
|
| 206 |
-
if "_showAc" not in c:
|
| 207 |
-
old = "<ChatInput "
|
| 208 |
-
new = "<ChatInput _showAc={_showAc} _acCancel={_acCancel} "
|
| 209 |
-
c = c.replace(old, new)
|
| 210 |
-
with open(session_tsx, 'w') as f: f.write(c)
|
| 211 |
-
print("OK: SessionChat.tsx")
|
| 212 |
-
|
| 213 |
-
# ========== PATCH 6: agent_loop.py - detection ==========
|
| 214 |
-
agent_py = "/app/agent/core/agent_loop.py"
|
| 215 |
-
def patch_agent():
|
| 216 |
-
if not os.path.exists(agent_py):
|
| 217 |
-
print(f"SKIP: {agent_py}")
|
| 218 |
-
return
|
| 219 |
-
with open(agent_py) as f:
|
| 220 |
-
c = f.read()
|
| 221 |
-
|
| 222 |
-
# Add helper function
|
| 223 |
-
if "_unfinished_plan" not in c:
|
| 224 |
-
fn = """
|
| 225 |
-
def _unfinished_plan(s: Session) -> list[dict[str, str]]:
|
| 226 |
-
p = getattr(s, "current_plan", None) or []
|
| 227 |
-
return [it for it in p if it.get("status") in ("pending", "in_progress")]
|
| 228 |
-
|
| 229 |
-
"""
|
| 230 |
-
c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
|
| 231 |
-
|
| 232 |
-
# Add detection
|
| 233 |
-
if "task_incomplete" not in c:
|
| 234 |
-
check = """
|
| 235 |
-
# Auto-continue detection
|
| 236 |
-
if not llm_result.tool_calls_acc and llm_result.content:
|
| 237 |
-
unfinished = _unfinished_plan(session)
|
| 238 |
-
if unfinished:
|
| 239 |
-
await session.send_event(Event(event_type="task_incomplete", data={"incomplete_plan": unfinished}))
|
| 240 |
-
"""
|
| 241 |
-
marker = " # -- End of turn --"
|
| 242 |
-
if marker in c:
|
| 243 |
-
c = c.replace(marker, check + "\n" + marker)
|
| 244 |
-
|
| 245 |
-
with open(agent_py, 'w') as f: f.write(c)
|
| 246 |
-
print("OK: agent_loop.py")
|
| 247 |
-
|
| 248 |
-
if __name__ == "__main__":
|
| 249 |
-
patch_events()
|
| 250 |
-
patch_sse()
|
| 251 |
-
patch_hook()
|
| 252 |
-
patch_chat_input()
|
| 253 |
-
patch_session()
|
| 254 |
-
patch_agent()
|
| 255 |
-
print("\nDONE: V7 - precise patches applied")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v8.py
DELETED
|
@@ -1,239 +0,0 @@
|
|
| 1 |
-
"""V8: Safe auto-continue via useEffect pattern (no hoisting issues)."""
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
|
| 5 |
-
events_ts = "/source/frontend/src/types/events.ts"
|
| 6 |
-
sse_ts = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 7 |
-
hook_ts = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 8 |
-
input_tsx = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 9 |
-
session_tsx = "/source/frontend/src/components/SessionChat.tsx"
|
| 10 |
-
agent_py = "/app/agent/core/agent_loop.py"
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
def patch_events():
|
| 14 |
-
with open(events_ts) as f:
|
| 15 |
-
c = f.read()
|
| 16 |
-
if "'task_incomplete'" not in c:
|
| 17 |
-
c = c.replace(
|
| 18 |
-
" | 'interrupted'",
|
| 19 |
-
" | 'interrupted'\n | 'task_incomplete'"
|
| 20 |
-
)
|
| 21 |
-
with open(events_ts, 'w') as f: f.write(c)
|
| 22 |
-
print("OK: events.ts")
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def patch_sse():
|
| 26 |
-
with open(sse_ts) as f:
|
| 27 |
-
c = f.read()
|
| 28 |
-
|
| 29 |
-
if "onTaskIncomplete:" not in c:
|
| 30 |
-
c = c.replace(
|
| 31 |
-
" onInterrupted: () => void;",
|
| 32 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 33 |
-
)
|
| 34 |
-
|
| 35 |
-
if "case 'task_incomplete'" not in c:
|
| 36 |
-
case_handler = """ case 'task_incomplete':
|
| 37 |
-
sideChannel.onTaskIncomplete(
|
| 38 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 39 |
-
);
|
| 40 |
-
break;
|
| 41 |
-
|
| 42 |
-
default:"""
|
| 43 |
-
c = c.replace("\n default:", "\n" + case_handler)
|
| 44 |
-
|
| 45 |
-
with open(sse_ts, 'w') as f: f.write(c)
|
| 46 |
-
print("OK: sse.ts")
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def patch_hook():
|
| 50 |
-
with open(hook_ts) as f:
|
| 51 |
-
c = f.read()
|
| 52 |
-
|
| 53 |
-
# 1. Import useState
|
| 54 |
-
if "useState" not in c:
|
| 55 |
-
c = c.replace(
|
| 56 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 57 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 58 |
-
)
|
| 59 |
-
|
| 60 |
-
# 2. State after callbacksRef
|
| 61 |
-
state = """
|
| 62 |
-
// Auto-continue state for free models
|
| 63 |
-
const [_showAc, _setShowAc] = useState(false);
|
| 64 |
-
const _acPlanRef = useRef<Array<{ id: string; content: string; status: string }>>([]);"""
|
| 65 |
-
if "_showAc" not in c:
|
| 66 |
-
c = c.replace(
|
| 67 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 68 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };" + state
|
| 69 |
-
)
|
| 70 |
-
|
| 71 |
-
# 3. onTaskIncomplete in interface
|
| 72 |
-
if "onTaskIncomplete:" not in c:
|
| 73 |
-
c = c.replace(
|
| 74 |
-
" onInterrupted: () => void;",
|
| 75 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 76 |
-
)
|
| 77 |
-
|
| 78 |
-
# 4. Handler in sideChannel (only set state + ref, no chat.sendMessage)
|
| 79 |
-
handler = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {
|
| 80 |
-
_acPlanRef.current = incompletePlan;
|
| 81 |
-
_setShowAc(true);
|
| 82 |
-
},
|
| 83 |
-
onInterrupted: () => { /* no-op */ },"""
|
| 84 |
-
|
| 85 |
-
if "onTaskIncomplete: (incompletePlan" not in c:
|
| 86 |
-
old = ' onInterrupted: () => { /* no-op \u2014 handled by stop() caller */ },'
|
| 87 |
-
if old in c:
|
| 88 |
-
c = c.replace(old, handler)
|
| 89 |
-
else:
|
| 90 |
-
c = c.replace(' onInterrupted: () => { /* no-op */ },', handler)
|
| 91 |
-
|
| 92 |
-
# 5. useEffect to watch _showAc - runs timer, calls chat.sendMessage via chatActionsRef
|
| 93 |
-
effect = """
|
| 94 |
-
// Auto-continue effect: when _showAc becomes true, start 10s timer
|
| 95 |
-
useEffect(() => {
|
| 96 |
-
if (!_showAc) return;
|
| 97 |
-
const timer = setTimeout(() => {
|
| 98 |
-
const plan = _acPlanRef.current;
|
| 99 |
-
const setMsgs = chatActionsRef.current.setMessages;
|
| 100 |
-
const msgs = chatActionsRef.current.messages;
|
| 101 |
-
if (setMsgs && plan.length > 0) {
|
| 102 |
-
const planStr = plan.map(i => `- ${i.content}`).join('\\\\n');
|
| 103 |
-
const newMsg = { id: 'ac-' + Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: `[TIẾP TỤC] Task chưa hoàn thành:\\\\n${planStr}` }], content: '' };
|
| 104 |
-
setMsgs([...msgs, newMsg]);
|
| 105 |
-
}
|
| 106 |
-
_setShowAc(false);
|
| 107 |
-
}, 10000);
|
| 108 |
-
return () => clearTimeout(timer);
|
| 109 |
-
}, [_showAc]);
|
| 110 |
-
"""
|
| 111 |
-
if "Auto-continue effect" not in c:
|
| 112 |
-
# Insert before return
|
| 113 |
-
c = c.replace("\n return {\n messages: chat.messages,", effect + "\n return {\n messages: chat.messages,")
|
| 114 |
-
|
| 115 |
-
# 6. Cancel + return
|
| 116 |
-
cancel = """
|
| 117 |
-
const _acCancel = useCallback(() => { _setShowAc(false); }, []);"""
|
| 118 |
-
if "_acCancel" not in c:
|
| 119 |
-
c = c.replace("\n return {\n messages: chat.messages,", cancel + "\n return {\n messages: chat.messages,")
|
| 120 |
-
if "_showAc," not in c:
|
| 121 |
-
c = c.replace(
|
| 122 |
-
"refreshMessages,\n };",
|
| 123 |
-
"refreshMessages,\n _showAc,\n _acCancel,\n };"
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
with open(hook_ts, 'w') as f: f.write(c)
|
| 127 |
-
print("OK: useAgentChat.ts")
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
def patch_chat_input():
|
| 131 |
-
with open(input_tsx) as f:
|
| 132 |
-
c = f.read()
|
| 133 |
-
|
| 134 |
-
if "Button" not in c:
|
| 135 |
-
c = c.replace(" Tooltip,", " Tooltip,\n Button,")
|
| 136 |
-
|
| 137 |
-
if "_showAc" not in c:
|
| 138 |
-
old_iface = """interface ChatInputProps {
|
| 139 |
-
sessionId?: string;
|
| 140 |
-
initialModelPath?: string | null;
|
| 141 |
-
onSend: (text: string) => void;
|
| 142 |
-
onStop?: () => void;
|
| 143 |
-
onDatasetUploaded?: () => Promise<boolean> | boolean;
|
| 144 |
-
isProcessing?: boolean;
|
| 145 |
-
disabled?: boolean;
|
| 146 |
-
placeholder?: string;
|
| 147 |
-
}"""
|
| 148 |
-
new_iface = """interface ChatInputProps {
|
| 149 |
-
sessionId?: string;
|
| 150 |
-
initialModelPath?: string | null;
|
| 151 |
-
onSend: (text: string) => void;
|
| 152 |
-
onStop?: () => void;
|
| 153 |
-
onDatasetUploaded?: () => Promise<boolean> | boolean;
|
| 154 |
-
isProcessing?: boolean;
|
| 155 |
-
disabled?: boolean;
|
| 156 |
-
placeholder?: string;
|
| 157 |
-
_showAc?: boolean;
|
| 158 |
-
_acCancel?: () => void;
|
| 159 |
-
}"""
|
| 160 |
-
c = c.replace(old_iface, new_iface)
|
| 161 |
-
|
| 162 |
-
old_props = "placeholder = 'Ask anything...' }: ChatInputProps) {"
|
| 163 |
-
new_props = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
|
| 164 |
-
if old_props not in c:
|
| 165 |
-
old_props2 = "placeholder = 'Ask anything...' }: ChatInputProps) {"
|
| 166 |
-
new_props2 = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
|
| 167 |
-
c = c.replace(old_props2, new_props2)
|
| 168 |
-
else:
|
| 169 |
-
c = c.replace(old_props, new_props)
|
| 170 |
-
|
| 171 |
-
if "T\u1ea1m d\u1eebng" not in c:
|
| 172 |
-
btn = """
|
| 173 |
-
{_showAc && _acCancel && (
|
| 174 |
-
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
|
| 175 |
-
<Button variant="outlined" size="small" onClick={_acCancel}
|
| 176 |
-
sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
|
| 177 |
-
T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
|
| 178 |
-
</Button>
|
| 179 |
-
</Box>
|
| 180 |
-
)}
|
| 181 |
-
"""
|
| 182 |
-
if "<JobsUpgradeDialog" in c:
|
| 183 |
-
c = c.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
|
| 184 |
-
|
| 185 |
-
with open(input_tsx, 'w') as f: f.write(c)
|
| 186 |
-
print("OK: ChatInput.tsx")
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
def patch_session():
|
| 190 |
-
if not os.path.exists(session_tsx):
|
| 191 |
-
print(f"SKIP: {session_tsx}")
|
| 192 |
-
return
|
| 193 |
-
with open(session_tsx) as f:
|
| 194 |
-
c = f.read()
|
| 195 |
-
if "_showAc" not in c:
|
| 196 |
-
c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
|
| 197 |
-
with open(session_tsx, 'w') as f: f.write(c)
|
| 198 |
-
print("OK: SessionChat.tsx")
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
def patch_agent():
|
| 202 |
-
if not os.path.exists(agent_py):
|
| 203 |
-
print(f"SKIP: {agent_py}")
|
| 204 |
-
return
|
| 205 |
-
with open(agent_py) as f:
|
| 206 |
-
c = f.read()
|
| 207 |
-
|
| 208 |
-
if "_unfinished_plan" not in c:
|
| 209 |
-
fn = """
|
| 210 |
-
def _unfinished_plan(s: Session) -> list[dict[str, str]]:
|
| 211 |
-
p = getattr(s, "current_plan", None) or []
|
| 212 |
-
return [it for it in p if it.get("status") in ("pending", "in_progress")]
|
| 213 |
-
"""
|
| 214 |
-
c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
|
| 215 |
-
|
| 216 |
-
if "task_incomplete" not in c:
|
| 217 |
-
check = """
|
| 218 |
-
# Auto-continue detection
|
| 219 |
-
if not llm_result.tool_calls_acc and llm_result.content:
|
| 220 |
-
unfinished = _unfinished_plan(session)
|
| 221 |
-
if unfinished:
|
| 222 |
-
await session.send_event(Event(event_type="task_incomplete", data={"incomplete_plan": unfinished}))
|
| 223 |
-
"""
|
| 224 |
-
marker = " # -- End of turn --"
|
| 225 |
-
if marker in c:
|
| 226 |
-
c = c.replace(marker, check + "\n" + marker)
|
| 227 |
-
|
| 228 |
-
with open(agent_py, 'w') as f: f.write(c)
|
| 229 |
-
print("OK: agent_loop.py")
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
if __name__ == "__main__":
|
| 233 |
-
patch_events()
|
| 234 |
-
patch_sse()
|
| 235 |
-
patch_hook()
|
| 236 |
-
patch_chat_input()
|
| 237 |
-
patch_session()
|
| 238 |
-
patch_agent()
|
| 239 |
-
print("\nDONE: V8 - safe useEffect pattern")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_auto_continue_v9.py
DELETED
|
@@ -1,227 +0,0 @@
|
|
| 1 |
-
"""V9: SIMPLEST approach - no interface changes, no props chain.
|
| 2 |
-
|
| 3 |
-
Uses CustomEvent to communicate between sideChannel and React state.
|
| 4 |
-
This avoids ALL TypeScript issues:
|
| 5 |
-
- No interface changes needed
|
| 6 |
-
- No hoisting issues
|
| 7 |
-
- No unused variable warnings
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
import os
|
| 11 |
-
|
| 12 |
-
events_ts = "/source/frontend/src/types/events.ts"
|
| 13 |
-
sse_ts = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 14 |
-
hook_ts = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 15 |
-
input_tsx = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 16 |
-
agent_py = "/app/agent/core/agent_loop.py"
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def patch_events():
|
| 20 |
-
with open(events_ts) as f:
|
| 21 |
-
c = f.read()
|
| 22 |
-
if "'task_incomplete'" not in c:
|
| 23 |
-
c = c.replace(" | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'")
|
| 24 |
-
with open(events_ts, 'w') as f: f.write(c)
|
| 25 |
-
print("OK: events.ts")
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def patch_sse():
|
| 29 |
-
with open(sse_ts) as f:
|
| 30 |
-
c = f.read()
|
| 31 |
-
|
| 32 |
-
# Add case handler DIRECTLY without changing interface
|
| 33 |
-
# Use as any cast to bypass type checking
|
| 34 |
-
if "case 'task_incomplete'" not in c:
|
| 35 |
-
# Add handler before default, using (sideChannel as any) to avoid interface issue
|
| 36 |
-
case_block = """ case 'task_incomplete':
|
| 37 |
-
(sideChannel as any).onTaskIncomplete(
|
| 38 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 39 |
-
);
|
| 40 |
-
break;
|
| 41 |
-
|
| 42 |
-
default:"""
|
| 43 |
-
c = c.replace("\n default:", "\n" + case_block)
|
| 44 |
-
|
| 45 |
-
with open(sse_ts, 'w') as f: f.write(c)
|
| 46 |
-
print("OK: sse.ts")
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def patch_hook():
|
| 50 |
-
with open(hook_ts) as f:
|
| 51 |
-
c = f.read()
|
| 52 |
-
|
| 53 |
-
# 1. Import useState
|
| 54 |
-
if "useState" not in c:
|
| 55 |
-
c = c.replace(
|
| 56 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 57 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 58 |
-
)
|
| 59 |
-
|
| 60 |
-
# 2. Add state after callbacksRef - TS will complain "unused" but _ prefix helps
|
| 61 |
-
state_block = """
|
| 62 |
-
// Auto-continue for free models
|
| 63 |
-
const [_showAc, _setShowAc] = useState(false);"""
|
| 64 |
-
if "_showAc" not in c:
|
| 65 |
-
c = c.replace(
|
| 66 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 67 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };" + state_block
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
# 3. Add onTaskIncomplete to the sideChannel object via (sideChannel as any)
|
| 71 |
-
# We use the onSessionUpdate handler as injection point since it already exists
|
| 72 |
-
handler_block = """ onSessionUpdate: (data) => {
|
| 73 |
-
// Check for auto-continue trigger
|
| 74 |
-
if ((data as any).ac_plan) {
|
| 75 |
-
const plan = (data as any).ac_plan as Array<{ id: string; content: string; status: string }>;
|
| 76 |
-
_setShowAc(true);
|
| 77 |
-
setTimeout(() => {
|
| 78 |
-
_setShowAc(false);
|
| 79 |
-
if (plan.length > 0) {
|
| 80 |
-
const planStr = plan.map(i => `- ${i.content}`).join('\\\\n');
|
| 81 |
-
chat.sendMessage({ text: `[TIẾP TỤC] Task chưa hoàn thành:\\\\n${planStr}`, metadata: { createdAt: new Date().toISOString() } });
|
| 82 |
-
}
|
| 83 |
-
}, 10000);
|
| 84 |
-
return;
|
| 85 |
-
}
|
| 86 |
-
const autoApproval = data.auto_approval;"""
|
| 87 |
-
|
| 88 |
-
if "_setShowAc" in c and "ac_plan" not in c:
|
| 89 |
-
# Find existing onSessionUpdate
|
| 90 |
-
old = """ onSessionUpdate: (data) => {
|
| 91 |
-
const autoApproval = data.auto_approval;"""
|
| 92 |
-
if old in c:
|
| 93 |
-
c = c.replace(old, handler_block)
|
| 94 |
-
print("OK: injected onSessionUpdate handler")
|
| 95 |
-
|
| 96 |
-
# 4. Cancel function + return values
|
| 97 |
-
cancel = """
|
| 98 |
-
const _acCancel = useCallback(() => { _setShowAc(false); }, []);"""
|
| 99 |
-
if "_acCancel" not in c and "_showAc" in c:
|
| 100 |
-
c = c.replace("\n return {\n messages: chat.messages,", cancel + "\n return {\n messages: chat.messages,")
|
| 101 |
-
c = c.replace(
|
| 102 |
-
"refreshMessages,\n };",
|
| 103 |
-
"refreshMessages,\n _showAc,\n _acCancel,\n };"
|
| 104 |
-
)
|
| 105 |
-
|
| 106 |
-
with open(hook_ts, 'w') as f: f.write(c)
|
| 107 |
-
print("OK: useAgentChat.ts")
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
def patch_chat_input():
|
| 111 |
-
with open(input_tsx) as f:
|
| 112 |
-
c = f.read()
|
| 113 |
-
|
| 114 |
-
if "Button" not in c:
|
| 115 |
-
c = c.replace(" Tooltip,", " Tooltip,\n Button,")
|
| 116 |
-
|
| 117 |
-
if "_showAc" not in c:
|
| 118 |
-
old_iface = """interface ChatInputProps {
|
| 119 |
-
sessionId?: string;
|
| 120 |
-
initialModelPath?: string | null;
|
| 121 |
-
onSend: (text: string) => void;
|
| 122 |
-
onStop?: () => void;
|
| 123 |
-
onDatasetUploaded?: () => Promise<boolean> | boolean;
|
| 124 |
-
isProcessing?: boolean;
|
| 125 |
-
disabled?: boolean;
|
| 126 |
-
placeholder?: string;
|
| 127 |
-
}"""
|
| 128 |
-
new_iface = """interface ChatInputProps {
|
| 129 |
-
sessionId?: string;
|
| 130 |
-
initialModelPath?: string | null;
|
| 131 |
-
onSend: (text: string) => void;
|
| 132 |
-
onStop?: () => void;
|
| 133 |
-
onDatasetUploaded?: () => Promise<boolean> | boolean;
|
| 134 |
-
isProcessing?: boolean;
|
| 135 |
-
disabled?: boolean;
|
| 136 |
-
placeholder?: string;
|
| 137 |
-
_showAc?: boolean;
|
| 138 |
-
_acCancel?: () => void;
|
| 139 |
-
}"""
|
| 140 |
-
c = c.replace(old_iface, new_iface)
|
| 141 |
-
|
| 142 |
-
# Add destructured props
|
| 143 |
-
old_props = "placeholder = 'Ask anything...' }: ChatInputProps) {"
|
| 144 |
-
new_props = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
|
| 145 |
-
c = c.replace(old_props, new_props)
|
| 146 |
-
|
| 147 |
-
if "T\u1ea1m d\u1eebng" not in c:
|
| 148 |
-
btn = """
|
| 149 |
-
{_showAc && _acCancel && (
|
| 150 |
-
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
|
| 151 |
-
<Button variant="outlined" size="small" onClick={_acCancel}
|
| 152 |
-
sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}>
|
| 153 |
-
T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
|
| 154 |
-
</Button>
|
| 155 |
-
</Box>
|
| 156 |
-
)}
|
| 157 |
-
"""
|
| 158 |
-
if "<JobsUpgradeDialog" in c:
|
| 159 |
-
c = c.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
|
| 160 |
-
|
| 161 |
-
with open(input_tsx, 'w') as f: f.write(c)
|
| 162 |
-
print("OK: ChatInput.tsx")
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
def patch_session():
|
| 166 |
-
session_tsx = "/source/frontend/src/components/SessionChat.tsx"
|
| 167 |
-
if not os.path.exists(session_tsx):
|
| 168 |
-
print(f"SKIP: {session_tsx}")
|
| 169 |
-
return
|
| 170 |
-
with open(session_tsx) as f:
|
| 171 |
-
c = f.read()
|
| 172 |
-
if "_showAc" not in c:
|
| 173 |
-
c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
|
| 174 |
-
with open(session_tsx, 'w') as f: f.write(c)
|
| 175 |
-
print("OK: SessionChat.tsx")
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
def patch_agent():
|
| 179 |
-
if not os.path.exists(agent_py):
|
| 180 |
-
print(f"SKIP: {agent_py}")
|
| 181 |
-
return
|
| 182 |
-
with open(agent_py) as f:
|
| 183 |
-
c = f.read()
|
| 184 |
-
|
| 185 |
-
if "_unfinished_plan" not in c:
|
| 186 |
-
fn = """
|
| 187 |
-
def _unfinished_plan(s: Session) -> list[dict[str, str]]:
|
| 188 |
-
p = getattr(s, "current_plan", None) or []
|
| 189 |
-
return [it for it in p if it.get("status") in ("pending", "in_progress")]
|
| 190 |
-
"""
|
| 191 |
-
c = c.replace("class Handlers:", fn + "\n\nclass Handlers:")
|
| 192 |
-
|
| 193 |
-
if "task_incomplete" not in c:
|
| 194 |
-
check = """
|
| 195 |
-
# Auto-continue detection
|
| 196 |
-
if not llm_result.tool_calls_acc and llm_result.content:
|
| 197 |
-
unfinished = _unfinished_plan(session)
|
| 198 |
-
if unfinished:
|
| 199 |
-
# Use session_update event with ac_plan field to trigger auto-continue
|
| 200 |
-
await session.send_event(Event(event_type="session_update", data={
|
| 201 |
-
"ac_plan": unfinished,
|
| 202 |
-
"auto_approval": getattr(session, "auto_approval", None),
|
| 203 |
-
}))
|
| 204 |
-
"""
|
| 205 |
-
marker = " # -- End of turn --"
|
| 206 |
-
if marker in c:
|
| 207 |
-
c = c.replace(marker, check + "\n" + marker)
|
| 208 |
-
else:
|
| 209 |
-
print("WARN: Could not find -- End of turn -- marker")
|
| 210 |
-
# Try alternative: find where final_response is set
|
| 211 |
-
marker2 = "final_response = llm_result.content or None"
|
| 212 |
-
if marker2 in c:
|
| 213 |
-
c = c.replace(marker2, marker2 + "\n" + check)
|
| 214 |
-
|
| 215 |
-
with open(agent_py, 'w') as f: f.write(c)
|
| 216 |
-
print("OK: agent_loop.py")
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
if __name__ == "__main__":
|
| 220 |
-
patch_events()
|
| 221 |
-
patch_sse()
|
| 222 |
-
patch_hook()
|
| 223 |
-
patch_chat_input()
|
| 224 |
-
patch_session()
|
| 225 |
-
patch_agent()
|
| 226 |
-
print("\nDONE: V9 applied")
|
| 227 |
-
print("Strategy: Uses existing session_update event + (sideChannel as any) to avoid TS issues")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_beatapi_routing.py
DELETED
|
@@ -1,182 +0,0 @@
|
|
| 1 |
-
"""Patch llm_params.py: add a DEDICATED BeatAPI provider branch.
|
| 2 |
-
|
| 3 |
-
BEATAPI vs OpenRouter -- why this branch is separate and must stay separate
|
| 4 |
-
=========================================================================
|
| 5 |
-
Every other model in this app except the HF Router / local-ollama ones is an
|
| 6 |
-
*OpenRouter* model. They are all tagged with an `openai/` prefix, and
|
| 7 |
-
_resolve_llm_params has a single catch-all that:
|
| 8 |
-
|
| 9 |
-
1. strips the `openai/` prefix,
|
| 10 |
-
2. points api_base at https://openrouter.ai/api/v1,
|
| 11 |
-
3. authenticates with OPENROUTER_API_KEY.
|
| 12 |
-
|
| 13 |
-
BeatAPI (https://api.beatapi.io) is a *different provider* that happens to
|
| 14 |
-
expose an OpenAI-compatible surface. It must therefore NOT reuse that branch:
|
| 15 |
-
|
| 16 |
-
provider OpenRouter BeatAPI
|
| 17 |
-
----------- ----------------------------- ------------------------------
|
| 18 |
-
api_base https://openrouter.ai/api/v1 https://api.beatapi.io/v1
|
| 19 |
-
key env OPENROUTER_API_KEY BEATAPI_API_KEY
|
| 20 |
-
model id vendor/model space-bunny-alpha (bare, no vendor prefix)
|
| 21 |
-
effort unsupported (omitted) low/medium/high/xhigh/max via extra_body
|
| 22 |
-
|
| 23 |
-
BeatAPI additionally:
|
| 24 |
-
- does not understand the `openai/` prefix at all, so sending
|
| 25 |
-
"openai/space-bunny-alpha" or "openai/stealth/space-bunny-alpha" 404s;
|
| 26 |
-
- uses `space-bunny-alpha` as a *flat* model id, so we must strip our own
|
| 27 |
-
`beatapi/` routing prefix before the request leaves the app;
|
| 28 |
-
- rate-limits the free tier (1 req/min before first top-up, 10/min after,
|
| 29 |
-
1000/day) and returns HTTP 429 with Retry-After.
|
| 30 |
-
|
| 31 |
-
Therefore BeatAPI models use the `beatapi/` prefix -- a namespace nothing else
|
| 32 |
-
uses -- so this branch can never be shadowed by the `openai/` catch-all, and
|
| 33 |
-
the OpenRouter branch can never accidentally strip a BeatAPI model id.
|
| 34 |
-
|
| 35 |
-
If BEATAPI_API_KEY is unset we raise a clear, actionable error instead of
|
| 36 |
-
silently sending an empty key (which BeatAPI answers with a bare 401
|
| 37 |
-
missing_api_key).
|
| 38 |
-
|
| 39 |
-
Runs AFTER patch_models.py and patch_openrouter_fix.py. Idempotent.
|
| 40 |
-
"""
|
| 41 |
-
|
| 42 |
-
import ast
|
| 43 |
-
|
| 44 |
-
FILE = "/app/agent/core/llm_params.py"
|
| 45 |
-
|
| 46 |
-
BEATAPI_BASE_URL = "https://api.beatapi.io/v1"
|
| 47 |
-
BEATAPI_MODEL_PREFIX = "beatapi/"
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
with open(FILE) as f:
|
| 51 |
-
c = f.read()
|
| 52 |
-
|
| 53 |
-
if "BeatApiKeyMissingError" in c:
|
| 54 |
-
print("OK: llm_params.py already contains BeatAPI routing (idempotent)")
|
| 55 |
-
raise SystemExit(0)
|
| 56 |
-
|
| 57 |
-
if "\nimport os\n" not in c:
|
| 58 |
-
print("FAIL: expected `import os` in llm_params.py")
|
| 59 |
-
raise SystemExit(1)
|
| 60 |
-
|
| 61 |
-
# --- Insert the BeatAPI block before _resolve_llm_params ---
|
| 62 |
-
anchor = "def _resolve_llm_params(\n"
|
| 63 |
-
if anchor not in c:
|
| 64 |
-
print("FAIL: could not find _resolve_llm_params definition")
|
| 65 |
-
raise SystemExit(1)
|
| 66 |
-
|
| 67 |
-
beatapi_block = f'''BEATAPI_BASE_URL = "{BEATAPI_BASE_URL}"
|
| 68 |
-
BEATAPI_MODEL_PREFIX = "{BEATAPI_MODEL_PREFIX}"
|
| 69 |
-
# BeatAPI accepts low/medium/high/xhigh/max (HF Router only low/medium/high).
|
| 70 |
-
_BEATAPI_EFFORTS = {{"low", "medium", "high", "xhigh", "max"}}
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
class BeatApiKeyMissingError(ValueError):
|
| 74 |
-
"""BEATAPI_API_KEY is not configured, so BeatAPI cannot be called.
|
| 75 |
-
|
| 76 |
-
Raised before any network call so the UI shows an actionable message
|
| 77 |
-
("set BEATAPI_API_KEY") instead of an opaque 401 missing_api_key.
|
| 78 |
-
"""
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def _beatapi_base_model(model_name: str) -> str:
|
| 82 |
-
"""Return the wire model id for a BeatAPI request.
|
| 83 |
-
|
| 84 |
-
``beatapi/space-bunny-alpha`` -> ``space-bunny-alpha``
|
| 85 |
-
BeatAPI model ids are flat; they must never carry our routing prefix.
|
| 86 |
-
"""
|
| 87 |
-
return model_name[len(BEATAPI_MODEL_PREFIX):].strip()
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def _resolve_beatapi_params(
|
| 91 |
-
model_name: str,
|
| 92 |
-
reasoning_effort: str | None = None,
|
| 93 |
-
strict: bool = False,
|
| 94 |
-
) -> dict:
|
| 95 |
-
"""Build LiteLLM kwargs for a BeatAPI (``beatapi/``-prefixed) model.
|
| 96 |
-
|
| 97 |
-
Mirrors the OpenRouter branch but targets api.beatapi.io, uses
|
| 98 |
-
BEATAPI_API_KEY, forces LiteLLM's OpenAI-compatible client, and forwards
|
| 99 |
-
BeatAPI's own reasoning_effort levels (it accepts xhigh and max, which the
|
| 100 |
-
HF Router does not).
|
| 101 |
-
"""
|
| 102 |
-
base_model = _beatapi_base_model(model_name)
|
| 103 |
-
if not base_model:
|
| 104 |
-
raise ValueError(f"Missing BeatAPI model id: {{model_name!r}}")
|
| 105 |
-
|
| 106 |
-
api_key = os.environ.get("BEATAPI_API_KEY", "").strip()
|
| 107 |
-
if not api_key:
|
| 108 |
-
raise BeatApiKeyMissingError(
|
| 109 |
-
"BEATAPI_API_KEY is not set. BeatAPI models (beatapi/...) require a "
|
| 110 |
-
"BeatAPI key from https://beatapi.io (Dashboard -> API Keys); "
|
| 111 |
-
"set it as a Space secret named BEATAPI_API_KEY."
|
| 112 |
-
)
|
| 113 |
-
|
| 114 |
-
params: dict = {{
|
| 115 |
-
"model": f"openai/{{base_model}}",
|
| 116 |
-
"api_base": BEATAPI_BASE_URL,
|
| 117 |
-
"api_key": api_key,
|
| 118 |
-
# Force LiteLLM's OpenAI-compatible client. Without this, LiteLLM would
|
| 119 |
-
# try to resolve a provider from the model string and raise
|
| 120 |
-
# "LLM Provider NOT provided" (same class of bug as the OpenRouter fix).
|
| 121 |
-
"custom_llm_provider": "openai",
|
| 122 |
-
}}
|
| 123 |
-
|
| 124 |
-
if reasoning_effort:
|
| 125 |
-
level = reasoning_effort
|
| 126 |
-
if level not in _BEATAPI_EFFORTS:
|
| 127 |
-
if strict:
|
| 128 |
-
raise UnsupportedEffortError(
|
| 129 |
-
f"BeatAPI doesn't accept effort={{level!r}}"
|
| 130 |
-
)
|
| 131 |
-
else:
|
| 132 |
-
# BeatAPI reads reasoning_effort from the request body directly.
|
| 133 |
-
params["extra_body"] = {{"reasoning_effort": level}}
|
| 134 |
-
return params
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
'''
|
| 138 |
-
|
| 139 |
-
c = c.replace(anchor, beatapi_block + anchor, 1)
|
| 140 |
-
|
| 141 |
-
# --- Add the dispatch inside _resolve_llm_params, BEFORE the openai/ catch-all ---
|
| 142 |
-
# The dispatch must sit after the local-model checks and before the OpenRouter
|
| 143 |
-
# branch so `beatapi/` can never be swallowed by `openai/`.
|
| 144 |
-
dispatch_anchor = " hf_model = normalized_model\n"
|
| 145 |
-
if dispatch_anchor not in c:
|
| 146 |
-
print("FAIL: could not find hf_model assignment in _resolve_llm_params")
|
| 147 |
-
raise SystemExit(1)
|
| 148 |
-
|
| 149 |
-
dispatch = """ # --- BeatAPI provider (beatapi/ prefix) -------------------------------
|
| 150 |
-
# NOTE: distinct from the OpenRouter branch below on api_base, API key,
|
| 151 |
-
# model-id shape and reasoning_effort levels. Keep this check above the
|
| 152 |
-
# openai/ catch-all so the two providers can never collide.
|
| 153 |
-
if normalized_model.startswith(BEATAPI_MODEL_PREFIX):
|
| 154 |
-
return _resolve_beatapi_params(
|
| 155 |
-
normalized_model, reasoning_effort, strict
|
| 156 |
-
)
|
| 157 |
-
|
| 158 |
-
"""
|
| 159 |
-
|
| 160 |
-
c = c.replace(dispatch_anchor, dispatch + dispatch_anchor, 1)
|
| 161 |
-
|
| 162 |
-
with open(FILE, "w") as f:
|
| 163 |
-
f.write(c)
|
| 164 |
-
|
| 165 |
-
try:
|
| 166 |
-
ast.parse(c)
|
| 167 |
-
print("OK: llm_params.py patched & syntax valid")
|
| 168 |
-
except SyntaxError as e:
|
| 169 |
-
print(f"FAIL: llm_params.py syntax error: {e}")
|
| 170 |
-
raise
|
| 171 |
-
|
| 172 |
-
# Sanity: the BeatAPI branch must appear before the openai/ catch-all.
|
| 173 |
-
beat_i = c.find("startswith(BEATAPI_MODEL_PREFIX)")
|
| 174 |
-
openai_i = c.find('startswith("openai/")')
|
| 175 |
-
if beat_i != -1 and openai_i != -1:
|
| 176 |
-
if beat_i < openai_i:
|
| 177 |
-
print("OK: BeatAPI dispatch precedes the openai/ catch-all")
|
| 178 |
-
else:
|
| 179 |
-
print("FAIL: BeatAPI dispatch is AFTER the openai/ catch-all")
|
| 180 |
-
raise SystemExit(1)
|
| 181 |
-
|
| 182 |
-
print("DONE: BeatAPI routing branch added to llm_params.py")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_catch_up_context_restore.py
DELETED
|
@@ -1,188 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Patch: Restore full session context from the permanent HF dataset.
|
| 4 |
-
|
| 5 |
-
Fixes two related bugs in bep40/ml-intern (ML Intern V2 Space):
|
| 6 |
-
|
| 7 |
-
1. "Catch me up" banner appears constantly after a Space restart.
|
| 8 |
-
The backend only loads sessions from get_session_store() (Mongo/Noop).
|
| 9 |
-
On HF Spaces there is no MONGODB_URI, so NoopSessionStore.load_session()
|
| 10 |
-
always returns None -> every previously-open session is treated as dead
|
| 11 |
-
-> the frontend shows "Where were we? / Catch me up" for ALL old
|
| 12 |
-
sessions, even though the full conversation is safely stored in the
|
| 13 |
-
bep40/ml-intern-sessions dataset.
|
| 14 |
-
|
| 15 |
-
2. After clicking "Catch me up", the model forgets prior context.
|
| 16 |
-
seed_from_summary() creates a brand-new session and seeds it with only a
|
| 17 |
-
lossy LLM summary (or raw messages that then get pruned). The model
|
| 18 |
-
never sees the full prior conversation, so it genuinely does not
|
| 19 |
-
remember.
|
| 20 |
-
|
| 21 |
-
Fix (both in backend/session_manager.py):
|
| 22 |
-
- ensure_session_loaded(): when the persistence store has no session,
|
| 23 |
-
fall back to the permanent HF dataset (backend.session_logger.
|
| 24 |
-
load_session_history) and re-materialize the session with the FULL
|
| 25 |
-
conversation, so old sessions come back alive with complete context.
|
| 26 |
-
- seed_from_summary(): if the dataset has the full history for the old
|
| 27 |
-
session, seed the new session with the full conversation instead of a
|
| 28 |
-
lossy summary (falling back to the summary path when unavailable).
|
| 29 |
-
|
| 30 |
-
Runs during Docker build; never fails the build.
|
| 31 |
-
"""
|
| 32 |
-
|
| 33 |
-
import ast
|
| 34 |
-
import os
|
| 35 |
-
|
| 36 |
-
PATCHED_MARKER = "# === PATCH: dataset-context-restore ==="
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def _read(path: str) -> str:
|
| 40 |
-
with open(path, encoding="utf-8") as f:
|
| 41 |
-
return f.read()
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def _write(path: str, content: str) -> None:
|
| 45 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 46 |
-
f.write(content)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def _replace(content: str, old: str, new: str) -> tuple[str, bool]:
|
| 50 |
-
if old in content:
|
| 51 |
-
return content.replace(old, new, 1), True
|
| 52 |
-
return content, False
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
SESSION_MANAGER = "/app/backend/session_manager.py"
|
| 56 |
-
|
| 57 |
-
if os.path.exists(SESSION_MANAGER):
|
| 58 |
-
content = _read(SESSION_MANAGER)
|
| 59 |
-
changed = False
|
| 60 |
-
|
| 61 |
-
if PATCHED_MARKER in content:
|
| 62 |
-
print("SKIP: session_manager.py already patched")
|
| 63 |
-
else:
|
| 64 |
-
# ---- 1) Dataset fallback in ensure_session_loaded -----------------
|
| 65 |
-
old_load = (
|
| 66 |
-
" store = self._store()\n"
|
| 67 |
-
" loaded = await store.load_session(session_id)\n"
|
| 68 |
-
" if not loaded:\n"
|
| 69 |
-
" return None\n"
|
| 70 |
-
)
|
| 71 |
-
new_load = (
|
| 72 |
-
" store = self._store()\n"
|
| 73 |
-
" loaded = await store.load_session(session_id)\n"
|
| 74 |
-
" if not loaded:\n"
|
| 75 |
-
" # === PATCH: dataset-context-restore ===\n"
|
| 76 |
-
" # The persistence store has no record for this session\n"
|
| 77 |
-
" # (typical on HF Spaces: no MONGODB_URI -> NoopSessionStore).\n"
|
| 78 |
-
" # Before declaring the session dead, try the permanent HF\n"
|
| 79 |
-
" # dataset so old sessions survive a Space restart with\n"
|
| 80 |
-
" # their FULL context intact.\n"
|
| 81 |
-
" try:\n"
|
| 82 |
-
" from backend.session_logger import load_session_history\n"
|
| 83 |
-
" ds_rows = load_session_history(session_id)\n"
|
| 84 |
-
" ds_messages = [\n"
|
| 85 |
-
" r for r in ds_rows\n"
|
| 86 |
-
" if r.get(\"role\") in (\"user\", \"assistant\", \"tool\")\n"
|
| 87 |
-
" ]\n"
|
| 88 |
-
" if ds_messages:\n"
|
| 89 |
-
" loaded = {\n"
|
| 90 |
-
" \"metadata\": {\n"
|
| 91 |
-
" \"user_id\": user_id,\n"
|
| 92 |
-
" \"model\": next(\n"
|
| 93 |
-
" (r.get(\"model_name\") for r in ds_rows if r.get(\"model_name\")),\n"
|
| 94 |
-
" self.config.model_name,\n"
|
| 95 |
-
" ),\n"
|
| 96 |
-
" },\n"
|
| 97 |
-
" \"messages\": ds_messages,\n"
|
| 98 |
-
" }\n"
|
| 99 |
-
" logger.info(\n"
|
| 100 |
-
" \"Restored session %s from HF dataset (%d messages)\",\n"
|
| 101 |
-
" session_id, len(ds_messages),\n"
|
| 102 |
-
" )\n"
|
| 103 |
-
" except Exception as e:\n"
|
| 104 |
-
" logger.debug(\"Dataset restore fallback failed for %s: %s\", session_id, e)\n"
|
| 105 |
-
" if not loaded:\n"
|
| 106 |
-
" return None\n"
|
| 107 |
-
)
|
| 108 |
-
content, ok = _replace(content, old_load, new_load)
|
| 109 |
-
if ok:
|
| 110 |
-
print("+ ensure_session_loaded falls back to HF dataset")
|
| 111 |
-
changed = True
|
| 112 |
-
else:
|
| 113 |
-
print("INFO: store.load fallback anchor not found")
|
| 114 |
-
|
| 115 |
-
# ---- 2) seed_from_summary: prefer full dataset history ------------
|
| 116 |
-
old_seed = (
|
| 117 |
-
" seed = Message(\n"
|
| 118 |
-
" role=\"user\",\n"
|
| 119 |
-
" content=(\n"
|
| 120 |
-
" \"[SYSTEM: Your prior memory of this conversation \u2014 written \"\n"
|
| 121 |
-
" \"in your own voice right before restart. Continue from here.]\\n\\n\"\n"
|
| 122 |
-
" + (summary or \"(no summary returned)\")\n"
|
| 123 |
-
" ),\n"
|
| 124 |
-
" )\n"
|
| 125 |
-
)
|
| 126 |
-
new_seed = (
|
| 127 |
-
" # === PATCH: dataset-context-restore ===\n"
|
| 128 |
-
" # If the permanent dataset has the full history for this\n"
|
| 129 |
-
" # session, seed the new session with the ACTUAL conversation\n"
|
| 130 |
-
" # instead of a lossy summary so the model really remembers.\n"
|
| 131 |
-
" seeded_from_dataset = 0\n"
|
| 132 |
-
" try:\n"
|
| 133 |
-
" from backend.session_logger import load_session_history\n"
|
| 134 |
-
" ds_rows = load_session_history(session_id)\n"
|
| 135 |
-
" full_msgs = [\n"
|
| 136 |
-
" r for r in ds_rows\n"
|
| 137 |
-
" if r.get(\"role\") in (\"user\", \"assistant\", \"tool\")\n"
|
| 138 |
-
" ]\n"
|
| 139 |
-
" if full_msgs:\n"
|
| 140 |
-
" for raw in full_msgs:\n"
|
| 141 |
-
" try:\n"
|
| 142 |
-
" session.context_manager.items.append(Message.model_validate(raw))\n"
|
| 143 |
-
" seeded_from_dataset += 1\n"
|
| 144 |
-
" except Exception as e:\n"
|
| 145 |
-
" logger.warning(\"Dropping dataset msg during seed: %s\", e)\n"
|
| 146 |
-
" except Exception as e:\n"
|
| 147 |
-
" logger.debug(\"Dataset seed fallback failed: %s\", e)\n"
|
| 148 |
-
"\n"
|
| 149 |
-
" if seeded_from_dataset:\n"
|
| 150 |
-
" self._touch(agent_session)\n"
|
| 151 |
-
" await self.persist_session_snapshot(agent_session, runtime_state=\"idle\")\n"
|
| 152 |
-
" return seeded_from_dataset\n"
|
| 153 |
-
"\n"
|
| 154 |
-
" seed = Message(\n"
|
| 155 |
-
" role=\"user\",\n"
|
| 156 |
-
" content=(\n"
|
| 157 |
-
" \"[SYSTEM: Your prior memory of this conversation \u2014 written \"\n"
|
| 158 |
-
" \"in your own voice right before restart. Continue from here.]\\n\\n\"\n"
|
| 159 |
-
" + (summary or \"(no summary returned)\")\n"
|
| 160 |
-
" ),\n"
|
| 161 |
-
" )\n"
|
| 162 |
-
)
|
| 163 |
-
content, ok = _replace(content, old_seed, new_seed)
|
| 164 |
-
if ok:
|
| 165 |
-
print("+ seed_from_summary restores full dataset history")
|
| 166 |
-
changed = True
|
| 167 |
-
else:
|
| 168 |
-
print("INFO: seed block anchor not found")
|
| 169 |
-
|
| 170 |
-
if changed:
|
| 171 |
-
try:
|
| 172 |
-
ast.parse(content)
|
| 173 |
-
_write(SESSION_MANAGER, content)
|
| 174 |
-
print("OK: session_manager.py patched (dataset context restore)")
|
| 175 |
-
except SyntaxError as e:
|
| 176 |
-
print("FAIL: session_manager.py syntax error:", e)
|
| 177 |
-
else:
|
| 178 |
-
print("SKIP: no changes applied")
|
| 179 |
-
|
| 180 |
-
# ---- Verify importability of patched modules -----------------------------
|
| 181 |
-
try:
|
| 182 |
-
import ast as _ast
|
| 183 |
-
for f in ["/app/backend/session_manager.py", "/app/backend/session_logger.py", "/app/backend/routes/agent.py"]:
|
| 184 |
-
if os.path.exists(f):
|
| 185 |
-
_ast.parse(open(f, encoding="utf-8").read())
|
| 186 |
-
print("SYNTAX OK:", f)
|
| 187 |
-
except Exception as e:
|
| 188 |
-
print("VERIFY ERR:", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_catch_up_dataset_fallback.py
DELETED
|
@@ -1,102 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Patch frontend: Add dataset fallback to ExpiredBanner handleCatchUp.
|
| 4 |
-
|
| 5 |
-
When localStorage caches (backend-messages, ui-messages) are empty
|
| 6 |
-
(user cleared cache, new browser, Space restarted), the catch-up flow
|
| 7 |
-
shows "Nothing to summarize from this chat.". This patch adds a fallback
|
| 8 |
-
that fetches messages from /api/session-history/{session_id} — which reads
|
| 9 |
-
from the permanent HF dataset (bep40/ml-intern-sessions).
|
| 10 |
-
|
| 11 |
-
This ensures the "Catch me up" button always works as long as the
|
| 12 |
-
session was previously logged to the permanent dataset.
|
| 13 |
-
"""
|
| 14 |
-
|
| 15 |
-
import os
|
| 16 |
-
import sys
|
| 17 |
-
|
| 18 |
-
PATCHED_MARKER = "// === PATCH: catch-up dataset fallback ==="
|
| 19 |
-
TARGET_FILE = "/source/frontend/src/components/Chat/ExpiredBanner.tsx"
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def patch_expired_banner(content: str) -> tuple[str, bool]:
|
| 23 |
-
changed = False
|
| 24 |
-
|
| 25 |
-
if PATCHED_MARKER in content:
|
| 26 |
-
print(f" SKIP: already patched")
|
| 27 |
-
return content, False
|
| 28 |
-
|
| 29 |
-
# Add a fallback to /api/session-history/{session_id} before the error.
|
| 30 |
-
# The old block checks if messages array is empty and bails with error.
|
| 31 |
-
old_block = (
|
| 32 |
-
" if (!messages || messages.length === 0) {\n"
|
| 33 |
-
" setError('Nothing to summarize from this chat.');\n"
|
| 34 |
-
" setBusy(null);\n"
|
| 35 |
-
" return;\n"
|
| 36 |
-
" }\n"
|
| 37 |
-
)
|
| 38 |
-
new_block = (
|
| 39 |
-
PATCHED_MARKER + "\n"
|
| 40 |
-
" // Fallback: when localStorage caches are empty (Space restart,\n"
|
| 41 |
-
" // new browser, cache cleared), read from the permanent HF dataset\n"
|
| 42 |
-
" // via /api/session-history/{session_id}.\n"
|
| 43 |
-
" if (!messages || messages.length === 0) {\n"
|
| 44 |
-
" try {\n"
|
| 45 |
-
" const res = await apiFetch(`/api/session-history/${sessionId}`);\n"
|
| 46 |
-
" if (res.ok) {\n"
|
| 47 |
-
" const history = await res.json();\n"
|
| 48 |
-
" if (Array.isArray(history) && history.length > 0) {\n"
|
| 49 |
-
" messages = history.map((row: any) => ({\n"
|
| 50 |
-
" role: row.role || 'assistant',\n"
|
| 51 |
-
" content: row.content || '',\n"
|
| 52 |
-
" }));\n"
|
| 53 |
-
" }\n"
|
| 54 |
-
" }\n"
|
| 55 |
-
" } catch {\n"
|
| 56 |
-
" /* dataset fetch failed — continue to error below */\n"
|
| 57 |
-
" }\n"
|
| 58 |
-
" }\n"
|
| 59 |
-
" if (!messages || messages.length === 0) {\n"
|
| 60 |
-
" setError('Nothing to summarize from this chat.');\n"
|
| 61 |
-
" setBusy(null);\n"
|
| 62 |
-
" return;\n"
|
| 63 |
-
" }\n"
|
| 64 |
-
)
|
| 65 |
-
if old_block in content:
|
| 66 |
-
content = content.replace(old_block, new_block, 1)
|
| 67 |
-
changed = True
|
| 68 |
-
print(" + Added dataset fallback to handleCatchUp")
|
| 69 |
-
else:
|
| 70 |
-
print(" SKIP: target block not found (pattern may have changed)")
|
| 71 |
-
|
| 72 |
-
return content, changed
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def main():
|
| 76 |
-
if not os.path.exists(TARGET_FILE):
|
| 77 |
-
print(f"SKIP: {TARGET_FILE} not found — frontend source not available")
|
| 78 |
-
return
|
| 79 |
-
|
| 80 |
-
with open(TARGET_FILE, "r", encoding="utf-8") as f:
|
| 81 |
-
content = f.read()
|
| 82 |
-
|
| 83 |
-
if PATCHED_MARKER in content:
|
| 84 |
-
print(f"SKIP: {TARGET_FILE} already patched")
|
| 85 |
-
return
|
| 86 |
-
|
| 87 |
-
content, changed = patch_expired_banner(content)
|
| 88 |
-
if changed:
|
| 89 |
-
with open(TARGET_FILE, "w", encoding="utf-8") as f:
|
| 90 |
-
f.write(content)
|
| 91 |
-
print(f"OK: {TARGET_FILE} patched with dataset fallback")
|
| 92 |
-
else:
|
| 93 |
-
print(f"SKIP: {TARGET_FILE} — no changes applied")
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
if __name__ == "__main__":
|
| 97 |
-
try:
|
| 98 |
-
main()
|
| 99 |
-
except Exception as e:
|
| 100 |
-
print(f"WARN: patch_catch_up_dataset_fallback.py error: {e}")
|
| 101 |
-
# Never fail the Docker build
|
| 102 |
-
sys.exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_catch_up_fix_backend.py
DELETED
|
@@ -1,220 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Patch: Fix catch-up backend robustness.
|
| 4 |
-
|
| 5 |
-
Issues fixed:
|
| 6 |
-
1. /api/session-history/{session_id} returns ALL rows including 'event' type
|
| 7 |
-
rows (role='event'). These cause Message.model_validate to silently drop them.
|
| 8 |
-
Fix: Filter out non-message roles (event, system) from history endpoint.
|
| 9 |
-
|
| 10 |
-
2. seed_from_summary raises if summarization fails (API error, rate limit, credits),
|
| 11 |
-
causing restore-summary to return 500 and the frontend to show "Couldn't catch up."
|
| 12 |
-
Fix: Fall back to raw messages if summarization fails.
|
| 13 |
-
|
| 14 |
-
3. Event rows with role='event' fail Message.model_validate.
|
| 15 |
-
Fix: Skip event rows explicitly in seed_from_summary.
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
import ast
|
| 19 |
-
import os
|
| 20 |
-
import sys
|
| 21 |
-
|
| 22 |
-
PATCHED_MARKER = "# === PATCH: catch-up fix (backend) ==="
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def _safe_replace(content, old, new, count=1):
|
| 26 |
-
if old in content:
|
| 27 |
-
content = content.replace(old, new, count)
|
| 28 |
-
return content, True
|
| 29 |
-
return content, False
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def patch_file(path, patch_fn):
|
| 33 |
-
if not os.path.exists(path):
|
| 34 |
-
print(f"SKIP: {path} not found")
|
| 35 |
-
return False
|
| 36 |
-
try:
|
| 37 |
-
with open(path, "r") as f:
|
| 38 |
-
content = f.read()
|
| 39 |
-
content, changed = patch_fn(content)
|
| 40 |
-
if not changed:
|
| 41 |
-
print(f"SKIP: {path} — no changes needed")
|
| 42 |
-
return False
|
| 43 |
-
try:
|
| 44 |
-
ast.parse(content)
|
| 45 |
-
except SyntaxError as e:
|
| 46 |
-
print(f"FAIL: {path} syntax: {e}")
|
| 47 |
-
return False
|
| 48 |
-
with open(path, "w") as f:
|
| 49 |
-
f.write(content)
|
| 50 |
-
print(f"OK: {path} patched (catch-up fix)")
|
| 51 |
-
return True
|
| 52 |
-
except Exception as e:
|
| 53 |
-
print(f"WARN: {path} patch error: {e}")
|
| 54 |
-
return False
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
# ─── Patch routes/agent.py: filter event rows from session-history ─────────
|
| 58 |
-
print("=== Patching routes/agent.py (session-history filter) ===")
|
| 59 |
-
|
| 60 |
-
ROUTES_AGENT = "/app/backend/routes/agent.py"
|
| 61 |
-
|
| 62 |
-
def _patch_routes(content):
|
| 63 |
-
changed = False
|
| 64 |
-
|
| 65 |
-
if PATCHED_MARKER in content:
|
| 66 |
-
print(" SKIP: already patched")
|
| 67 |
-
return content, False
|
| 68 |
-
|
| 69 |
-
# Patch the get_session_history endpoint to filter out event rows
|
| 70 |
-
old_endpoint = (
|
| 71 |
-
'@router.get("/session-history/{session_id}")\n'
|
| 72 |
-
'async def get_session_history(\n'
|
| 73 |
-
' session_id: str, user: dict = Depends(get_current_user)\n'
|
| 74 |
-
') -> list:\n'
|
| 75 |
-
' """Get full message history for a session from the permanent dataset."""\n'
|
| 76 |
-
' from backend.session_logger import load_session_history\n'
|
| 77 |
-
' return load_session_history(session_id)'
|
| 78 |
-
)
|
| 79 |
-
|
| 80 |
-
new_endpoint = (
|
| 81 |
-
'@router.get("/session-history/{session_id}")\n'
|
| 82 |
-
'async def get_session_history(\n'
|
| 83 |
-
' session_id: str, user: dict = Depends(get_current_user)\n'
|
| 84 |
-
') -> list:\n'
|
| 85 |
-
' """Get full message history for a session from the permanent dataset.\n'
|
| 86 |
-
'\n'
|
| 87 |
-
' Returns only valid conversation messages (user/assistant/tool roles).\n'
|
| 88 |
-
" Event rows (role='event') are filtered out because they are internal\n"
|
| 89 |
-
' tracing data that cannot be parsed as Message objects and would be\n'
|
| 90 |
-
' silently dropped during catch-up, causing message loss.\n'
|
| 91 |
-
' """\n'
|
| 92 |
-
' from backend.session_logger import load_session_history\n'
|
| 93 |
-
' rows = load_session_history(session_id)\n'
|
| 94 |
-
' # Filter out event rows and any rows without valid roles\n'
|
| 95 |
-
' valid_roles = {"user", "assistant", "tool"}\n'
|
| 96 |
-
' return [r for r in rows if r.get("role") in valid_roles]'
|
| 97 |
-
)
|
| 98 |
-
|
| 99 |
-
content, ok = _safe_replace(content, old_endpoint, new_endpoint)
|
| 100 |
-
if ok:
|
| 101 |
-
print(" + Filtered event rows from session-history endpoint")
|
| 102 |
-
changed = True
|
| 103 |
-
else:
|
| 104 |
-
print(" SKIP: target block not found")
|
| 105 |
-
|
| 106 |
-
# Add marker
|
| 107 |
-
content = content.rstrip() + "\n\n" + PATCHED_MARKER + "\n"
|
| 108 |
-
changed = True
|
| 109 |
-
|
| 110 |
-
return content, changed
|
| 111 |
-
|
| 112 |
-
patch_file(ROUTES_AGENT, _patch_routes)
|
| 113 |
-
|
| 114 |
-
# ─── Patch session_manager.py: robust seed_from_summary ─────────────────────
|
| 115 |
-
print("\n=== Patching session_manager.py (seed_from_summary robustness) ===")
|
| 116 |
-
|
| 117 |
-
SESSION_MANAGER = "/app/backend/session_manager.py"
|
| 118 |
-
|
| 119 |
-
def _patch_seed_from_summary(content):
|
| 120 |
-
changed = False
|
| 121 |
-
|
| 122 |
-
# Check if already patched
|
| 123 |
-
if PATCHED_MARKER in content:
|
| 124 |
-
print(" SKIP: already patched")
|
| 125 |
-
return content, False
|
| 126 |
-
|
| 127 |
-
# Patch 1: Skip event rows explicitly in seed_from_summary
|
| 128 |
-
old_parse = (
|
| 129 |
-
' # Parse into Message objects, tolerating malformed entries.\n'
|
| 130 |
-
' parsed: list[Message] = []\n'
|
| 131 |
-
' for raw in messages:\n'
|
| 132 |
-
' if raw.get("role") == "system":\n'
|
| 133 |
-
' continue # the new session has its own system prompt\n'
|
| 134 |
-
' try:\n'
|
| 135 |
-
' parsed.append(Message.model_validate(raw))\n'
|
| 136 |
-
' except Exception as e:\n'
|
| 137 |
-
' logger.warning("Dropping malformed message during seed: %s", e)'
|
| 138 |
-
)
|
| 139 |
-
|
| 140 |
-
new_parse = (
|
| 141 |
-
' # Parse into Message objects, tolerating malformed entries.\n'
|
| 142 |
-
' # === PATCH: skip event rows (not just system) ===\n'
|
| 143 |
-
' parsed: list[Message] = []\n'
|
| 144 |
-
' for raw in messages:\n'
|
| 145 |
-
' role = raw.get("role", "")\n'
|
| 146 |
-
' if role in ("system", "event"):\n'
|
| 147 |
-
' continue # not conversation messages\n'
|
| 148 |
-
' try:\n'
|
| 149 |
-
' parsed.append(Message.model_validate(raw))\n'
|
| 150 |
-
' except Exception as e:\n'
|
| 151 |
-
' logger.warning("Dropping malformed message during seed: %s", e)'
|
| 152 |
-
)
|
| 153 |
-
|
| 154 |
-
content, ok = _safe_replace(content, old_parse, new_parse)
|
| 155 |
-
if ok:
|
| 156 |
-
print(" + Skip event rows explicitly in seed_from_summary")
|
| 157 |
-
changed = True
|
| 158 |
-
|
| 159 |
-
# Patch 2: Fall back to raw messages if summarization fails
|
| 160 |
-
old_try = (
|
| 161 |
-
' try:\n'
|
| 162 |
-
' summary, _ = await summarize_messages(\n'
|
| 163 |
-
' parsed,\n'
|
| 164 |
-
' model_name=session.config.model_name,\n'
|
| 165 |
-
' hf_token=session.hf_token,\n'
|
| 166 |
-
' max_tokens=4000,\n'
|
| 167 |
-
' prompt=_RESTORE_PROMPT,\n'
|
| 168 |
-
' tool_specs=tool_specs,\n'
|
| 169 |
-
' session=session,\n'
|
| 170 |
-
' kind="restore",\n'
|
| 171 |
-
' )\n'
|
| 172 |
-
' except Exception as e:\n'
|
| 173 |
-
' logger.error("Summary call failed during seed: %s", e)\n'
|
| 174 |
-
' raise\n'
|
| 175 |
-
'\n'
|
| 176 |
-
' seed = Message('
|
| 177 |
-
)
|
| 178 |
-
|
| 179 |
-
new_try = (
|
| 180 |
-
' # === PATCH: fall back to raw messages if summarization fails ===\n'
|
| 181 |
-
' summary = None\n'
|
| 182 |
-
' try:\n'
|
| 183 |
-
' summary, _ = await summarize_messages(\n'
|
| 184 |
-
' parsed,\n'
|
| 185 |
-
' model_name=session.config.model_name,\n'
|
| 186 |
-
' hf_token=session.hf_token,\n'
|
| 187 |
-
' max_tokens=4000,\n'
|
| 188 |
-
' prompt=_RESTORE_PROMPT,\n'
|
| 189 |
-
' tool_specs=tool_specs,\n'
|
| 190 |
-
' session=session,\n'
|
| 191 |
-
' kind="restore",\n'
|
| 192 |
-
' )\n'
|
| 193 |
-
' except Exception as e:\n'
|
| 194 |
-
' logger.error("Summary call failed during seed, falling back to raw messages: %s", e)\n'
|
| 195 |
-
' # Fall back: inject raw messages directly into the new session context\n'
|
| 196 |
-
' # This preserves the full conversation instead of losing it\n'
|
| 197 |
-
' for msg in parsed:\n'
|
| 198 |
-
' session.context_manager.items.append(msg)\n'
|
| 199 |
-
' self._touch(agent_session)\n'
|
| 200 |
-
' await self.persist_session_snapshot(agent_session, runtime_state="idle")\n'
|
| 201 |
-
' return len(parsed)\n'
|
| 202 |
-
'\n'
|
| 203 |
-
' seed = Message('
|
| 204 |
-
)
|
| 205 |
-
|
| 206 |
-
content, ok = _safe_replace(content, old_try, new_try)
|
| 207 |
-
if ok:
|
| 208 |
-
print(" + Fall back to raw messages if summarization fails")
|
| 209 |
-
changed = True
|
| 210 |
-
|
| 211 |
-
# Add marker
|
| 212 |
-
if changed:
|
| 213 |
-
content = content + "\n" + PATCHED_MARKER + "\n"
|
| 214 |
-
changed = True
|
| 215 |
-
|
| 216 |
-
return content, changed
|
| 217 |
-
|
| 218 |
-
patch_file(SESSION_MANAGER, _patch_seed_from_summary)
|
| 219 |
-
|
| 220 |
-
print("\n=== Backend catch-up fixes applied ===")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_catch_up_fix_frontend.py
DELETED
|
@@ -1,289 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Patch: Fix frontend catch-up message loss.
|
| 4 |
-
|
| 5 |
-
Issue: After catch-up succeeds, the new session's backend hydration
|
| 6 |
-
fetches /api/session/{newId}/messages from the backend, which only
|
| 7 |
-
contains the summary message. It then calls setMessages() and
|
| 8 |
-
saveMessages(), OVERWRITING the old full messages that were moved to
|
| 9 |
-
the new ID via renameSession -> moveMessages.
|
| 10 |
-
|
| 11 |
-
Fix: In each hydration code path, only overwrite frontend messages if
|
| 12 |
-
the backend returns MORE non-system messages than the frontend already has.
|
| 13 |
-
If the frontend already has more messages (from localStorage), keep them.
|
| 14 |
-
|
| 15 |
-
Also: Fix the catch-up dataset fallback in ExpiredBanner to properly
|
| 16 |
-
filter event rows and handle malformed data.
|
| 17 |
-
|
| 18 |
-
Runs during Docker build — never fails the build.
|
| 19 |
-
"""
|
| 20 |
-
|
| 21 |
-
import ast
|
| 22 |
-
import os
|
| 23 |
-
import sys
|
| 24 |
-
|
| 25 |
-
PATCHED_MARKER = "// PATCH: catch-up fix (frontend)"
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def _safe_replace(content, old, new, count=1):
|
| 29 |
-
if old in content:
|
| 30 |
-
content = content.replace(old, new, count)
|
| 31 |
-
return content, True
|
| 32 |
-
return content, False
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def _is_python_file(path):
|
| 36 |
-
return path.endswith(".py")
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def patch_file(path, patch_fn):
|
| 40 |
-
if not os.path.exists(path):
|
| 41 |
-
print(f"SKIP: {path} not found")
|
| 42 |
-
return False
|
| 43 |
-
try:
|
| 44 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 45 |
-
content = f.read()
|
| 46 |
-
content, changed = patch_fn(content)
|
| 47 |
-
if not changed:
|
| 48 |
-
print(f"SKIP: {path} — no changes needed")
|
| 49 |
-
return False
|
| 50 |
-
# Only validate Python syntax for .py files
|
| 51 |
-
if _is_python_file(path):
|
| 52 |
-
try:
|
| 53 |
-
ast.parse(content)
|
| 54 |
-
except SyntaxError as e:
|
| 55 |
-
print(f"FAIL: {path} syntax: {e}")
|
| 56 |
-
return False
|
| 57 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 58 |
-
f.write(content)
|
| 59 |
-
print(f"OK: {path} patched (catch-up fix)")
|
| 60 |
-
return True
|
| 61 |
-
except Exception as e:
|
| 62 |
-
print(f"WARN: {path} patch error: {e}")
|
| 63 |
-
return False
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
# ─── Patch useAgentChat.ts ──────────────────────────────────────────────────
|
| 67 |
-
print("=== Patching useAgentChat.ts (prevent message overwrite) ===")
|
| 68 |
-
|
| 69 |
-
USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 70 |
-
|
| 71 |
-
def _patch_use_agent_chat(content):
|
| 72 |
-
changed = False
|
| 73 |
-
|
| 74 |
-
if PATCHED_MARKER in content:
|
| 75 |
-
print(" SKIP: already patched")
|
| 76 |
-
return content, False
|
| 77 |
-
|
| 78 |
-
# Patch 1: onRecoverMessages
|
| 79 |
-
# The pattern uses 10-space indentation (inside a nested callback)
|
| 80 |
-
old_pattern1 = (
|
| 81 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 82 |
-
" if (setMsgs && uiMsgs.length >= currentMessageCount) {\n"
|
| 83 |
-
" setMsgs(uiMsgs);\n"
|
| 84 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 85 |
-
" }"
|
| 86 |
-
)
|
| 87 |
-
|
| 88 |
-
new_pattern1 = (
|
| 89 |
-
" // === PATCH: catch-up fix (frontend) ===\n"
|
| 90 |
-
" // After catch-up, the backend only has a summary message but\n"
|
| 91 |
-
" // the frontend (via localStorage moveMessages) has the full\n"
|
| 92 |
-
" // old conversation. Don't overwrite if backend has fewer.\n"
|
| 93 |
-
" const _currentNonSystem = chatActionsRef.current.messages.filter(\n"
|
| 94 |
-
" (m) => !(m.parts?.[0]?.type === 'text' && typeof m.parts?.[0]?.text === 'string' && m.parts?.[0].text.trimStart().startsWith('[SYSTEM:'))\n"
|
| 95 |
-
" );\n"
|
| 96 |
-
" const _shouldOverwrite = uiMsgs.length > _currentNonSystem.length || _currentNonSystem.length === 0;\n"
|
| 97 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 98 |
-
" if (setMsgs && _shouldOverwrite) {\n"
|
| 99 |
-
" setMsgs(uiMsgs);\n"
|
| 100 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 101 |
-
" }"
|
| 102 |
-
)
|
| 103 |
-
|
| 104 |
-
content, ok = _safe_replace(content, old_pattern1, new_pattern1)
|
| 105 |
-
if ok:
|
| 106 |
-
print(" + Prevented backend overwrite in onRecoverMessages")
|
| 107 |
-
changed = True
|
| 108 |
-
else:
|
| 109 |
-
print(" INFO: onRecoverMessages pattern not found")
|
| 110 |
-
|
| 111 |
-
# Patch 2: refreshMessages
|
| 112 |
-
# The pattern uses 6-space indentation and ends with 'return true;'
|
| 113 |
-
old_pattern2 = (
|
| 114 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 115 |
-
" if (setMsgs && uiMsgs.length > 0) {\n"
|
| 116 |
-
" setMsgs(uiMsgs);\n"
|
| 117 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 118 |
-
" }\n"
|
| 119 |
-
" return true;"
|
| 120 |
-
)
|
| 121 |
-
|
| 122 |
-
new_pattern2 = (
|
| 123 |
-
" // === PATCH: catch-up fix (frontend) ===\n"
|
| 124 |
-
" const _currentNonSystem2 = chatActionsRef.current.messages.filter(\n"
|
| 125 |
-
" (m) => !(m.parts?.[0]?.type === 'text' && typeof m.parts?.[0]?.text === 'string' && m.parts?.[0].text.trimStart().startsWith('[SYSTEM:'))\n"
|
| 126 |
-
" );\n"
|
| 127 |
-
" const _shouldOverwrite2 = uiMsgs.length > _currentNonSystem2.length || _currentNonSystem2.length === 0;\n"
|
| 128 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 129 |
-
" if (setMsgs && _shouldOverwrite2) {\n"
|
| 130 |
-
" setMsgs(uiMsgs);\n"
|
| 131 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 132 |
-
" }\n"
|
| 133 |
-
" return true;"
|
| 134 |
-
)
|
| 135 |
-
|
| 136 |
-
content, ok2 = _safe_replace(content, old_pattern2, new_pattern2)
|
| 137 |
-
if ok2:
|
| 138 |
-
print(" + Prevented backend overwrite in refreshMessages")
|
| 139 |
-
changed = True
|
| 140 |
-
else:
|
| 141 |
-
print(" INFO: refreshMessages pattern not found")
|
| 142 |
-
|
| 143 |
-
# Patch 3: Main hydration useEffect (uses chat.setMessages)
|
| 144 |
-
# There are multiple code paths with different indentation levels:
|
| 145 |
-
# - 6-space indent: in some code paths
|
| 146 |
-
# - 10-space indent: main hydration useEffect
|
| 147 |
-
# - 14-space indent: hydrateMessages helper (appears twice)
|
| 148 |
-
# We patch each indentation level separately.
|
| 149 |
-
|
| 150 |
-
old_pattern3a = (
|
| 151 |
-
" if (uiMsgs.length > 0) {\n"
|
| 152 |
-
" chat.setMessages(uiMsgs);\n"
|
| 153 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 154 |
-
" }"
|
| 155 |
-
)
|
| 156 |
-
|
| 157 |
-
new_pattern3a = (
|
| 158 |
-
" // === PATCH: catch-up fix (frontend) ===\n"
|
| 159 |
-
" // Don't overwrite frontend messages with backend's fewer messages\n"
|
| 160 |
-
" const _currentMsgs = (chatActionsRef.current.messages || []).filter(\n"
|
| 161 |
-
" (m) => !(m.parts?.[0]?.type === 'text' && typeof m.parts?.[0]?.text === 'string' && m.parts?.[0].text.trimStart().startsWith('[SYSTEM:'))\n"
|
| 162 |
-
" );\n"
|
| 163 |
-
" const _shouldSet = uiMsgs.length > _currentMsgs.length || _currentMsgs.length === 0;\n"
|
| 164 |
-
" if (_shouldSet) {\n"
|
| 165 |
-
" chat.setMessages(uiMsgs);\n"
|
| 166 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 167 |
-
" }"
|
| 168 |
-
)
|
| 169 |
-
|
| 170 |
-
# 10-space indentation variant
|
| 171 |
-
old_pattern3b = (
|
| 172 |
-
" if (uiMsgs.length > 0) {\n"
|
| 173 |
-
" chat.setMessages(uiMsgs);\n"
|
| 174 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 175 |
-
" }"
|
| 176 |
-
)
|
| 177 |
-
|
| 178 |
-
new_pattern3b = (
|
| 179 |
-
" // === PATCH: catch-up fix (frontend) ===\n"
|
| 180 |
-
" const _currentMsgsB = (chatActionsRef.current.messages || []).filter(\n"
|
| 181 |
-
" (m) => !(m.parts?.[0]?.type === 'text' && typeof m.parts?.[0]?.text === 'string' && m.parts?.[0].text.trimStart().startsWith('[SYSTEM:'))\n"
|
| 182 |
-
" );\n"
|
| 183 |
-
" const _shouldSetB = uiMsgs.length > _currentMsgsB.length || _currentMsgsB.length === 0;\n"
|
| 184 |
-
" if (_shouldSetB) {\n"
|
| 185 |
-
" chat.setMessages(uiMsgs);\n"
|
| 186 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 187 |
-
" }"
|
| 188 |
-
)
|
| 189 |
-
|
| 190 |
-
# 14-space indentation variant
|
| 191 |
-
old_pattern3c = (
|
| 192 |
-
" if (uiMsgs.length > 0) {\n"
|
| 193 |
-
" chat.setMessages(uiMsgs);\n"
|
| 194 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 195 |
-
" }"
|
| 196 |
-
)
|
| 197 |
-
|
| 198 |
-
new_pattern3c = (
|
| 199 |
-
" // === PATCH: catch-up fix (frontend) ===\n"
|
| 200 |
-
" const _currentMsgsC = (chatActionsRef.current.messages || []).filter(\n"
|
| 201 |
-
" (m) => !(m.parts?.[0]?.type === 'text' && typeof m.parts?.[0]?.text === 'string' && m.parts?.[0].text.trimStart().startsWith('[SYSTEM:'))\n"
|
| 202 |
-
" );\n"
|
| 203 |
-
" const _shouldSetC = uiMsgs.length > _currentMsgsC.length || _currentMsgsC.length === 0;\n"
|
| 204 |
-
" if (_shouldSetC) {\n"
|
| 205 |
-
" chat.setMessages(uiMsgs);\n"
|
| 206 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 207 |
-
" }"
|
| 208 |
-
)
|
| 209 |
-
|
| 210 |
-
count = 0
|
| 211 |
-
for old_p, new_p in [(old_pattern3a, new_pattern3a), (old_pattern3b, new_pattern3b), (old_pattern3c, new_pattern3c)]:
|
| 212 |
-
while True:
|
| 213 |
-
content, ok3 = _safe_replace(content, old_p, new_p)
|
| 214 |
-
if ok3:
|
| 215 |
-
count += 1
|
| 216 |
-
changed = True
|
| 217 |
-
else:
|
| 218 |
-
break
|
| 219 |
-
if count > 0:
|
| 220 |
-
print(f" + Prevented backend overwrite in {count} hydration code paths")
|
| 221 |
-
|
| 222 |
-
if changed:
|
| 223 |
-
content = content.replace(
|
| 224 |
-
"import { logger } from '@/utils/logger';",
|
| 225 |
-
"import { logger } from '@/utils/logger';\n// " + PATCHED_MARKER + " applied",
|
| 226 |
-
1
|
| 227 |
-
)
|
| 228 |
-
|
| 229 |
-
return content, changed
|
| 230 |
-
|
| 231 |
-
patch_file(USE_AGENT_CHAT, _patch_use_agent_chat)
|
| 232 |
-
|
| 233 |
-
# ─── Patch ExpiredBanner.tsx: filter event rows in dataset fallback ─────────
|
| 234 |
-
print("\n=== Patching ExpiredBanner.tsx (dataset fallback filter) ===")
|
| 235 |
-
|
| 236 |
-
EXPIRED_BANNER = "/source/frontend/src/components/Chat/ExpiredBanner.tsx"
|
| 237 |
-
|
| 238 |
-
def _patch_expired_banner(content):
|
| 239 |
-
changed = False
|
| 240 |
-
|
| 241 |
-
if PATCHED_MARKER in content:
|
| 242 |
-
print(" SKIP: already patched")
|
| 243 |
-
return content, False
|
| 244 |
-
|
| 245 |
-
# Check if the dataset fallback is present (from patch_catch_up_dataset_fallback.py)
|
| 246 |
-
if "/api/session-history/" not in content:
|
| 247 |
-
print(" SKIP: no dataset fallback found in ExpiredBanner.tsx")
|
| 248 |
-
return content, False
|
| 249 |
-
|
| 250 |
-
# Patch the dataset fallback mapping to filter event rows
|
| 251 |
-
old_mapping = (
|
| 252 |
-
" messages = history.map((row: any) => ({\n"
|
| 253 |
-
" role: row.role || 'assistant',\n"
|
| 254 |
-
" content: row.content || '',\n"
|
| 255 |
-
" }));"
|
| 256 |
-
)
|
| 257 |
-
|
| 258 |
-
new_mapping = (
|
| 259 |
-
" // === PATCH: catch-up fix (frontend) ===\n"
|
| 260 |
-
" // Filter out 'event' rows — they are internal tracing\n"
|
| 261 |
-
" // data that cannot be parsed as Message objects\n"
|
| 262 |
-
" const validRoles = new Set(['user', 'assistant', 'tool']);\n"
|
| 263 |
-
" messages = history\n"
|
| 264 |
-
" .filter((row: any) => validRoles.has(row.role))\n"
|
| 265 |
-
" .map((row: any) => ({\n"
|
| 266 |
-
" role: row.role,\n"
|
| 267 |
-
" content: row.content || '',\n"
|
| 268 |
-
" }));"
|
| 269 |
-
)
|
| 270 |
-
|
| 271 |
-
content, ok = _safe_replace(content, old_mapping, new_mapping)
|
| 272 |
-
if ok:
|
| 273 |
-
print(" + Filtered event rows in dataset fallback")
|
| 274 |
-
changed = True
|
| 275 |
-
else:
|
| 276 |
-
print(" INFO: dataset fallback mapping pattern not found")
|
| 277 |
-
|
| 278 |
-
if changed:
|
| 279 |
-
content = content.replace(
|
| 280 |
-
"import { logger } from '@/utils/logger';",
|
| 281 |
-
"import { logger } from '@/utils/logger';\n// " + PATCHED_MARKER + " applied",
|
| 282 |
-
1
|
| 283 |
-
)
|
| 284 |
-
|
| 285 |
-
return content, changed
|
| 286 |
-
|
| 287 |
-
patch_file(EXPIRED_BANNER, _patch_expired_banner)
|
| 288 |
-
|
| 289 |
-
print("\n=== Frontend catch-up fixes applied ===")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_catch_up_fix_v2.py
DELETED
|
@@ -1,758 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Patch v2: Comprehensive catch-up + shareable URL fix for bep40/ml-intern.
|
| 4 |
-
|
| 5 |
-
Addresses three problems:
|
| 6 |
-
1. "Catch me up" throws errors frequently
|
| 7 |
-
2. Context is lost after catch-up (context preserved in session but not restored)
|
| 8 |
-
3. Per-session shareable URLs
|
| 9 |
-
|
| 10 |
-
Runs during Docker build — never fails the build.
|
| 11 |
-
"""
|
| 12 |
-
|
| 13 |
-
import ast
|
| 14 |
-
import os
|
| 15 |
-
import re
|
| 16 |
-
import sys
|
| 17 |
-
|
| 18 |
-
# ─── Backend paths (exist in the Python/stage-2 build) ───────────────────────
|
| 19 |
-
|
| 20 |
-
BACKEND_SESSION_MANAGER = "/app/backend/session_manager.py"
|
| 21 |
-
BACKEND_SESSION_LOGGER = "/app/backend/session_logger.py"
|
| 22 |
-
BACKEND_ROUTES_AGENT = "/app/backend/routes/agent.py"
|
| 23 |
-
|
| 24 |
-
MARKER = "# === PATCH: catch-up-v2 ==="
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def _read(path):
|
| 28 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 29 |
-
return f.read()
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def _write(path, content):
|
| 33 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 34 |
-
f.write(content)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def _safe_replace(content, old, new):
|
| 38 |
-
if old in content:
|
| 39 |
-
return content.replace(old, new, 1), True
|
| 40 |
-
return content, False
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
# ─── Backend session_manager.py ─────────────────────────────────────────────
|
| 44 |
-
|
| 45 |
-
print("=== Patching session_manager.py ===")
|
| 46 |
-
|
| 47 |
-
if os.path.exists(BACKEND_SESSION_MANAGER):
|
| 48 |
-
content = _read(BACKEND_SESSION_MANAGER)
|
| 49 |
-
changed = False
|
| 50 |
-
|
| 51 |
-
# 1. seed_from_summary: fall back to raw messages on summary failure
|
| 52 |
-
if 'logger.error("Summary call failed during seed: %s", e)' in content and "Fall back to raw messages" not in content:
|
| 53 |
-
old = (
|
| 54 |
-
' except Exception as e:\n'
|
| 55 |
-
' logger.error("Summary call failed during seed: %s", e)\n'
|
| 56 |
-
' raise\n'
|
| 57 |
-
)
|
| 58 |
-
new = (
|
| 59 |
-
' except Exception as e:\n'
|
| 60 |
-
' logger.error("Summary call failed during seed, falling back to raw messages: %s", e)\n'
|
| 61 |
-
' for msg in parsed:\n'
|
| 62 |
-
' session.context_manager.items.append(msg)\n'
|
| 63 |
-
' self._touch(agent_session)\n'
|
| 64 |
-
' await self.persist_session_snapshot(agent_session, runtime_state="idle")\n'
|
| 65 |
-
' return len(parsed)\n'
|
| 66 |
-
)
|
| 67 |
-
if old in content:
|
| 68 |
-
content, ok = _safe_replace(content, old, new)
|
| 69 |
-
if ok:
|
| 70 |
-
print(" + seed_from_summary falls back to raw messages")
|
| 71 |
-
changed = True
|
| 72 |
-
else:
|
| 73 |
-
# Try regex match for the pattern
|
| 74 |
-
pattern = re.compile(
|
| 75 |
-
r' logger\.error\("Summary call failed during seed: %s", e\)\n raise\n'
|
| 76 |
-
)
|
| 77 |
-
match = pattern.search(content)
|
| 78 |
-
if match:
|
| 79 |
-
content = pattern.sub(
|
| 80 |
-
' logger.error("Summary call failed during seed, falling back to raw messages: %s", e)\n'
|
| 81 |
-
' for msg in parsed:\n'
|
| 82 |
-
' session.context_manager.items.append(msg)\n'
|
| 83 |
-
' self._touch(agent_session)\n'
|
| 84 |
-
' await self.persist_session_snapshot(agent_session, runtime_state="idle")\n'
|
| 85 |
-
' return len(parsed)\n',
|
| 86 |
-
content, count=1
|
| 87 |
-
)
|
| 88 |
-
print(" + seed_from_summary fallback (regex match)")
|
| 89 |
-
changed = True
|
| 90 |
-
|
| 91 |
-
# 2. seed_from_summary: skip event rows
|
| 92 |
-
if "skip event rows" not in content.lower():
|
| 93 |
-
old_parse = (
|
| 94 |
-
' parsed: list[Message] = []\n'
|
| 95 |
-
' for raw in messages:\n'
|
| 96 |
-
' if raw.get("role") == "system":\n'
|
| 97 |
-
' continue # the new session has its own system prompt\n'
|
| 98 |
-
)
|
| 99 |
-
new_parse = (
|
| 100 |
-
' parsed: list[Message] = []\n'
|
| 101 |
-
' for raw in messages:\n'
|
| 102 |
-
' role = raw.get("role", "")\n'
|
| 103 |
-
' if role in ("system", "event"):\n'
|
| 104 |
-
' continue # not conversation messages\n'
|
| 105 |
-
)
|
| 106 |
-
if old_parse in content:
|
| 107 |
-
content, ok = _safe_replace(content, old_parse, new_parse)
|
| 108 |
-
if ok:
|
| 109 |
-
print(" + seed_from_summary skips event rows")
|
| 110 |
-
changed = True
|
| 111 |
-
|
| 112 |
-
# 3. Add record_catchup_done method
|
| 113 |
-
if "def record_catchup_done" not in content:
|
| 114 |
-
insert_after = " return len(parsed)\n\n @staticmethod"
|
| 115 |
-
if insert_after in content:
|
| 116 |
-
new_method = (
|
| 117 |
-
" return len(parsed)\n\n"
|
| 118 |
-
" async def record_catchup_done(self, session_id: str) -> None:\n"
|
| 119 |
-
" \"\"\"Record that a session was successfully catch-up restored.\"\"\"\n"
|
| 120 |
-
" store = self._store()\n"
|
| 121 |
-
" if not getattr(store, 'enabled', False):\n"
|
| 122 |
-
" return\n"
|
| 123 |
-
" try:\n"
|
| 124 |
-
" await store.update_session_fields(\n"
|
| 125 |
-
" session_id,\n"
|
| 126 |
-
" catchup_done=True,\n"
|
| 127 |
-
" )\n"
|
| 128 |
-
" except Exception as e:\n"
|
| 129 |
-
" logger.debug('record_catchup_done failed: %s', e)\n\n"
|
| 130 |
-
" @staticmethod"
|
| 131 |
-
)
|
| 132 |
-
content, ok = _safe_replace(content, insert_after, new_method)
|
| 133 |
-
if ok:
|
| 134 |
-
print(" + Added record_catchup_done method")
|
| 135 |
-
changed = True
|
| 136 |
-
|
| 137 |
-
# 4. Record catchup_done after seed
|
| 138 |
-
old_seed_done = (
|
| 139 |
-
' session.context_manager.items.append(seed)\n'
|
| 140 |
-
' self._touch(agent_session)\n'
|
| 141 |
-
' await self.persist_session_snapshot(agent_session, runtime_state="idle")\n'
|
| 142 |
-
' return len(parsed)\n'
|
| 143 |
-
)
|
| 144 |
-
new_seed_done = (
|
| 145 |
-
' session.context_manager.items.append(seed)\n'
|
| 146 |
-
' self._touch(agent_session)\n'
|
| 147 |
-
' await self.persist_session_snapshot(agent_session, runtime_state="idle")\n'
|
| 148 |
-
' # === PATCH: catch-up-v2 ===\n'
|
| 149 |
-
' try:\n'
|
| 150 |
-
' await self.record_catchup_done(session_id)\n'
|
| 151 |
-
' except Exception as e:\n'
|
| 152 |
-
' logger.debug("record_catchup_done failed: %s", e)\n'
|
| 153 |
-
' return len(parsed)\n'
|
| 154 |
-
)
|
| 155 |
-
if old_seed_done in content:
|
| 156 |
-
content, ok = _safe_replace(content, old_seed_done, new_seed_done)
|
| 157 |
-
if ok:
|
| 158 |
-
print(" + seed_from_summary records catchup_done")
|
| 159 |
-
changed = True
|
| 160 |
-
|
| 161 |
-
if changed:
|
| 162 |
-
if MARKER not in content:
|
| 163 |
-
content = content + "\n" + MARKER + "\n"
|
| 164 |
-
try:
|
| 165 |
-
ast.parse(content)
|
| 166 |
-
_write(BACKEND_SESSION_MANAGER, content)
|
| 167 |
-
print("OK: session_manager.py patched")
|
| 168 |
-
except SyntaxError as e:
|
| 169 |
-
print(f"FAIL: session_manager.py syntax: {e}")
|
| 170 |
-
else:
|
| 171 |
-
print("SKIP: session_manager.py not found")
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
# ─── Backend session_logger.py ───────────────────────────────────────────────
|
| 175 |
-
|
| 176 |
-
print("\n=== Patching session_logger.py ===")
|
| 177 |
-
|
| 178 |
-
if os.path.exists(BACKEND_SESSION_LOGGER):
|
| 179 |
-
content = _read(BACKEND_SESSION_LOGGER)
|
| 180 |
-
changed = False
|
| 181 |
-
|
| 182 |
-
# load_session_history: filter event rows at source
|
| 183 |
-
sort_line = ' rows.sort(key=lambda r: (r.get("seq", 0), r.get("timestamp", "")))'
|
| 184 |
-
if sort_line in content and 'valid_roles' not in content.split(sort_line, 1)[1][:300]:
|
| 185 |
-
idx = content.find(sort_line)
|
| 186 |
-
end_idx = content.find("\n", idx) + 1
|
| 187 |
-
insert_code = (
|
| 188 |
-
' # === PATCH: catch-up-v2 ===\n'
|
| 189 |
-
' # Filter out event/system rows so summarization never sees\n'
|
| 190 |
-
' # internal tracing data that cant be parsed as Message objects.\n'
|
| 191 |
-
' valid_roles = {"user", "assistant", "tool"}\n'
|
| 192 |
-
' rows = [r for r in rows if r.get("role") in valid_roles]\n'
|
| 193 |
-
)
|
| 194 |
-
content = content[:end_idx] + insert_code + content[end_idx:]
|
| 195 |
-
print(" + load_session_history filters event rows")
|
| 196 |
-
changed = True
|
| 197 |
-
|
| 198 |
-
if changed:
|
| 199 |
-
try:
|
| 200 |
-
ast.parse(content)
|
| 201 |
-
_write(BACKEND_SESSION_LOGGER, content)
|
| 202 |
-
print("OK: session_logger.py patched")
|
| 203 |
-
except SyntaxError as e:
|
| 204 |
-
print(f"FAIL: session_logger.py syntax: {e}")
|
| 205 |
-
else:
|
| 206 |
-
print("SKIP: session_logger.py not found")
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
# ─── Backend routes/agent.py ─────────────────────────────────────────────────
|
| 210 |
-
|
| 211 |
-
print("\n=== Patching routes/agent.py ===")
|
| 212 |
-
|
| 213 |
-
if os.path.exists(BACKEND_ROUTES_AGENT):
|
| 214 |
-
content = _read(BACKEND_ROUTES_AGENT)
|
| 215 |
-
changed = False
|
| 216 |
-
|
| 217 |
-
# Ensure session-history endpoint filters events
|
| 218 |
-
old_history = (
|
| 219 |
-
' from backend.session_logger import load_session_history\n'
|
| 220 |
-
' return load_session_history(session_id)'
|
| 221 |
-
)
|
| 222 |
-
new_history = (
|
| 223 |
-
' from backend.session_logger import load_session_history\n'
|
| 224 |
-
' rows = load_session_history(session_id)\n'
|
| 225 |
-
' valid_roles = {"user", "assistant", "tool"}\n'
|
| 226 |
-
' return [r for r in rows if r.get("role") in valid_roles]'
|
| 227 |
-
)
|
| 228 |
-
if old_history in content:
|
| 229 |
-
content, ok = _safe_replace(content, old_history, new_history)
|
| 230 |
-
if ok:
|
| 231 |
-
print(" + session-history endpoint filters event rows")
|
| 232 |
-
changed = True
|
| 233 |
-
|
| 234 |
-
# Add catchup-done endpoint
|
| 235 |
-
if "/session/{session_id}/catchup-done" not in content:
|
| 236 |
-
catchup_endpoint = '''
|
| 237 |
-
|
| 238 |
-
@router.post("/session/{session_id}/catchup-done")
|
| 239 |
-
async def mark_catchup_done(
|
| 240 |
-
session_id: str, user: dict = Depends(get_current_user)
|
| 241 |
-
) -> dict:
|
| 242 |
-
"""Mark that a session was successfully catch-up restored.
|
| 243 |
-
|
| 244 |
-
The frontend calls this after a successful catch-up so the backend
|
| 245 |
-
can track which sessions were restored from summary vs. fresh.
|
| 246 |
-
"""
|
| 247 |
-
await _check_session_access(session_id, user, preload_sandbox=False)
|
| 248 |
-
try:
|
| 249 |
-
await session_manager.record_catchup_done(session_id)
|
| 250 |
-
except Exception as e:
|
| 251 |
-
logger.warning("record_catchup_done failed for %s: %s", session_id, e)
|
| 252 |
-
return {"status": "ok", "session_id": session_id}
|
| 253 |
-
'''
|
| 254 |
-
content = content.rstrip() + "\n" + catchup_endpoint
|
| 255 |
-
print(" + Added /session/{id}/catchup-done endpoint")
|
| 256 |
-
changed = True
|
| 257 |
-
|
| 258 |
-
if changed:
|
| 259 |
-
try:
|
| 260 |
-
ast.parse(content)
|
| 261 |
-
_write(BACKEND_ROUTES_AGENT, content)
|
| 262 |
-
print("OK: routes/agent.py patched")
|
| 263 |
-
except SyntaxError as e:
|
| 264 |
-
print(f"FAIL: routes/agent.py syntax: {e}")
|
| 265 |
-
else:
|
| 266 |
-
print("SKIP: routes/agent.py not found")
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
# ─── Frontend patches ────────────────────────────────────────────────────────
|
| 270 |
-
|
| 271 |
-
# These files are only present in the frontend-builder stage.
|
| 272 |
-
|
| 273 |
-
FRONTEND_DIR = "/source/frontend/src"
|
| 274 |
-
FE_MARKER = "// === PATCH: catch-up-v2 ==="
|
| 275 |
-
URL_MARKER = "// === PATCH: shareable-url-v2 ==="
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
def patch_frontend_file(relpath, patch_fn, label):
|
| 279 |
-
path = os.path.join(FRONTEND_DIR, relpath)
|
| 280 |
-
if not os.path.exists(path):
|
| 281 |
-
print(f"SKIP: {path} not found")
|
| 282 |
-
return
|
| 283 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 284 |
-
content = f.read()
|
| 285 |
-
content, changed = patch_fn(content, path)
|
| 286 |
-
if not changed:
|
| 287 |
-
print(f"SKIP: {path} — no changes needed ({label})")
|
| 288 |
-
return
|
| 289 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 290 |
-
f.write(content)
|
| 291 |
-
print(f"OK: {path} patched ({label})")
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
# ─── useAgentChat.ts ─────────────────────────────────────────────────────────
|
| 295 |
-
|
| 296 |
-
print("\n=== Patching useAgentChat.ts ===")
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
def patch_useAgentChat(content, _path):
|
| 300 |
-
changed = False
|
| 301 |
-
|
| 302 |
-
if FE_MARKER in content:
|
| 303 |
-
print(" SKIP: already patched")
|
| 304 |
-
return content, False
|
| 305 |
-
|
| 306 |
-
# 1. Add catch-up guard + URL sync helpers after chatActionsRef
|
| 307 |
-
ref_def = " const chatActionsRef = useRef<"
|
| 308 |
-
if ref_def in content:
|
| 309 |
-
idx = content.find(ref_def)
|
| 310 |
-
end_idx = content.find("});", idx)
|
| 311 |
-
if end_idx > 0:
|
| 312 |
-
insert_after = content.find("\n", end_idx) + 1
|
| 313 |
-
helper = '''
|
| 314 |
-
// === PATCH: catch-up-v2 (frontend) ===
|
| 315 |
-
// After catch-up (renameSession), the backend only has the summary message
|
| 316 |
-
// but the frontend has the full conversation in localStorage. These guards
|
| 317 |
-
// prevent the backend's sparse data from overwriting the frontend's rich
|
| 318 |
-
// conversation. The ml-intern-catchup-done flag is set after renameSession
|
| 319 |
-
// and cleared after the first message is sent.
|
| 320 |
-
const _catchupDone = (() => {
|
| 321 |
-
try {
|
| 322 |
-
return localStorage.getItem(`ml-intern-catchup-done:${sessionId}`) === 'true';
|
| 323 |
-
} catch {
|
| 324 |
-
return false;
|
| 325 |
-
}
|
| 326 |
-
})();
|
| 327 |
-
const _clearCatchupDone = useCallback(() => {
|
| 328 |
-
try {
|
| 329 |
-
localStorage.removeItem(`ml-intern-catchup-done:${sessionId}`);
|
| 330 |
-
} catch {}
|
| 331 |
-
}, [sessionId]);
|
| 332 |
-
'''
|
| 333 |
-
content = content[:insert_after] + helper + content[insert_after:]
|
| 334 |
-
print(" + Added catchup guard helper")
|
| 335 |
-
changed = True
|
| 336 |
-
|
| 337 |
-
# 2. Guard onRecoverMessages
|
| 338 |
-
old_recover = (
|
| 339 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 340 |
-
" if (setMsgs && uiMsgs.length >= currentMessageCount) {\n"
|
| 341 |
-
" setMsgs(uiMsgs);\n"
|
| 342 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 343 |
-
" }"
|
| 344 |
-
)
|
| 345 |
-
new_recover = (
|
| 346 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 347 |
-
" // === PATCH: catch-up-v2 (frontend) ===\n"
|
| 348 |
-
" const _feCount = chatActionsRef.current.messages.length;\n"
|
| 349 |
-
" if (setMsgs && (!_catchupDone || uiMsgs.length >= _feCount) && uiMsgs.length >= currentMessageCount) {\n"
|
| 350 |
-
" setMsgs(uiMsgs);\n"
|
| 351 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 352 |
-
" }"
|
| 353 |
-
)
|
| 354 |
-
content, ok = _safe_replace(content, old_recover, new_recover)
|
| 355 |
-
if ok:
|
| 356 |
-
print(" + Guarded onRecoverMessages")
|
| 357 |
-
changed = True
|
| 358 |
-
|
| 359 |
-
# 3. Guard refreshMessages
|
| 360 |
-
old_refresh = (
|
| 361 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 362 |
-
" if (setMsgs && uiMsgs.length > 0) {\n"
|
| 363 |
-
" setMsgs(uiMsgs);\n"
|
| 364 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 365 |
-
" }\n"
|
| 366 |
-
" return true;"
|
| 367 |
-
)
|
| 368 |
-
new_refresh = (
|
| 369 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 370 |
-
" // === PATCH: catch-up-v2 (frontend) ===\n"
|
| 371 |
-
" const _feCountR = chatActionsRef.current.messages.length;\n"
|
| 372 |
-
" if (setMsgs && (!_catchupDone || uiMsgs.length > _feCountR) && uiMsgs.length > 0) {\n"
|
| 373 |
-
" setMsgs(uiMsgs);\n"
|
| 374 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 375 |
-
" }\n"
|
| 376 |
-
" return true;"
|
| 377 |
-
)
|
| 378 |
-
content, ok = _safe_replace(content, old_refresh, new_refresh)
|
| 379 |
-
if ok:
|
| 380 |
-
print(" + Guarded refreshMessages")
|
| 381 |
-
changed = True
|
| 382 |
-
|
| 383 |
-
# 4. Guard all chat.setMessages(uiMsgs) in hydration paths (10-space indent)
|
| 384 |
-
old_hydrate = (
|
| 385 |
-
" if (uiMsgs.length > 0) {\n"
|
| 386 |
-
" chat.setMessages(uiMsgs);\n"
|
| 387 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 388 |
-
" }"
|
| 389 |
-
)
|
| 390 |
-
new_hydrate = (
|
| 391 |
-
" // === PATCH: catch-up-v2 (frontend) ===\n"
|
| 392 |
-
" const _feCountH = chatActionsRef.current.messages.length;\n"
|
| 393 |
-
" if (uiMsgs.length > 0 && (!_catchupDone || uiMsgs.length > _feCountH)) {\n"
|
| 394 |
-
" chat.setMessages(uiMsgs);\n"
|
| 395 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 396 |
-
" }"
|
| 397 |
-
)
|
| 398 |
-
count = 0
|
| 399 |
-
while old_hydrate in content:
|
| 400 |
-
content, ok = _safe_replace(content, old_hydrate, new_hydrate)
|
| 401 |
-
if ok:
|
| 402 |
-
count += 1
|
| 403 |
-
changed = True
|
| 404 |
-
else:
|
| 405 |
-
break
|
| 406 |
-
if count > 0:
|
| 407 |
-
print(f" + Guarded {count} hydration setMessages paths")
|
| 408 |
-
|
| 409 |
-
# 5. Wrap sendMessage to clear catchup flag + update URL
|
| 410 |
-
old_send = " sendMessage: chat.sendMessage,\n stop,\n status: chat.status,\n"
|
| 411 |
-
if old_send in content:
|
| 412 |
-
new_send = (
|
| 413 |
-
" sendMessage,\n"
|
| 414 |
-
" stop,\n"
|
| 415 |
-
" status: chat.status,\n"
|
| 416 |
-
)
|
| 417 |
-
# Actually, we need to insert the wrapper BEFORE the return and change sendMessage in it
|
| 418 |
-
# Find the return block
|
| 419 |
-
return_block = " return {\n messages: chat.messages,\n sendMessage: chat.sendMessage,\n stop,\n status: chat.status,\n undoLastTurn,\n"
|
| 420 |
-
if return_block in content:
|
| 421 |
-
wrapper_block = (
|
| 422 |
-
" const _sendMessageWrapped = useCallback((msg) => {\n"
|
| 423 |
-
" // === PATCH: catch-up-v2 (frontend) ===\n"
|
| 424 |
-
" _clearCatchupDone();\n"
|
| 425 |
-
" // === PATCH: shareable-url-v2 ===\n"
|
| 426 |
-
" const params = new URLSearchParams(window.location.search);\n"
|
| 427 |
-
" if (params.get('chat') !== sessionId) {\n"
|
| 428 |
-
" params.set('chat', sessionId);\n"
|
| 429 |
-
" const qs = params.toString();\n"
|
| 430 |
-
" window.history.replaceState(null, '', qs ? `${window.location.pathname}?${qs}` : window.location.pathname);\n"
|
| 431 |
-
" }\n"
|
| 432 |
-
" return chat.sendMessage(msg);\n"
|
| 433 |
-
" }, [chat, _clearCatchupDone, sessionId]);\n"
|
| 434 |
-
"\n"
|
| 435 |
-
" return {\n messages: chat.messages,\n sendMessage: _sendMessageWrapped,\n stop,\n status: chat.status,\n undoLastTurn,\n"
|
| 436 |
-
)
|
| 437 |
-
content, ok = _safe_replace(content, return_block, wrapper_block)
|
| 438 |
-
if ok:
|
| 439 |
-
print(" + Wrapped sendMessage (clear flag + URL sync)")
|
| 440 |
-
changed = True
|
| 441 |
-
else:
|
| 442 |
-
# Try the simpler pattern
|
| 443 |
-
simple_return = " return {\n messages: chat.messages,\n sendMessage: chat.sendMessage,\n"
|
| 444 |
-
if simple_return in content:
|
| 445 |
-
wrapper_block = (
|
| 446 |
-
" const _sendMessageWrapped = useCallback((msg) => {\n"
|
| 447 |
-
" // === PATCH: catch-up-v2 (frontend) ===\n"
|
| 448 |
-
" _clearCatchupDone();\n"
|
| 449 |
-
" // === PATCH: shareable-url-v2 ===\n"
|
| 450 |
-
" const params = new URLSearchParams(window.location.search);\n"
|
| 451 |
-
" if (params.get('chat') !== sessionId) {\n"
|
| 452 |
-
" params.set('chat', sessionId);\n"
|
| 453 |
-
" const qs = params.toString();\n"
|
| 454 |
-
" window.history.replaceState(null, '', qs ? `${window.location.pathname}?${qs}` : window.location.pathname);\n"
|
| 455 |
-
" }\n"
|
| 456 |
-
" return chat.sendMessage(msg);\n"
|
| 457 |
-
" }, [chat, _clearCatchupDone, sessionId]);\n"
|
| 458 |
-
"\n"
|
| 459 |
-
" return {\n messages: chat.messages,\n sendMessage: _sendMessageWrapped,\n"
|
| 460 |
-
)
|
| 461 |
-
content, ok = _safe_replace(content, simple_return, wrapper_block)
|
| 462 |
-
if ok:
|
| 463 |
-
print(" + Wrapped sendMessage (simple pattern)")
|
| 464 |
-
changed = True
|
| 465 |
-
|
| 466 |
-
if changed and "import { logger } from '@/utils/logger';" in content:
|
| 467 |
-
content = content.replace(
|
| 468 |
-
"import { logger } from '@/utils/logger';",
|
| 469 |
-
"import { logger } from '@/utils/logger';\n" + FE_MARKER + " applied",
|
| 470 |
-
1
|
| 471 |
-
)
|
| 472 |
-
|
| 473 |
-
return content, changed
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
# ─── ExpiredBanner.tsx ──────────────────────────────────────────────────────
|
| 477 |
-
|
| 478 |
-
print("\n=== Patching ExpiredBanner.tsx ===")
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
def patch_expired_banner(content, _path):
|
| 482 |
-
changed = False
|
| 483 |
-
|
| 484 |
-
if FE_MARKER in content:
|
| 485 |
-
print(" SKIP: already patched")
|
| 486 |
-
return content, False
|
| 487 |
-
|
| 488 |
-
old_success = (
|
| 489 |
-
" useAgentStore.getState().clearSessionState(sessionId);\n"
|
| 490 |
-
" renameSession(sessionId, newId);\n"
|
| 491 |
-
" if (data.model) updateSessionModel(newId, data.model);"
|
| 492 |
-
)
|
| 493 |
-
new_success = (
|
| 494 |
-
" useAgentStore.getState().clearSessionState(sessionId);\n"
|
| 495 |
-
" renameSession(sessionId, newId);\n"
|
| 496 |
-
" if (data.model) updateSessionModel(newId, data.model);\n"
|
| 497 |
-
" // === PATCH: catch-up-v2 (frontend) ===\n"
|
| 498 |
-
" // Mark session as catch-up done so hydration paths don't\n"
|
| 499 |
-
" // overwrite the full localStorage conversation with the\n"
|
| 500 |
-
" // backend's sparse summary-only data.\n"
|
| 501 |
-
" try {\n"
|
| 502 |
-
" localStorage.setItem(`ml-intern-catchup-done:${newId}`, 'true');\n"
|
| 503 |
-
" } catch {}\n"
|
| 504 |
-
" // Also notify the backend\n"
|
| 505 |
-
" try { await apiFetch(`/api/session/${newId}/catchup-done`, { method: 'POST' }); } catch {}"
|
| 506 |
-
)
|
| 507 |
-
content, ok = _safe_replace(content, old_success, new_success)
|
| 508 |
-
if ok:
|
| 509 |
-
print(" + Set catchup-done flag after successful catch-up")
|
| 510 |
-
changed = True
|
| 511 |
-
else:
|
| 512 |
-
print(" INFO: catch-up success pattern not found")
|
| 513 |
-
|
| 514 |
-
if changed and "import { logger } from '@/utils/logger';" in content:
|
| 515 |
-
content = content.replace(
|
| 516 |
-
"import { logger } from '@/utils/logger';",
|
| 517 |
-
"import { logger } from '@/utils/logger';\n" + FE_MARKER + " applied",
|
| 518 |
-
1
|
| 519 |
-
)
|
| 520 |
-
|
| 521 |
-
return content, changed
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
# ─── AppLayout.tsx ───────────────────────────────────────────────────────────
|
| 525 |
-
|
| 526 |
-
print("\n=== Patching AppLayout.tsx ===")
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
def patch_app_layout(content, _path):
|
| 530 |
-
changed = False
|
| 531 |
-
|
| 532 |
-
if URL_MARKER in content:
|
| 533 |
-
print(" SKIP: already patched")
|
| 534 |
-
return content, False
|
| 535 |
-
|
| 536 |
-
# 1. Add ShareIcon import
|
| 537 |
-
if "import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';" in content:
|
| 538 |
-
content = content.replace(
|
| 539 |
-
"import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';",
|
| 540 |
-
"import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';\n"
|
| 541 |
-
"import ShareIcon from '@mui/icons-material/Share';\n"
|
| 542 |
-
"// === PATCH: shareable-url-v2 ===",
|
| 543 |
-
1
|
| 544 |
-
)
|
| 545 |
-
print(" + Added ShareIcon import")
|
| 546 |
-
changed = True
|
| 547 |
-
|
| 548 |
-
# 2. Add URL-based session selection on mount
|
| 549 |
-
old_health = " // -- LLM health check on mount -----------------------------------------"
|
| 550 |
-
new_code = """ // === PATCH: shareable-url-v2 ===
|
| 551 |
-
// On mount, read ?chat={sessionId} from URL and auto-select that session
|
| 552 |
-
useEffect(() => {
|
| 553 |
-
const params = new URLSearchParams(window.location.search);
|
| 554 |
-
const chatParam = params.get('chat');
|
| 555 |
-
if (chatParam) {
|
| 556 |
-
const { sessions: currentSessions, switchSession } = useSessionStore.getState();
|
| 557 |
-
if (currentSessions.find((s) => s.id === chatParam)) {
|
| 558 |
-
switchSession(chatParam);
|
| 559 |
-
}
|
| 560 |
-
}
|
| 561 |
-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
| 562 |
-
|
| 563 |
-
// Update URL whenever the active session changes
|
| 564 |
-
useEffect(() => {
|
| 565 |
-
if (activeSessionId) {
|
| 566 |
-
const params = new URLSearchParams(window.location.search);
|
| 567 |
-
if (params.get('chat') !== activeSessionId) {
|
| 568 |
-
params.set('chat', activeSessionId);
|
| 569 |
-
const qs = params.toString();
|
| 570 |
-
window.history.replaceState(null, '', qs ? `${window.location.pathname}?${qs}` : window.location.pathname);
|
| 571 |
-
}
|
| 572 |
-
}
|
| 573 |
-
}, [activeSessionId]);
|
| 574 |
-
|
| 575 |
-
// -- LLM health check on mount -----------------------------------------"""
|
| 576 |
-
content, ok = _safe_replace(content, old_health, new_code)
|
| 577 |
-
if ok:
|
| 578 |
-
print(" + URL-based session selection + URL sync on active change")
|
| 579 |
-
changed = True
|
| 580 |
-
|
| 581 |
-
# 3. Add Share button to the header
|
| 582 |
-
old_avatar = (
|
| 583 |
-
" {user?.picture ? (\n"
|
| 584 |
-
" <Avatar\n"
|
| 585 |
-
" src={user.picture}\n"
|
| 586 |
-
" alt={user.username || 'User'}\n"
|
| 587 |
-
" sx={{ width: 28, height: 28, ml: 0.5 }}\n"
|
| 588 |
-
" />\n"
|
| 589 |
-
" ) : user?.username ? ("
|
| 590 |
-
)
|
| 591 |
-
new_avatar = (
|
| 592 |
-
" {activeSessionId && (\n"
|
| 593 |
-
" <Tooltip title=\"Share this chat\">\n"
|
| 594 |
-
" <IconButton\n"
|
| 595 |
-
" size=\"small\"\n"
|
| 596 |
-
" onClick={async () => {\n"
|
| 597 |
-
" const url = window.location.href;\n"
|
| 598 |
-
" if (navigator.clipboard?.writeText) {\n"
|
| 599 |
-
" await navigator.clipboard.writeText(url);\n"
|
| 600 |
-
" } else {\n"
|
| 601 |
-
" prompt('Copy this link:', url);\n"
|
| 602 |
-
" }\n"
|
| 603 |
-
" }}\n"
|
| 604 |
-
" sx={{ color: 'text.secondary', ml: 0.5 }}\n"
|
| 605 |
-
" >\n"
|
| 606 |
-
" <ShareIcon fontSize=\"small\" />\n"
|
| 607 |
-
" </IconButton>\n"
|
| 608 |
-
" </Tooltip>\n"
|
| 609 |
-
" )}\n"
|
| 610 |
-
" {user?.picture ? (\n"
|
| 611 |
-
" <Avatar\n"
|
| 612 |
-
" src={user.picture}\n"
|
| 613 |
-
" alt={user.username || 'User'}\n"
|
| 614 |
-
" sx={{ width: 28, height: 28, ml: 0.5 }}\n"
|
| 615 |
-
" />\n"
|
| 616 |
-
" ) : user?.username ? ("
|
| 617 |
-
)
|
| 618 |
-
content, ok = _safe_replace(content, old_avatar, new_avatar)
|
| 619 |
-
if ok:
|
| 620 |
-
print(" + Added Share button to header")
|
| 621 |
-
changed = True
|
| 622 |
-
|
| 623 |
-
return content, changed
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
# ─── SessionSidebar.tsx ──────────────────────────────────────────────────────
|
| 627 |
-
|
| 628 |
-
print("\n=== Patching SessionSidebar.tsx ===")
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
def patch_session_sidebar(content, _path):
|
| 632 |
-
changed = False
|
| 633 |
-
|
| 634 |
-
if URL_MARKER in content:
|
| 635 |
-
print(" SKIP: already patched")
|
| 636 |
-
return content, False
|
| 637 |
-
|
| 638 |
-
# 1. URL update after handleNewSession
|
| 639 |
-
old_create = (
|
| 640 |
-
" createSession(data.session_id, data.model);\n"
|
| 641 |
-
" void useUsageStore.getState().fetchUsage(data.session_id);\n"
|
| 642 |
-
" setPlan([]);\n"
|
| 643 |
-
" clearPanel();\n"
|
| 644 |
-
" onClose?.();"
|
| 645 |
-
)
|
| 646 |
-
new_create = (
|
| 647 |
-
" createSession(data.session_id, data.model);\n"
|
| 648 |
-
" void useUsageStore.getState().fetchUsage(data.session_id);\n"
|
| 649 |
-
" setPlan([]);\n"
|
| 650 |
-
" clearPanel();\n"
|
| 651 |
-
" // === PATCH: shareable-url-v2 ===\n"
|
| 652 |
-
" const _params = new URLSearchParams(window.location.search);\n"
|
| 653 |
-
" _params.set('chat', data.session_id);\n"
|
| 654 |
-
" const _qs = _params.toString();\n"
|
| 655 |
-
" window.history.replaceState(null, '', _qs ? `${window.location.pathname}?${_qs}` : window.location.pathname);\n"
|
| 656 |
-
" onClose?.();"
|
| 657 |
-
)
|
| 658 |
-
content, ok = _safe_replace(content, old_create, new_create)
|
| 659 |
-
if ok:
|
| 660 |
-
print(" + URL update on new session")
|
| 661 |
-
changed = True
|
| 662 |
-
|
| 663 |
-
# 2. URL update on second new session (delete-last case)
|
| 664 |
-
old_create2 = (
|
| 665 |
-
" createSession(data.session_id, data.model);\n"
|
| 666 |
-
" void useUsageStore.getState().fetchUsage(data.session_id);\n"
|
| 667 |
-
" setPlan([]);\n"
|
| 668 |
-
" clearPanel();"
|
| 669 |
-
)
|
| 670 |
-
new_create2 = (
|
| 671 |
-
" createSession(data.session_id, data.model);\n"
|
| 672 |
-
" void useUsageStore.getState().fetchUsage(data.session_id);\n"
|
| 673 |
-
" setPlan([]);\n"
|
| 674 |
-
" clearPanel();\n"
|
| 675 |
-
" // === PATCH: shareable-url-v2 ===\n"
|
| 676 |
-
" const _params2 = new URLSearchParams(window.location.search);\n"
|
| 677 |
-
" _params2.set('chat', data.session_id);\n"
|
| 678 |
-
" const _qs2 = _params2.toString();\n"
|
| 679 |
-
" window.history.replaceState(null, '', _qs2 ? `${window.location.pathname}?${_qs2}` : window.location.pathname);"
|
| 680 |
-
)
|
| 681 |
-
content, ok = _safe_replace(content, old_create2, new_create2)
|
| 682 |
-
if ok:
|
| 683 |
-
print(" + URL update on new session (delete-last case)")
|
| 684 |
-
changed = True
|
| 685 |
-
|
| 686 |
-
# 3. URL update on session select
|
| 687 |
-
old_select = (
|
| 688 |
-
" const shouldActivateSession = sessionId !== activeSessionId;\n"
|
| 689 |
-
" switchSession(sessionId);"
|
| 690 |
-
)
|
| 691 |
-
new_select = (
|
| 692 |
-
" const shouldActivateSession = sessionId !== activeSessionId;\n"
|
| 693 |
-
" switchSession(sessionId);\n"
|
| 694 |
-
" // === PATCH: shareable-url-v2 ===\n"
|
| 695 |
-
" const _p = new URLSearchParams(window.location.search);\n"
|
| 696 |
-
" if (_p.get('chat') !== sessionId) {\n"
|
| 697 |
-
" _p.set('chat', sessionId);\n"
|
| 698 |
-
" const _q = _p.toString();\n"
|
| 699 |
-
" window.history.replaceState(null, '', _q ? `${window.location.pathname}?${_q}` : window.location.pathname);\n"
|
| 700 |
-
" }"
|
| 701 |
-
)
|
| 702 |
-
content, ok = _safe_replace(content, old_select, new_select)
|
| 703 |
-
if ok:
|
| 704 |
-
print(" + URL update on session select")
|
| 705 |
-
changed = True
|
| 706 |
-
|
| 707 |
-
return content, changed
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
# ─── WelcomeScreen.tsx ───────────────────────────────────────────────────────
|
| 711 |
-
|
| 712 |
-
print("\n=== Patching WelcomeScreen.tsx ===")
|
| 713 |
-
|
| 714 |
-
WS_PATH = os.path.join(FRONTEND_DIR, "components/WelcomeScreen/WelcomeScreen.tsx")
|
| 715 |
-
if os.path.exists(WS_PATH):
|
| 716 |
-
with open(WS_PATH, "r", encoding="utf-8") as f:
|
| 717 |
-
ws_content = f.read()
|
| 718 |
-
if URL_MARKER not in ws_content:
|
| 719 |
-
old_ws = (
|
| 720 |
-
" createSession(data.session_id, data.model);\n"
|
| 721 |
-
" void useUsageStore.getState().fetchUsage(data.session_id);\n"
|
| 722 |
-
" setPlan([]);\n"
|
| 723 |
-
" clearPanel();\n"
|
| 724 |
-
" } catch"
|
| 725 |
-
)
|
| 726 |
-
new_ws = (
|
| 727 |
-
" createSession(data.session_id, data.model);\n"
|
| 728 |
-
" void useUsageStore.getState().fetchUsage(data.session_id);\n"
|
| 729 |
-
" setPlan([]);\n"
|
| 730 |
-
" clearPanel();\n"
|
| 731 |
-
" // === PATCH: shareable-url-v2 ===\n"
|
| 732 |
-
" const _params = new URLSearchParams(window.location.search);\n"
|
| 733 |
-
" _params.set('chat', data.session_id);\n"
|
| 734 |
-
" const _qs = _params.toString();\n"
|
| 735 |
-
" window.history.replaceState(null, '', _qs ? `${window.location.pathname}?${_qs}` : window.location.pathname);\n"
|
| 736 |
-
" } catch"
|
| 737 |
-
)
|
| 738 |
-
if old_ws in ws_content:
|
| 739 |
-
ws_content = ws_content.replace(old_ws, new_ws, 1)
|
| 740 |
-
with open(WS_PATH, "w", encoding="utf-8") as f:
|
| 741 |
-
f.write(ws_content)
|
| 742 |
-
print(" + URL update on welcome screen session creation")
|
| 743 |
-
else:
|
| 744 |
-
print(" INFO: welcome screen pattern not found")
|
| 745 |
-
else:
|
| 746 |
-
print(" SKIP: already patched")
|
| 747 |
-
else:
|
| 748 |
-
print("SKIP: WelcomeScreen.tsx not found")
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
# ─── Execute frontend patches ─────────────────────────────────────────────────
|
| 752 |
-
|
| 753 |
-
patch_frontend_file("hooks/useAgentChat.ts", patch_useAgentChat, "catch-up guard + URL sync")
|
| 754 |
-
patch_frontend_file("components/Chat/ExpiredBanner.tsx", patch_expired_banner, "catchup-done flag")
|
| 755 |
-
patch_frontend_file("components/Layout/AppLayout.tsx", patch_app_layout, "URL session selection + share button")
|
| 756 |
-
patch_frontend_file("components/SessionSidebar/SessionSidebar.tsx", patch_session_sidebar, "URL update on session actions")
|
| 757 |
-
|
| 758 |
-
print("\n=== All v2 patches applied ===")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_catch_up_fix_v3.py
DELETED
|
@@ -1,675 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Patch v3: Fix the frontend build + complete catch-up & shareable-URL fixes.
|
| 4 |
-
|
| 5 |
-
Build failure root cause (v2):
|
| 6 |
-
v2's patch_catch_up_fix_v2.py introduced THREE TypeScript build errors
|
| 7 |
-
because its inner patterns were already modified by earlier patches
|
| 8 |
-
(patch_catch_up_fix_frontend.py ran first and changed the same code):
|
| 9 |
-
|
| 10 |
-
src/components/Layout/AppLayout.tsx(20,1): error TS6133:
|
| 11 |
-
'ShareIcon' is declared but its value is never read. <- share button patch FAILED to match
|
| 12 |
-
src/hooks/useAgentChat.ts(572,9): error TS6133:
|
| 13 |
-
'_catchupDone' is declared but its value is never read. <- guard patches FAILED to match
|
| 14 |
-
src/hooks/useAgentChat.ts(1174,44): error TS7006:
|
| 15 |
-
Parameter 'msg' implicitly has an 'any' type. <- wrapper typed poorly
|
| 16 |
-
|
| 17 |
-
Because npm run build fails, /app/static ends up EMPTY and the Space
|
| 18 |
-
serves NO frontend. All user-visible symptoms (catch-up errors, context
|
| 19 |
-
loss, no share URL) trace back to this.
|
| 20 |
-
|
| 21 |
-
v3 fixes (runs AFTER all other patches, idempotent):
|
| 22 |
-
1. useAgentChat.ts
|
| 23 |
-
a. Wire _catchupDone into the existing v1 guards so the flag is used
|
| 24 |
-
(kills TS6133) AND the "backend sparse summary overwrites full
|
| 25 |
-
local conversation" race is actually prevented.
|
| 26 |
-
b. Type _sendMessageWrapped's msg parameter using the same type as
|
| 27 |
-
chat.sendMessage (kills TS7006).
|
| 28 |
-
2. AppLayout.tsx
|
| 29 |
-
a. Render the missing Share button in the header (kills TS6133 for
|
| 30 |
-
ShareIcon). The button copies a PUBLIC share URL (?share=<id>).
|
| 31 |
-
b. If the URL has ?share=<sessionId>, render a read-only ShareView
|
| 32 |
-
instead of the chat (auth-free, works for anyone with the link).
|
| 33 |
-
3. ShareView.tsx (new file): read-only transcript viewer.
|
| 34 |
-
4. Backend routes/agent.py: add public GET /api/public/session/{id}
|
| 35 |
-
(no auth) that serves sanitized conversation rows from the permanent
|
| 36 |
-
public sessions dataset.
|
| 37 |
-
5. Backend session_logger.py: fix v2's indent bug (insert filter
|
| 38 |
-
correctly so no SyntaxError) — event rows filtered at source.
|
| 39 |
-
|
| 40 |
-
Never fails the build (all writes guarded by markers / ast.parse, and the
|
| 41 |
-
Dockerfile already appends `|| true`).
|
| 42 |
-
"""
|
| 43 |
-
|
| 44 |
-
import ast
|
| 45 |
-
import os
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def _read(path):
|
| 49 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 50 |
-
return f.read()
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def _write(path, content):
|
| 54 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 55 |
-
f.write(content)
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def _replace(content, old, new):
|
| 59 |
-
if old in content:
|
| 60 |
-
return content.replace(old, new, 1), True
|
| 61 |
-
return content, False
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
MARKER3 = "// === PATCH: catch-up-v3 ==="
|
| 65 |
-
MARKER3_PY = "# === PATCH: catch-up-v3 ==="
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def patch_frontend_file(relpath, patch_fn, label):
|
| 69 |
-
path = os.path.join(FRONTEND_DIR, relpath)
|
| 70 |
-
if not os.path.exists(path):
|
| 71 |
-
print(f"SKIP: {path} not found ({label})")
|
| 72 |
-
return
|
| 73 |
-
content = _read(path)
|
| 74 |
-
content, changed = patch_fn(content, path)
|
| 75 |
-
if not changed:
|
| 76 |
-
print(f"SKIP: {path} — no changes needed ({label})")
|
| 77 |
-
return
|
| 78 |
-
_write(path, content)
|
| 79 |
-
print(f"OK: {path} patched ({label})")
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
# ─── Frontend files ─────────────────────────────────────────────────────────
|
| 83 |
-
|
| 84 |
-
FRONTEND_DIR = "/source/frontend/src"
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
def patch_use_agent_chat(content, _path):
|
| 88 |
-
"""Wire _catchupDone into guards + type the sendMessage wrapper."""
|
| 89 |
-
changed = False
|
| 90 |
-
if MARKER3 in content:
|
| 91 |
-
print(" SKIP: already patched")
|
| 92 |
-
return content, False
|
| 93 |
-
|
| 94 |
-
# (1a) onRecoverMessages guard (v1 already present):
|
| 95 |
-
old_recover = (
|
| 96 |
-
" const _shouldOverwrite = uiMsgs.length > _currentNonSystem.length || _currentNonSystem.length === 0;\n"
|
| 97 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 98 |
-
" if (setMsgs && _shouldOverwrite) {"
|
| 99 |
-
)
|
| 100 |
-
new_recover = (
|
| 101 |
-
" // === PATCH: catch-up-v3 ===\n"
|
| 102 |
-
" // After catch-up the backend only has the summary message;\n"
|
| 103 |
-
" // never let it clobber the full local conversation.\n"
|
| 104 |
-
" const _shouldOverwrite = _catchupDone\n"
|
| 105 |
-
" ? false\n"
|
| 106 |
-
" : (uiMsgs.length > _currentNonSystem.length || _currentNonSystem.length === 0);\n"
|
| 107 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 108 |
-
" if (setMsgs && _shouldOverwrite) {"
|
| 109 |
-
)
|
| 110 |
-
content, ok = _replace(content, old_recover, new_recover)
|
| 111 |
-
if ok:
|
| 112 |
-
print(" + onRecoverMessages guard honors _catchupDone")
|
| 113 |
-
changed = True
|
| 114 |
-
else:
|
| 115 |
-
print(" INFO: onRecoverMessages guard pattern not found (may already be v3)")
|
| 116 |
-
|
| 117 |
-
# refreshMessages guard (v1 already present):
|
| 118 |
-
old_refresh = (
|
| 119 |
-
" const _shouldOverwrite2 = uiMsgs.length > _currentNonSystem2.length || _currentNonSystem2.length === 0;\n"
|
| 120 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 121 |
-
" if (setMsgs && _shouldOverwrite2) {"
|
| 122 |
-
)
|
| 123 |
-
new_refresh = (
|
| 124 |
-
" // === PATCH: catch-up-v3 ===\n"
|
| 125 |
-
" const _shouldOverwrite2 = _catchupDone\n"
|
| 126 |
-
" ? false\n"
|
| 127 |
-
" : (uiMsgs.length > _currentNonSystem2.length || _currentNonSystem2.length === 0);\n"
|
| 128 |
-
" const setMsgs = chatActionsRef.current.setMessages;\n"
|
| 129 |
-
" if (setMsgs && _shouldOverwrite2) {"
|
| 130 |
-
)
|
| 131 |
-
content, ok = _replace(content, old_refresh, new_refresh)
|
| 132 |
-
if ok:
|
| 133 |
-
print(" + refreshMessages guard honors _catchupDone")
|
| 134 |
-
changed = True
|
| 135 |
-
else:
|
| 136 |
-
print(" INFO: refreshMessages guard pattern not found (may already be v3)")
|
| 137 |
-
|
| 138 |
-
# (1b) Type the sendMessage wrapper param using chat.sendMessage's type.
|
| 139 |
-
old_wrapper = " const _sendMessageWrapped = useCallback((msg) => {"
|
| 140 |
-
new_wrapper = (
|
| 141 |
-
" // === PATCH: catch-up-v3 ===\n"
|
| 142 |
-
" type _ChatSendArgs = Parameters<typeof chat.sendMessage>[0];\n"
|
| 143 |
-
" const _sendMessageWrapped = useCallback((msg: _ChatSendArgs) => {"
|
| 144 |
-
)
|
| 145 |
-
content, ok = _replace(content, old_wrapper, new_wrapper)
|
| 146 |
-
if ok:
|
| 147 |
-
print(" + Typed _sendMessageWrapped msg parameter")
|
| 148 |
-
changed = True
|
| 149 |
-
else:
|
| 150 |
-
print(" INFO: wrapper pattern not found (may already be v3)")
|
| 151 |
-
|
| 152 |
-
# (1c) Strengthen the remaining v1 count-based hydration guards
|
| 153 |
-
# (_shouldSetB 10-space, _shouldSetC 14-space x2, _shouldSet 6-space).
|
| 154 |
-
guard_rules = [
|
| 155 |
-
(
|
| 156 |
-
" const _shouldSetB = uiMsgs.length > _currentMsgsB.length || _currentMsgsB.length === 0;\n"
|
| 157 |
-
" if (_shouldSetB) {",
|
| 158 |
-
" // === PATCH: catch-up-v3 ===\n"
|
| 159 |
-
" const _shouldSetB = _catchupDone\n"
|
| 160 |
-
" ? false\n"
|
| 161 |
-
" : (uiMsgs.length > _currentMsgsB.length || _currentMsgsB.length === 0);\n"
|
| 162 |
-
" if (_shouldSetB) {",
|
| 163 |
-
),
|
| 164 |
-
(
|
| 165 |
-
" const _shouldSetC = uiMsgs.length > _currentMsgsC.length || _currentMsgsC.length === 0;\n"
|
| 166 |
-
" if (_shouldSetC) {",
|
| 167 |
-
" // === PATCH: catch-up-v3 ===\n"
|
| 168 |
-
" const _shouldSetC = _catchupDone\n"
|
| 169 |
-
" ? false\n"
|
| 170 |
-
" : (uiMsgs.length > _currentMsgsC.length || _currentMsgsC.length === 0);\n"
|
| 171 |
-
" if (_shouldSetC) {",
|
| 172 |
-
),
|
| 173 |
-
(
|
| 174 |
-
" const _shouldSet = uiMsgs.length > _currentMsgs.length || _currentMsgs.length === 0;\n"
|
| 175 |
-
" if (_shouldSet) {",
|
| 176 |
-
" // === PATCH: catch-up-v3 ===\n"
|
| 177 |
-
" const _shouldSet = _catchupDone\n"
|
| 178 |
-
" ? false\n"
|
| 179 |
-
" : (uiMsgs.length > _currentMsgs.length || _currentMsgs.length === 0);\n"
|
| 180 |
-
" if (_shouldSet) {",
|
| 181 |
-
),
|
| 182 |
-
]
|
| 183 |
-
for old_g, new_g in guard_rules:
|
| 184 |
-
n = content.count(old_g)
|
| 185 |
-
if n:
|
| 186 |
-
content = content.replace(old_g, new_g)
|
| 187 |
-
print(f" + _catchupDone wired into hydration guard ({n} site(s))")
|
| 188 |
-
changed = True
|
| 189 |
-
|
| 190 |
-
# (1d) Extra safety: guard any remaining bare setMessages path in the
|
| 191 |
-
# event-stream consume / visibility handlers that v1 did not touch.
|
| 192 |
-
old_consume = (
|
| 193 |
-
" if (uiMsgs.length > 0) {\n"
|
| 194 |
-
" chat.setMessages(uiMsgs);\n"
|
| 195 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 196 |
-
" }"
|
| 197 |
-
)
|
| 198 |
-
new_consume = (
|
| 199 |
-
" // === PATCH: catch-up-v3 ===\n"
|
| 200 |
-
" const _c3a = (chatActionsRef.current.messages || []).filter(\n"
|
| 201 |
-
" (m) => !(m.parts?.[0]?.type === 'text' && typeof m.parts?.[0]?.text === 'string' && m.parts?.[0].text.trimStart().startsWith('[SYSTEM:'))\n"
|
| 202 |
-
" );\n"
|
| 203 |
-
" if (uiMsgs.length > 0 && (!_catchupDone || uiMsgs.length > _c3a.length)) {\n"
|
| 204 |
-
" chat.setMessages(uiMsgs);\n"
|
| 205 |
-
" saveMessages(sessionId, uiMsgs);\n"
|
| 206 |
-
" }"
|
| 207 |
-
)
|
| 208 |
-
count = 0
|
| 209 |
-
while old_consume in content:
|
| 210 |
-
content, ok = _replace(content, old_consume, new_consume)
|
| 211 |
-
if ok:
|
| 212 |
-
count += 1
|
| 213 |
-
changed = True
|
| 214 |
-
else:
|
| 215 |
-
break
|
| 216 |
-
if count:
|
| 217 |
-
print(f" + Guarded {count} event-stream/visibility setMessages paths")
|
| 218 |
-
|
| 219 |
-
return content, changed
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
def patch_app_layout(content, _path):
|
| 223 |
-
"""Share button (public ?share= URL) + read-only ShareView mode."""
|
| 224 |
-
changed = False
|
| 225 |
-
if MARKER3 in content:
|
| 226 |
-
print(" SKIP: already patched")
|
| 227 |
-
return content, False
|
| 228 |
-
|
| 229 |
-
# (2a) Import ShareView (new component in the same folder).
|
| 230 |
-
if "import ShareView" not in content:
|
| 231 |
-
anchor = "import ShareIcon from '@mui/icons-material/Share';"
|
| 232 |
-
if anchor in content:
|
| 233 |
-
content = content.replace(
|
| 234 |
-
anchor,
|
| 235 |
-
anchor + "\nimport ShareView from '@/components/ShareView';\n" + MARKER3,
|
| 236 |
-
1,
|
| 237 |
-
)
|
| 238 |
-
changed = True
|
| 239 |
-
print(" + Imported ShareView")
|
| 240 |
-
else:
|
| 241 |
-
print(" INFO: ShareIcon import not found for ShareView import")
|
| 242 |
-
|
| 243 |
-
# (2b) Share button before theme toggle. Builds a PUBLIC share URL
|
| 244 |
-
# (?share=<sessionId>) instead of the owner-only ?chat= URL.
|
| 245 |
-
old_theme_btn = (
|
| 246 |
-
" <IconButton\n"
|
| 247 |
-
" onClick={toggleTheme}\n"
|
| 248 |
-
" size=\"small\"\n"
|
| 249 |
-
" sx={{\n"
|
| 250 |
-
" color: 'text.secondary',\n"
|
| 251 |
-
" '&:hover': { color: 'primary.main' },\n"
|
| 252 |
-
" }}\n"
|
| 253 |
-
" >\n"
|
| 254 |
-
" {themeMode === 'dark' ? <LightModeOutlinedIcon fontSize=\"small\" /> : <DarkModeOutlinedIcon fontSize=\"small\" />}\n"
|
| 255 |
-
" </IconButton>"
|
| 256 |
-
)
|
| 257 |
-
new_theme_btn = (
|
| 258 |
-
" <IconButton\n"
|
| 259 |
-
" onClick={toggleTheme}\n"
|
| 260 |
-
" size=\"small\"\n"
|
| 261 |
-
" sx={{\n"
|
| 262 |
-
" color: 'text.secondary',\n"
|
| 263 |
-
" '&:hover': { color: 'primary.main' },\n"
|
| 264 |
-
" }}\n"
|
| 265 |
-
" >\n"
|
| 266 |
-
" {themeMode === 'dark' ? <LightModeOutlinedIcon fontSize=\"small\" /> : <DarkModeOutlinedIcon fontSize=\"small\" />}\n"
|
| 267 |
-
" </IconButton>\n"
|
| 268 |
-
"\n"
|
| 269 |
-
" {activeSessionId && (\n"
|
| 270 |
-
" <Tooltip title=\"Copy public share link\">\n"
|
| 271 |
-
" <IconButton\n"
|
| 272 |
-
" size=\"small\"\n"
|
| 273 |
-
" onClick={(e: React.MouseEvent) => {\n"
|
| 274 |
-
" e.stopPropagation();\n"
|
| 275 |
-
" const _p = new URLSearchParams(window.location.search);\n"
|
| 276 |
-
" const _sid = _p.get('chat') || activeSessionId || '';\n"
|
| 277 |
-
" const _sp = new URLSearchParams();\n"
|
| 278 |
-
" if (_sid) _sp.set('share', _sid);\n"
|
| 279 |
-
" const _qs = _sp.toString();\n"
|
| 280 |
-
" const url = `${window.location.origin}${window.location.pathname}${_qs ? `?${_qs}` : ''}`;\n"
|
| 281 |
-
" if (navigator.clipboard?.writeText) {\n"
|
| 282 |
-
" navigator.clipboard.writeText(url).catch(() => {\n"
|
| 283 |
-
" prompt('Copy this link:', url);\n"
|
| 284 |
-
" });\n"
|
| 285 |
-
" } else {\n"
|
| 286 |
-
" prompt('Copy this link:', url);\n"
|
| 287 |
-
" }\n"
|
| 288 |
-
" }}\n"
|
| 289 |
-
" sx={{\n"
|
| 290 |
-
" color: 'text.secondary',\n"
|
| 291 |
-
" '&:hover': { color: 'primary.main' },\n"
|
| 292 |
-
" }}\n"
|
| 293 |
-
" >\n"
|
| 294 |
-
" <ShareIcon fontSize=\"small\" />\n"
|
| 295 |
-
" </IconButton>\n"
|
| 296 |
-
" </Tooltip>\n"
|
| 297 |
-
" )}"
|
| 298 |
-
)
|
| 299 |
-
content, ok = _replace(content, old_theme_btn, new_theme_btn)
|
| 300 |
-
if ok:
|
| 301 |
-
changed = True
|
| 302 |
-
print(" + Added Share button before theme toggle (public ?share= URL)")
|
| 303 |
-
else:
|
| 304 |
-
print(" INFO: theme toggle pattern not found (may already be v3)")
|
| 305 |
-
|
| 306 |
-
# Ensure Tooltip is imported (used by the Share button).
|
| 307 |
-
if "Tooltip," not in content and "import Tooltip" not in content:
|
| 308 |
-
if "import {\n Avatar," in content:
|
| 309 |
-
content = content.replace(
|
| 310 |
-
"import {\n Avatar,",
|
| 311 |
-
"import {\n Tooltip,\n Avatar,",
|
| 312 |
-
1,
|
| 313 |
-
)
|
| 314 |
-
changed = True
|
| 315 |
-
print(" + Added Tooltip import")
|
| 316 |
-
elif "Avatar," in content:
|
| 317 |
-
content = content.replace(" Avatar,", " Tooltip,\n Avatar,", 1)
|
| 318 |
-
changed = True
|
| 319 |
-
print(" + Added Tooltip import (fallback)")
|
| 320 |
-
else:
|
| 321 |
-
print(" INFO: could not add Tooltip import (Avatar import not found)")
|
| 322 |
-
|
| 323 |
-
# (2c) Read-only share mode: if ?share=<id> is present, render ShareView.
|
| 324 |
-
# Must come AFTER all hooks (it is — right before the WelcomeScreen return).
|
| 325 |
-
old_welcome = " // -- Welcome screen: no sessions at all ---------------------------------\n"
|
| 326 |
-
new_welcome = (
|
| 327 |
-
" // === PATCH: catch-up-v3 ===\n"
|
| 328 |
-
" // Public read-only share mode (?share=<sessionId>). Works without\n"
|
| 329 |
-
" // login for anyone who has the link; data comes from the public\n"
|
| 330 |
-
" // sessions dataset via GET /api/public/session/{id}.\n"
|
| 331 |
-
" const _shareParam = new URLSearchParams(window.location.search).get('share');\n"
|
| 332 |
-
" if (_shareParam) {\n"
|
| 333 |
-
" return <ShareView sessionId={_shareParam} />;\n"
|
| 334 |
-
" }\n"
|
| 335 |
-
"\n"
|
| 336 |
-
" // -- Welcome screen: no sessions at all ---------------------------------\n"
|
| 337 |
-
)
|
| 338 |
-
content, ok = _replace(content, old_welcome, new_welcome)
|
| 339 |
-
if ok:
|
| 340 |
-
changed = True
|
| 341 |
-
print(" + ?share= renders read-only ShareView")
|
| 342 |
-
else:
|
| 343 |
-
print(" INFO: welcome-screen anchor not found for share mode")
|
| 344 |
-
|
| 345 |
-
return content, changed
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
def patch_share_view(path):
|
| 349 |
-
"""Create the read-only ShareView component (new file)."""
|
| 350 |
-
if os.path.exists(path):
|
| 351 |
-
print(" SKIP: ShareView.tsx already exists")
|
| 352 |
-
return
|
| 353 |
-
content = (
|
| 354 |
-
"/**\n"
|
| 355 |
-
" * Read-only public session viewer (no auth required).\n"
|
| 356 |
-
" *\n"
|
| 357 |
-
" * Rendered by AppLayout when the URL contains ?share=<sessionId>.\n"
|
| 358 |
-
" * Fetches sanitized conversation rows from the public endpoint\n"
|
| 359 |
-
" * GET /api/public/session/{id}, which reads the permanent public\n"
|
| 360 |
-
" * sessions dataset. Anyone with the link can view the transcript.\n"
|
| 361 |
-
" */\n"
|
| 362 |
-
"// === PATCH: catch-up-v3 ===\n"
|
| 363 |
-
"import { useEffect, useState } from 'react';\n"
|
| 364 |
-
"import {\n"
|
| 365 |
-
" Alert,\n"
|
| 366 |
-
" Box,\n"
|
| 367 |
-
" Button,\n"
|
| 368 |
-
" CircularProgress,\n"
|
| 369 |
-
" Paper,\n"
|
| 370 |
-
" Typography,\n"
|
| 371 |
-
"} from '@mui/material';\n"
|
| 372 |
-
"\n"
|
| 373 |
-
"interface ShareRow {\n"
|
| 374 |
-
" role: string;\n"
|
| 375 |
-
" content: string;\n"
|
| 376 |
-
" timestamp?: string;\n"
|
| 377 |
-
"}\n"
|
| 378 |
-
"\n"
|
| 379 |
-
"interface ShareViewProps {\n"
|
| 380 |
-
" sessionId: string;\n"
|
| 381 |
-
"}\n"
|
| 382 |
-
"\n"
|
| 383 |
-
"export default function ShareView({ sessionId }: ShareViewProps) {\n"
|
| 384 |
-
" const [rows, setRows] = useState<ShareRow[]>([]);\n"
|
| 385 |
-
" const [loading, setLoading] = useState(true);\n"
|
| 386 |
-
" const [error, setError] = useState<string | null>(null);\n"
|
| 387 |
-
"\n"
|
| 388 |
-
" useEffect(() => {\n"
|
| 389 |
-
" let cancelled = false;\n"
|
| 390 |
-
" (async () => {\n"
|
| 391 |
-
" try {\n"
|
| 392 |
-
" const res = await fetch(\n"
|
| 393 |
-
" `/api/public/session/${encodeURIComponent(sessionId)}`,\n"
|
| 394 |
-
" { credentials: 'include' },\n"
|
| 395 |
-
" );\n"
|
| 396 |
-
" if (res.status === 404) {\n"
|
| 397 |
-
" if (!cancelled) setError('Session not found.');\n"
|
| 398 |
-
" } else if (!res.ok) {\n"
|
| 399 |
-
" if (!cancelled) setError('Failed to load this session.');\n"
|
| 400 |
-
" } else {\n"
|
| 401 |
-
" const data = await res.json();\n"
|
| 402 |
-
" if (!cancelled && Array.isArray(data)) setRows(data);\n"
|
| 403 |
-
" }\n"
|
| 404 |
-
" } catch {\n"
|
| 405 |
-
" if (!cancelled) setError('Network error while loading this session.');\n"
|
| 406 |
-
" } finally {\n"
|
| 407 |
-
" if (!cancelled) setLoading(false);\n"
|
| 408 |
-
" }\n"
|
| 409 |
-
" })();\n"
|
| 410 |
-
" return () => {\n"
|
| 411 |
-
" cancelled = true;\n"
|
| 412 |
-
" };\n"
|
| 413 |
-
" }, [sessionId]);\n"
|
| 414 |
-
"\n"
|
| 415 |
-
" return (\n"
|
| 416 |
-
" <Box\n"
|
| 417 |
-
" sx={{\n"
|
| 418 |
-
" minHeight: '100vh',\n"
|
| 419 |
-
" bgcolor: 'var(--body-gradient)' as string,\n"
|
| 420 |
-
" py: 4,\n"
|
| 421 |
-
" px: { xs: 2, md: 6 },\n"
|
| 422 |
-
" }}\n"
|
| 423 |
-
" >\n"
|
| 424 |
-
" <Box sx={{ maxWidth: 860, mx: 'auto' }}>\n"
|
| 425 |
-
" <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2 }}>\n"
|
| 426 |
-
" <Typography variant=\"h6\" sx={{ fontSize: '1rem', fontWeight: 700 }}>\n"
|
| 427 |
-
" Shared session\n"
|
| 428 |
-
" </Typography>\n"
|
| 429 |
-
" <Button component=\"a\" href=\"/\" size=\"small\" sx={{ textTransform: 'none', ml: 'auto' }}>\n"
|
| 430 |
-
" Open ML Intern\n"
|
| 431 |
-
" </Button>\n"
|
| 432 |
-
" </Box>\n"
|
| 433 |
-
"\n"
|
| 434 |
-
" {loading && <CircularProgress size={20} />}\n"
|
| 435 |
-
" {error && (\n"
|
| 436 |
-
" <Alert severity=\"warning\" variant=\"outlined\">\n"
|
| 437 |
-
" {error}\n"
|
| 438 |
-
" </Alert>\n"
|
| 439 |
-
" )}\n"
|
| 440 |
-
" {!loading && !error && rows.length === 0 && (\n"
|
| 441 |
-
" <Typography color=\"text.secondary\" variant=\"body2\">\n"
|
| 442 |
-
" No messages in this session.\n"
|
| 443 |
-
" </Typography>\n"
|
| 444 |
-
" )}\n"
|
| 445 |
-
" {rows.map((row, i) => (\n"
|
| 446 |
-
" <Paper\n"
|
| 447 |
-
" key={i}\n"
|
| 448 |
-
" sx={{\n"
|
| 449 |
-
" mb: 1,\n"
|
| 450 |
-
" p: 1.5,\n"
|
| 451 |
-
" bgcolor: row.role === 'user' ? 'var(--hover-bg)' : 'var(--panel)',\n"
|
| 452 |
-
" borderRadius: 2,\n"
|
| 453 |
-
" }}\n"
|
| 454 |
-
" >\n"
|
| 455 |
-
" <Typography\n"
|
| 456 |
-
" variant=\"caption\"\n"
|
| 457 |
-
" sx={{ color: 'var(--muted-text)', fontWeight: 700, textTransform: 'uppercase' }}\n"
|
| 458 |
-
" >\n"
|
| 459 |
-
" {row.role}\n"
|
| 460 |
-
" </Typography>\n"
|
| 461 |
-
" <Typography\n"
|
| 462 |
-
" variant=\"body2\"\n"
|
| 463 |
-
" sx={{ whiteSpace: 'pre-wrap', mt: 0.5, wordBreak: 'break-word' }}\n"
|
| 464 |
-
" >\n"
|
| 465 |
-
" {row.content}\n"
|
| 466 |
-
" </Typography>\n"
|
| 467 |
-
" </Paper>\n"
|
| 468 |
-
" ))}\n"
|
| 469 |
-
" </Box>\n"
|
| 470 |
-
" </Box>\n"
|
| 471 |
-
" );\n"
|
| 472 |
-
"}\n"
|
| 473 |
-
)
|
| 474 |
-
_write(path, content)
|
| 475 |
-
print(" + Created ShareView.tsx")
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
def patch_session_sidebar(content, _path):
|
| 479 |
-
changed = False
|
| 480 |
-
if MARKER3 in content:
|
| 481 |
-
return content, False
|
| 482 |
-
return content, changed
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
def patch_expired_banner(content, _path):
|
| 486 |
-
changed = False
|
| 487 |
-
if MARKER3 in content:
|
| 488 |
-
return content, False
|
| 489 |
-
return content, changed
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
def patch_welcome_screen(content, _path):
|
| 493 |
-
changed = False
|
| 494 |
-
if MARKER3 in content:
|
| 495 |
-
return content, False
|
| 496 |
-
return content, changed
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
# ─── Backend routes/agent.py: public share endpoint ───────────────────────
|
| 500 |
-
|
| 501 |
-
BACKEND_ROUTES_AGENT = "/app/backend/routes/agent.py"
|
| 502 |
-
BACKEND_SESSION_LOGGER = "/app/backend/session_logger.py"
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
def patch_routes_agent(content):
|
| 506 |
-
"""Add public GET /api/public/session/{id} (no auth)."""
|
| 507 |
-
changed = False
|
| 508 |
-
if "public/session/" in content:
|
| 509 |
-
print(" SKIP: public endpoint already present")
|
| 510 |
-
return content, False
|
| 511 |
-
|
| 512 |
-
endpoint = '''
|
| 513 |
-
|
| 514 |
-
@router.get("/public/session/{session_id}")
|
| 515 |
-
async def public_session_transcript(session_id: str) -> list[dict]:
|
| 516 |
-
"""Public read-only transcript for a session (no auth required).
|
| 517 |
-
|
| 518 |
-
Reads sanitized conversation rows from the permanent public sessions
|
| 519 |
-
dataset (bep40/ml-intern-sessions). Returns only user/assistant/tool
|
| 520 |
-
messages — never internal event/system rows. Useful for sharing a
|
| 521 |
-
session transcript with anyone via a link.
|
| 522 |
-
"""
|
| 523 |
-
from backend.session_logger import load_session_history
|
| 524 |
-
|
| 525 |
-
rows = load_session_history(session_id)
|
| 526 |
-
valid_roles = {"user", "assistant", "tool"}
|
| 527 |
-
messages = [r for r in rows if r.get("role") in valid_roles]
|
| 528 |
-
# Cap payload size: last 300 messages, each content truncated to 20k chars.
|
| 529 |
-
truncated = []
|
| 530 |
-
for r in messages[-300:]:
|
| 531 |
-
content = r.get("content") or ""
|
| 532 |
-
if isinstance(content, str) and len(content) > 20000:
|
| 533 |
-
content = content[:20000] + "\\n...[truncated]"
|
| 534 |
-
truncated.append(
|
| 535 |
-
{
|
| 536 |
-
"role": r.get("role", "assistant"),
|
| 537 |
-
"content": content,
|
| 538 |
-
"timestamp": r.get("timestamp"),
|
| 539 |
-
}
|
| 540 |
-
)
|
| 541 |
-
return truncated
|
| 542 |
-
'''
|
| 543 |
-
content = content.rstrip() + "\n" + endpoint
|
| 544 |
-
print(" + Added public /api/public/session/{id} endpoint")
|
| 545 |
-
changed = True
|
| 546 |
-
return content, changed
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
def patch_session_logger(content):
|
| 550 |
-
"""Fix v2's broken insertion that caused a SyntaxError.
|
| 551 |
-
|
| 552 |
-
v2 inserted the filter between `rows.sort(...)` and `return rows` at the
|
| 553 |
-
WRONG indentation (4 spaces instead of 8), producing:
|
| 554 |
-
expected 'except' or 'finally' block
|
| 555 |
-
The whole patch was skipped, so event rows were never filtered.
|
| 556 |
-
|
| 557 |
-
v3 inserts the filter at the correct 8-space indent inside the try block.
|
| 558 |
-
"""
|
| 559 |
-
changed = False
|
| 560 |
-
if MARKER3_PY in content:
|
| 561 |
-
print(" SKIP: already patched")
|
| 562 |
-
return content, False
|
| 563 |
-
|
| 564 |
-
# Remove the broken v2 insertion if it ever made it in.
|
| 565 |
-
broken = (
|
| 566 |
-
" # === PATCH: catch-up-v2 ===\n"
|
| 567 |
-
" # Filter out event/system rows so summarization never sees\n"
|
| 568 |
-
" # internal tracing data that cant be parsed as Message objects.\n"
|
| 569 |
-
" valid_roles = {\"user\", \"assistant\", \"tool\"}\n"
|
| 570 |
-
" rows = [r for r in rows if r.get(\"role\") in valid_roles]\n"
|
| 571 |
-
)
|
| 572 |
-
if broken in content:
|
| 573 |
-
content = content.replace(broken, "", 1)
|
| 574 |
-
print(" + Removed broken v2 insertion")
|
| 575 |
-
changed = True
|
| 576 |
-
|
| 577 |
-
if "def _filter_valid_roles" not in content:
|
| 578 |
-
old_sort = (
|
| 579 |
-
" rows.sort(key=lambda r: (r.get(\"seq\", 0), r.get(\"timestamp\", \"\")))\n"
|
| 580 |
-
" return rows\n"
|
| 581 |
-
)
|
| 582 |
-
new_sort = (
|
| 583 |
-
" rows.sort(key=lambda r: (r.get(\"seq\", 0), r.get(\"timestamp\", \"\")))\n"
|
| 584 |
-
" # === PATCH: catch-up-v3 ===\n"
|
| 585 |
-
" # Summarization and Message.model_validate() cannot parse\n"
|
| 586 |
-
" # internal tracing rows (role='event'); filter them here so\n"
|
| 587 |
-
" # every consumer gets clean conversation data.\n"
|
| 588 |
-
" valid_roles = {\"user\", \"assistant\", \"tool\"}\n"
|
| 589 |
-
" return [r for r in rows if r.get(\"role\") in valid_roles]\n"
|
| 590 |
-
)
|
| 591 |
-
content, ok = _replace(content, old_sort, new_sort)
|
| 592 |
-
if ok:
|
| 593 |
-
print(" + Filter event rows at source (load_session_history)")
|
| 594 |
-
changed = True
|
| 595 |
-
else:
|
| 596 |
-
# Try the already-v2-patched variant (sort only, return separate)
|
| 597 |
-
old_sort2 = (
|
| 598 |
-
" rows.sort(key=lambda r: (r.get(\"seq\", 0), r.get(\"timestamp\", \"\")))\n"
|
| 599 |
-
)
|
| 600 |
-
new_sort2 = (
|
| 601 |
-
" rows.sort(key=lambda r: (r.get(\"seq\", 0), r.get(\"timestamp\", \"\")))\n"
|
| 602 |
-
" # === PATCH: catch-up-v3 ===\n"
|
| 603 |
-
" valid_roles = {\"user\", \"assistant\", \"tool\"}\n"
|
| 604 |
-
" rows = [r for r in rows if r.get(\"role\") in valid_roles]\n"
|
| 605 |
-
)
|
| 606 |
-
content, ok = _replace(content, old_sort2, new_sort2)
|
| 607 |
-
if ok:
|
| 608 |
-
print(" + Filter event rows at source (sort-then-filter variant)")
|
| 609 |
-
changed = True
|
| 610 |
-
|
| 611 |
-
return content, changed
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
# ─── Execute ────────────────────────────────────────────────────────────────
|
| 615 |
-
|
| 616 |
-
print("=== v3: Frontend ===")
|
| 617 |
-
patch_frontend_file(
|
| 618 |
-
"hooks/useAgentChat.ts",
|
| 619 |
-
patch_use_agent_chat,
|
| 620 |
-
"wire _catchupDone + type wrapper",
|
| 621 |
-
)
|
| 622 |
-
patch_frontend_file(
|
| 623 |
-
"components/Layout/AppLayout.tsx",
|
| 624 |
-
patch_app_layout,
|
| 625 |
-
"share button + share mode",
|
| 626 |
-
)
|
| 627 |
-
patch_share_view(os.path.join(FRONTEND_DIR, "components/ShareView.tsx"))
|
| 628 |
-
patch_frontend_file(
|
| 629 |
-
"components/SessionSidebar/SessionSidebar.tsx",
|
| 630 |
-
patch_session_sidebar,
|
| 631 |
-
"idempotency marker",
|
| 632 |
-
)
|
| 633 |
-
patch_frontend_file(
|
| 634 |
-
"components/Chat/ExpiredBanner.tsx",
|
| 635 |
-
patch_expired_banner,
|
| 636 |
-
"idempotency marker",
|
| 637 |
-
)
|
| 638 |
-
patch_frontend_file(
|
| 639 |
-
"components/WelcomeScreen/WelcomeScreen.tsx",
|
| 640 |
-
patch_welcome_screen,
|
| 641 |
-
"idempotency marker",
|
| 642 |
-
)
|
| 643 |
-
|
| 644 |
-
print("\n=== v3: Backend ===")
|
| 645 |
-
if os.path.exists(BACKEND_ROUTES_AGENT):
|
| 646 |
-
content = _read(BACKEND_ROUTES_AGENT)
|
| 647 |
-
content, changed = patch_routes_agent(content)
|
| 648 |
-
if changed:
|
| 649 |
-
try:
|
| 650 |
-
ast.parse(content)
|
| 651 |
-
_write(BACKEND_ROUTES_AGENT, content)
|
| 652 |
-
print("OK: routes/agent.py patched")
|
| 653 |
-
except SyntaxError as e:
|
| 654 |
-
print(f"FAIL: routes/agent.py syntax: {e}")
|
| 655 |
-
else:
|
| 656 |
-
print("SKIP: routes/agent.py — no changes needed")
|
| 657 |
-
else:
|
| 658 |
-
print("SKIP: routes/agent.py not found")
|
| 659 |
-
|
| 660 |
-
if os.path.exists(BACKEND_SESSION_LOGGER):
|
| 661 |
-
content = _read(BACKEND_SESSION_LOGGER)
|
| 662 |
-
content, changed = patch_session_logger(content)
|
| 663 |
-
if changed:
|
| 664 |
-
try:
|
| 665 |
-
ast.parse(content)
|
| 666 |
-
_write(BACKEND_SESSION_LOGGER, content)
|
| 667 |
-
print("OK: session_logger.py patched")
|
| 668 |
-
except SyntaxError as e:
|
| 669 |
-
print(f"FAIL: session_logger.py syntax: {e}")
|
| 670 |
-
else:
|
| 671 |
-
print("SKIP: session_logger.py — no changes needed")
|
| 672 |
-
else:
|
| 673 |
-
print("SKIP: session_logger.py not found")
|
| 674 |
-
|
| 675 |
-
print("\n=== v3 done ===")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_chat_input.py
DELETED
|
@@ -1,105 +0,0 @@
|
|
| 1 |
-
"""Patch ChatInput.tsx - Fixed import for DatasetUploadResponse."""
|
| 2 |
-
|
| 3 |
-
CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 4 |
-
|
| 5 |
-
def patch():
|
| 6 |
-
import os, re
|
| 7 |
-
if not os.path.exists(CHAT_INPUT):
|
| 8 |
-
print(f"SKIP: {CHAT_INPUT} not found")
|
| 9 |
-
return
|
| 10 |
-
|
| 11 |
-
with open(CHAT_INPUT, "r", encoding="utf-8") as f:
|
| 12 |
-
content = f.read()
|
| 13 |
-
|
| 14 |
-
# 1. Add Button to MUI imports
|
| 15 |
-
if "Button" not in content and "@mui/material" in content:
|
| 16 |
-
content = content.replace(
|
| 17 |
-
"import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
|
| 18 |
-
"import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
|
| 19 |
-
)
|
| 20 |
-
|
| 21 |
-
# 2. Fix DatasetUploadResponse import - check existing imports
|
| 22 |
-
if "DatasetUploadResponse" not in content:
|
| 23 |
-
# Find existing import from types/agent
|
| 24 |
-
if "from '@/types/agent'" in content:
|
| 25 |
-
content = content.replace(
|
| 26 |
-
"from '@/types/agent'",
|
| 27 |
-
"from '@/types/agent'"
|
| 28 |
-
)
|
| 29 |
-
# Add DatasetUploadResponse to existing import
|
| 30 |
-
content = content.replace(
|
| 31 |
-
"import type { ",
|
| 32 |
-
"import type { DatasetUploadResponse, "
|
| 33 |
-
)
|
| 34 |
-
else:
|
| 35 |
-
content = content.replace(
|
| 36 |
-
"import { apiFetch } from '@/utils/api';",
|
| 37 |
-
"import { apiFetch } from '@/utils/api';\nimport type { DatasetUploadResponse } from '@/types/agent';"
|
| 38 |
-
)
|
| 39 |
-
|
| 40 |
-
# 3. Add props to interface
|
| 41 |
-
if "showAutoContinue" not in content:
|
| 42 |
-
props_code = '''
|
| 43 |
-
interface ChatInputProps {
|
| 44 |
-
sessionId: string;
|
| 45 |
-
initialModelPath: string | null | undefined;
|
| 46 |
-
onSend: (text: string) => Promise<void>;
|
| 47 |
-
onStop: () => void;
|
| 48 |
-
onDatasetUploaded: () => Promise<boolean>;
|
| 49 |
-
isProcessing: boolean;
|
| 50 |
-
disabled: boolean;
|
| 51 |
-
placeholder?: string;
|
| 52 |
-
showAutoContinue?: boolean;
|
| 53 |
-
taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
|
| 54 |
-
startAutoContinue?: () => void;
|
| 55 |
-
cancelAutoContinue?: () => void;
|
| 56 |
-
}
|
| 57 |
-
'''
|
| 58 |
-
match = re.search(r'interface ChatInputProps[^}]*[}][^}]*[}]', content, re.DOTALL)
|
| 59 |
-
if match:
|
| 60 |
-
content = content.replace(match.group(0), props_code)
|
| 61 |
-
else:
|
| 62 |
-
content = content.replace(
|
| 63 |
-
"export default function ChatInput",
|
| 64 |
-
props_code + "\nexport default function ChatInput"
|
| 65 |
-
)
|
| 66 |
-
|
| 67 |
-
# 4. Add auto-continue buttons before the flex box
|
| 68 |
-
buttons_code = '''
|
| 69 |
-
{/* Auto-continue controls for free models */}
|
| 70 |
-
{showAutoContinue && taskIncompleteInfo && (
|
| 71 |
-
<Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
|
| 72 |
-
<Button
|
| 73 |
-
variant="contained"
|
| 74 |
-
color="primary"
|
| 75 |
-
size="small"
|
| 76 |
-
onClick={startAutoContinue}
|
| 77 |
-
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
|
| 78 |
-
>
|
| 79 |
-
Tự động tiếp tục (10s)
|
| 80 |
-
</Button>
|
| 81 |
-
<Button
|
| 82 |
-
variant="outlined"
|
| 83 |
-
color="secondary"
|
| 84 |
-
size="small"
|
| 85 |
-
onClick={cancelAutoContinue}
|
| 86 |
-
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
|
| 87 |
-
>
|
| 88 |
-
Tạm dừng
|
| 89 |
-
</Button>
|
| 90 |
-
</Stack>
|
| 91 |
-
)}
|
| 92 |
-
'''
|
| 93 |
-
|
| 94 |
-
if "Tự động tiếp tục" not in content:
|
| 95 |
-
content = content.replace(
|
| 96 |
-
"<Box sx={{ flex: 1 ",
|
| 97 |
-
buttons_code + " <Box sx={{ flex: 1 "
|
| 98 |
-
)
|
| 99 |
-
|
| 100 |
-
with open(CHAT_INPUT, "w", encoding="utf-8") as f:
|
| 101 |
-
f.write(content)
|
| 102 |
-
print(f"OK: Patched {CHAT_INPUT}")
|
| 103 |
-
|
| 104 |
-
if __name__ == "__main__":
|
| 105 |
-
patch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_document_upload.py
DELETED
|
@@ -1,225 +0,0 @@
|
|
| 1 |
-
"""Patch: extend dataset uploads to support office/binary documents
|
| 2 |
-
(PDF, images, Excel, Word, PPT, OpenDocument, text) and extract their text
|
| 3 |
-
so the agent can understand the file content even on text-only LLM backends.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import ast
|
| 7 |
-
import os
|
| 8 |
-
|
| 9 |
-
FILE = "/app/backend/dataset_uploads.py"
|
| 10 |
-
|
| 11 |
-
with open(FILE) as f:
|
| 12 |
-
c = f.read()
|
| 13 |
-
|
| 14 |
-
orig = c
|
| 15 |
-
|
| 16 |
-
# ---- 1. Expand allowed extensions ----
|
| 17 |
-
old_ext = 'ALLOWED_DATASET_EXTENSIONS = {"csv", "json", "jsonl"}'
|
| 18 |
-
new_ext = (
|
| 19 |
-
'ALLOWED_DATASET_EXTENSIONS = {\n'
|
| 20 |
-
' "csv", "json", "jsonl",\n'
|
| 21 |
-
' "pdf",\n'
|
| 22 |
-
' "png", "jpg", "jpeg", "webp", "bmp", "tiff", "tif", "gif",\n'
|
| 23 |
-
' "xlsx", "xlsm", "xls",\n'
|
| 24 |
-
' "docx", "doc",\n'
|
| 25 |
-
' "pptx",\n'
|
| 26 |
-
' "odt", "ods", "odp",\n'
|
| 27 |
-
' "txt", "md", "rtf", "log",\n'
|
| 28 |
-
'}\n'
|
| 29 |
-
'BINARY_EXTRACTION_EXTS = {\n'
|
| 30 |
-
' "pdf",\n'
|
| 31 |
-
' "png", "jpg", "jpeg", "webp", "bmp", "tiff", "tif", "gif",\n'
|
| 32 |
-
' "xlsx", "xlsm", "xls",\n'
|
| 33 |
-
' "docx", "doc",\n'
|
| 34 |
-
' "pptx",\n'
|
| 35 |
-
' "odt", "ods", "odp",\n'
|
| 36 |
-
' "rtf",\n'
|
| 37 |
-
'}\n'
|
| 38 |
-
)
|
| 39 |
-
if old_ext in c:
|
| 40 |
-
c = c.replace(old_ext, new_ext)
|
| 41 |
-
print("OK: expanded ALLOWED_DATASET_EXTENSIONS")
|
| 42 |
-
|
| 43 |
-
else:
|
| 44 |
-
print("WARN: extension line not found")
|
| 45 |
-
|
| 46 |
-
# ---- 1b. Update the legacy error message so no stale message remains ----
|
| 47 |
-
old_msg = 'detail="Only .csv, .json, and .jsonl dataset files are supported."'
|
| 48 |
-
new_msg = 'detail="Không hỗ trợ định dạng này. Các định dạng hỗ trợ: CSV, JSON, JSONL, PDF, ảnh, Excel (XLSX/XLSM/XLS), Word (DOCX/DOC), PowerPoint (PPTX), OpenDocument (ODT/ODS/ODP), và văn bản (TXT/MD/RTF/LOG)."'
|
| 49 |
-
if old_msg in c:
|
| 50 |
-
c = c.replace(old_msg, new_msg)
|
| 51 |
-
print("OK: updated legacy error message")
|
| 52 |
-
else:
|
| 53 |
-
print("WARN: legacy dataset message not found (already updated?)")
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
# ---- 2. Import the document parser ----
|
| 57 |
-
if "document_parser" not in c:
|
| 58 |
-
c = c.replace(
|
| 59 |
-
"from huggingface_hub import HfApi\n",
|
| 60 |
-
"from huggingface_hub import HfApi\n"
|
| 61 |
-
"from document_parser import (\n"
|
| 62 |
-
" extract_document_text,\n"
|
| 63 |
-
" format_uploaded_document_context,\n"
|
| 64 |
-
")\n",
|
| 65 |
-
)
|
| 66 |
-
print("OK: imported document_parser")
|
| 67 |
-
else:
|
| 68 |
-
print("OK: document_parser already imported")
|
| 69 |
-
|
| 70 |
-
# ---- 3. Extract text inside push_dataset_upload_to_hub ----
|
| 71 |
-
# Find the return statement that constructs the DatasetUpload and inject
|
| 72 |
-
# extraction before it.
|
| 73 |
-
marker = " return DatasetUpload(\n"
|
| 74 |
-
if marker in c and "extract_document_text" in c and "extracted_text" not in c:
|
| 75 |
-
extraction_block = (
|
| 76 |
-
" # Extract plain-text content so the agent can understand the file\n"
|
| 77 |
-
" # even when the LLM backend has no native attachment support.\n"
|
| 78 |
-
" extracted_text, extraction_warning = \"\", \"\"\n"
|
| 79 |
-
" if dataset_format in BINARY_EXTRACTION_EXTS:\n"
|
| 80 |
-
" try:\n"
|
| 81 |
-
" await asyncio.to_thread(upload.file.seek, 0)\n"
|
| 82 |
-
" raw_bytes = await asyncio.to_thread(upload.file.read)\n"
|
| 83 |
-
" extracted_text, extraction_warning = await asyncio.to_thread(\n"
|
| 84 |
-
" extract_document_text, safe_filename, raw_bytes\n"
|
| 85 |
-
" )\n"
|
| 86 |
-
" await asyncio.to_thread(upload.file.seek, 0)\n"
|
| 87 |
-
" except Exception as exc: # never let extraction break the upload\n"
|
| 88 |
-
" extraction_warning = f\"Could not extract text: {exc}\"\n"
|
| 89 |
-
" extracted_text = \"\"\n"
|
| 90 |
-
" await asyncio.to_thread(upload.file.seek, 0)\n"
|
| 91 |
-
"\n"
|
| 92 |
-
)
|
| 93 |
-
c = c.replace(marker, extraction_block + marker)
|
| 94 |
-
print("OK: inserted extraction block before DatasetUpload return")
|
| 95 |
-
else:
|
| 96 |
-
print("WARN: could not insert extraction block (marker or state issue)")
|
| 97 |
-
|
| 98 |
-
# ---- 4. Thread the extracted fields into the DatasetUpload dataclass ----
|
| 99 |
-
# Add fields to the dataclass
|
| 100 |
-
if "extracted_text: str" not in c:
|
| 101 |
-
c = c.replace(
|
| 102 |
-
" load_dataset_snippet: str\n",
|
| 103 |
-
" load_dataset_snippet: str\n"
|
| 104 |
-
" extracted_text: str = \"\"\n"
|
| 105 |
-
" extraction_warning: str = \"\"\n",
|
| 106 |
-
)
|
| 107 |
-
print("OK: added dataclass fields")
|
| 108 |
-
else:
|
| 109 |
-
print("OK: dataclass fields present")
|
| 110 |
-
|
| 111 |
-
# Add to response_payload
|
| 112 |
-
if '"extracted_text": self.extracted_text' not in c:
|
| 113 |
-
c = c.replace(
|
| 114 |
-
' "load_dataset_snippet": self.load_dataset_snippet,\n',
|
| 115 |
-
' "load_dataset_snippet": self.load_dataset_snippet,\n'
|
| 116 |
-
' "extracted_text": self.extracted_text,\n'
|
| 117 |
-
' "extraction_warning": self.extraction_warning,\n',
|
| 118 |
-
)
|
| 119 |
-
print("OK: response_payload includes extracted_text")
|
| 120 |
-
else:
|
| 121 |
-
print("OK: response_payload present")
|
| 122 |
-
|
| 123 |
-
# Add to the final constructor call
|
| 124 |
-
if "extracted_text=extracted_text" not in c:
|
| 125 |
-
c = c.replace(
|
| 126 |
-
" load_dataset_snippet=snippet,\n"
|
| 127 |
-
" )",
|
| 128 |
-
" load_dataset_snippet=snippet,\n"
|
| 129 |
-
" extracted_text=extracted_text,\n"
|
| 130 |
-
" extraction_warning=extraction_warning,\n"
|
| 131 |
-
" )",
|
| 132 |
-
)
|
| 133 |
-
print("OK: constructor passes extracted fields")
|
| 134 |
-
else:
|
| 135 |
-
print("OK: constructor present")
|
| 136 |
-
|
| 137 |
-
try:
|
| 138 |
-
ast.parse(c)
|
| 139 |
-
with open(FILE, "w") as f:
|
| 140 |
-
f.write(c)
|
| 141 |
-
print("OK: dataset_uploads.py written & syntax valid")
|
| 142 |
-
except SyntaxError as e:
|
| 143 |
-
print(f"FAIL: dataset_uploads.py syntax error: {e}")
|
| 144 |
-
|
| 145 |
-
# ---- Also patch models.py DatasetUploadResponse.format literal ----
|
| 146 |
-
MODELS_FILE = "/app/backend/models.py"
|
| 147 |
-
with open(MODELS_FILE) as f:
|
| 148 |
-
mc = f.read()
|
| 149 |
-
|
| 150 |
-
if 'Literal["csv", "json", "jsonl"]' in mc:
|
| 151 |
-
mc = mc.replace(
|
| 152 |
-
'format: Literal["csv", "json", "jsonl"]',
|
| 153 |
-
'format: str',
|
| 154 |
-
)
|
| 155 |
-
# add new fields
|
| 156 |
-
mc = mc.replace(
|
| 157 |
-
" load_dataset_snippet: str\n",
|
| 158 |
-
" load_dataset_snippet: str\n"
|
| 159 |
-
" extracted_text: str = \"\"\n"
|
| 160 |
-
" extraction_warning: str = \"\"\n",
|
| 161 |
-
)
|
| 162 |
-
with open(MODELS_FILE, "w") as f:
|
| 163 |
-
f.write(mc)
|
| 164 |
-
print("OK: models.py DatasetUploadResponse widened")
|
| 165 |
-
else:
|
| 166 |
-
print("OK: models.py already patched")
|
| 167 |
-
|
| 168 |
-
# ---- Patch routes/agent.py to use the richer doc context note ----
|
| 169 |
-
AGENT_FILE = "/app/backend/routes/agent.py"
|
| 170 |
-
with open(AGENT_FILE) as f:
|
| 171 |
-
ac = f.read()
|
| 172 |
-
|
| 173 |
-
if "format_uploaded_document_context" not in ac:
|
| 174 |
-
# Update import
|
| 175 |
-
old_imp = " dataset_context_note,\n push_dataset_upload_to_hub,\n"
|
| 176 |
-
new_imp = (
|
| 177 |
-
" format_uploaded_document_context,\n"
|
| 178 |
-
" push_dataset_upload_to_hub,\n"
|
| 179 |
-
)
|
| 180 |
-
if old_imp in ac:
|
| 181 |
-
ac = ac.replace(old_imp, new_imp)
|
| 182 |
-
print("OK: agent.py import updated")
|
| 183 |
-
else:
|
| 184 |
-
# Try removing the dataset_context_note import line entirely
|
| 185 |
-
ac = ac.replace(" dataset_context_note,\n", "")
|
| 186 |
-
ac = ac.replace(
|
| 187 |
-
"from dataset_uploads import (\n push_dataset_upload_to_hub,\n)",
|
| 188 |
-
"from dataset_uploads import (\n"
|
| 189 |
-
" format_uploaded_document_context,\n"
|
| 190 |
-
" push_dataset_upload_to_hub,\n)",
|
| 191 |
-
)
|
| 192 |
-
print("OK: agent.py import swapped (fallback)")
|
| 193 |
-
|
| 194 |
-
# Update usage
|
| 195 |
-
old_use = 'Message(role="user", content=dataset_context_note(uploaded))'
|
| 196 |
-
new_use = (
|
| 197 |
-
'Message(role="user", content=format_uploaded_document_context(\n'
|
| 198 |
-
" filename=uploaded.original_filename,\n"
|
| 199 |
-
" stored_filename=uploaded.filename,\n"
|
| 200 |
-
" repo_id=uploaded.repo_id,\n"
|
| 201 |
-
" path_in_repo=uploaded.path_in_repo,\n"
|
| 202 |
-
" hub_url=uploaded.hub_url,\n"
|
| 203 |
-
" size_bytes=uploaded.size_bytes,\n"
|
| 204 |
-
" file_format=uploaded.format,\n"
|
| 205 |
-
" extracted_text=uploaded.extracted_text,\n"
|
| 206 |
-
" warning=uploaded.extraction_warning,\n"
|
| 207 |
-
" ))"
|
| 208 |
-
)
|
| 209 |
-
if old_use in ac:
|
| 210 |
-
ac = ac.replace(old_use, new_use)
|
| 211 |
-
print("OK: agent.py usage updated")
|
| 212 |
-
else:
|
| 213 |
-
print("WARN: agent.py usage line not found")
|
| 214 |
-
|
| 215 |
-
try:
|
| 216 |
-
ast.parse(ac)
|
| 217 |
-
with open(AGENT_FILE, "w") as f:
|
| 218 |
-
f.write(ac)
|
| 219 |
-
print("OK: agent.py written & syntax valid")
|
| 220 |
-
except SyntaxError as e:
|
| 221 |
-
print(f"FAIL: agent.py syntax error: {e}")
|
| 222 |
-
else:
|
| 223 |
-
print("OK: agent.py already patched")
|
| 224 |
-
|
| 225 |
-
print("DONE: document upload backend patch complete")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_fix_static_404.py
DELETED
|
@@ -1,105 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Patch: Fix 404 on root and /api when frontend build is missing.
|
| 4 |
-
|
| 5 |
-
Root cause: The Dockerfile creates /app/static with `mkdir -p` but the
|
| 6 |
-
frontend build may fail, leaving the directory empty. main.py checks
|
| 7 |
-
`if static_path.exists()` which returns True for an empty directory,
|
| 8 |
-
causing StaticFiles to mount at "/" and shadow ALL API routes with 404s.
|
| 9 |
-
|
| 10 |
-
Fix: Patch main.py to check that the static directory is non-empty
|
| 11 |
-
before mounting StaticFiles.
|
| 12 |
-
"""
|
| 13 |
-
|
| 14 |
-
import os
|
| 15 |
-
import sys
|
| 16 |
-
|
| 17 |
-
TARGET_FILE = "/app/backend/main.py"
|
| 18 |
-
PATCHED_MARKER = "# === PATCH: fix static 404 on empty dir ==="
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def patch_main_py(content: str) -> tuple[str, bool]:
|
| 22 |
-
changed = False
|
| 23 |
-
|
| 24 |
-
old_block = (
|
| 25 |
-
"# Serve static files (frontend build) in production\n"
|
| 26 |
-
"static_path = Path(__file__).parent.parent / \"static\"\n"
|
| 27 |
-
"if static_path.exists():\n"
|
| 28 |
-
" app.mount(\"/\", StaticFiles(directory=str(static_path), html=True), name=\"static\")\n"
|
| 29 |
-
" logger.info(f\"Serving static files from {static_path}\")\n"
|
| 30 |
-
"else:\n"
|
| 31 |
-
" logger.info(\"No static directory found, running in API-only mode\")"
|
| 32 |
-
)
|
| 33 |
-
|
| 34 |
-
new_block = (
|
| 35 |
-
"# Serve static files (frontend build) in production\n"
|
| 36 |
-
"static_path = Path(__file__).parent.parent / \"static\"\n"
|
| 37 |
-
f"{PATCHED_MARKER}\n"
|
| 38 |
-
"# Only mount if the directory exists AND has content — an empty\n"
|
| 39 |
-
"# directory would shadow all API routes with 404s.\n"
|
| 40 |
-
"if static_path.exists() and any(static_path.iterdir()):\n"
|
| 41 |
-
" app.mount(\"/\", StaticFiles(directory=str(static_path), html=True), name=\"static\")\n"
|
| 42 |
-
" logger.info(f\"Serving static files from {static_path}\")\n"
|
| 43 |
-
"else:\n"
|
| 44 |
-
" logger.info(\"No static files found, running in API-only mode\")"
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
if old_block in content:
|
| 48 |
-
content = content.replace(old_block, new_block, 1)
|
| 49 |
-
changed = True
|
| 50 |
-
print(" + Fixed static file mounting to check for non-empty directory")
|
| 51 |
-
else:
|
| 52 |
-
# Try a more flexible match
|
| 53 |
-
if "static_path.exists()" in content and PATCHED_MARKER not in content:
|
| 54 |
-
import re
|
| 55 |
-
# Replace the if condition
|
| 56 |
-
pattern = r'if static_path\.exists\(\):'
|
| 57 |
-
replacement = f'if static_path.exists() and any(static_path.iterdir()):\n {PATCHED_MARKER}'
|
| 58 |
-
content = re.sub(pattern, replacement, content, count=1)
|
| 59 |
-
changed = True
|
| 60 |
-
print(" + Fixed static file mounting (regex fallback)")
|
| 61 |
-
else:
|
| 62 |
-
print(f" SKIP: pattern already patched or not found")
|
| 63 |
-
|
| 64 |
-
return content, changed
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def main():
|
| 68 |
-
if not os.path.exists(TARGET_FILE):
|
| 69 |
-
print(f"SKIP: {TARGET_FILE} not found")
|
| 70 |
-
return
|
| 71 |
-
|
| 72 |
-
with open(TARGET_FILE, "r", encoding="utf-8") as f:
|
| 73 |
-
content = f.read()
|
| 74 |
-
|
| 75 |
-
if PATCHED_MARKER in content:
|
| 76 |
-
print(f"SKIP: {TARGET_FILE} already patched")
|
| 77 |
-
return
|
| 78 |
-
|
| 79 |
-
# Syntax-check by parsing
|
| 80 |
-
import ast
|
| 81 |
-
try:
|
| 82 |
-
ast.parse(content)
|
| 83 |
-
except SyntaxError as e:
|
| 84 |
-
print(f"WARN: {TARGET_FILE} has syntax errors before patching: {e}")
|
| 85 |
-
return
|
| 86 |
-
|
| 87 |
-
content, changed = patch_main_py(content)
|
| 88 |
-
if changed:
|
| 89 |
-
try:
|
| 90 |
-
ast.parse(content)
|
| 91 |
-
with open(TARGET_FILE, "w", encoding="utf-8") as f:
|
| 92 |
-
f.write(content)
|
| 93 |
-
print(f"OK: {TARGET_FILE} patched")
|
| 94 |
-
except SyntaxError as e:
|
| 95 |
-
print(f"FAIL: patched {TARGET_FILE} has syntax error: {e}")
|
| 96 |
-
else:
|
| 97 |
-
print(f"SKIP: {TARGET_FILE} — no changes applied")
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
if __name__ == "__main__":
|
| 101 |
-
try:
|
| 102 |
-
main()
|
| 103 |
-
except Exception as e:
|
| 104 |
-
print(f"WARN: patch_fix_static_404.py error: {e}")
|
| 105 |
-
sys.exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_flint_free.py
DELETED
|
@@ -1,229 +0,0 @@
|
|
| 1 |
-
"""Patch backend + frontend: add recursive/flint-v1.0:free (OpenRouter).
|
| 2 |
-
|
| 3 |
-
Uses the same `openai/` path convention as Laguna S 2.1:free:
|
| 4 |
-
- backend constant: RECURSIVE_FLINT_V1_FREE_MODEL_ID = "openai/recursive/flint-v1.0:free"
|
| 5 |
-
- registered in _available_models() backend list
|
| 6 |
-
- frontend: new ModelOption in ChatInput.tsx DEFAULT_MODEL_OPTIONS
|
| 7 |
-
|
| 8 |
-
The `openai/` prefix is stripped in _resolve_llm_params and routed to
|
| 9 |
-
OpenRouter, so no changes to llm_params.py routing are needed.
|
| 10 |
-
|
| 11 |
-
IMPORTANT: recursive/flint-v1.0:free has `reasoning: { mandatory: true }` on
|
| 12 |
-
OpenRouter. The agent loop sends `reasoning_effort` via HF Router's
|
| 13 |
-
`extra_body`, which OpenRouter doesn't translate to the `reasoning` parameter
|
| 14 |
-
expected by the Recursive provider. We patch _resolve_llm_params to inject
|
| 15 |
-
`reasoning: true` into the OpenRouter params for this model so the provider
|
| 16 |
-
accepts the request.
|
| 17 |
-
|
| 18 |
-
Runs AFTER patch_models.py / patch_new_models.py / patch_xing_nemotron_vl.py.
|
| 19 |
-
Idempotent.
|
| 20 |
-
"""
|
| 21 |
-
|
| 22 |
-
import ast
|
| 23 |
-
import os
|
| 24 |
-
|
| 25 |
-
IDS_FILE = "/app/agent/core/model_ids.py"
|
| 26 |
-
AGENT_FILE = "/app/backend/routes/agent.py"
|
| 27 |
-
LLM_PARAMS_FILE = "/app/agent/core/llm_params.py"
|
| 28 |
-
FRONTEND_FILE = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 29 |
-
|
| 30 |
-
# ---------- 1. model_ids.py ----------
|
| 31 |
-
with open(IDS_FILE) as f:
|
| 32 |
-
c = f.read()
|
| 33 |
-
|
| 34 |
-
new_constant = 'RECURSIVE_FLINT_V1_FREE_MODEL_ID = "openai/recursive/flint-v1.0:free"\n'
|
| 35 |
-
|
| 36 |
-
if "RECURSIVE_FLINT_V1_FREE_MODEL_ID" not in c:
|
| 37 |
-
# Anchor: the NEMOTRON_3_5_SUPER_VL line injected by patch_xing_nemotron_vl.py.
|
| 38 |
-
# Fall back to LAGUNA_S21 line from patch_models.py.
|
| 39 |
-
anchors = [
|
| 40 |
-
'NEMOTRON_3_5_SUPER_VL_FREE_MODEL_ID = "openai/nvidia/nemotron-3.5-super-vl:free"',
|
| 41 |
-
'LAGUNA_S21_FREE_MODEL_ID = "openai/poolside/laguna-s-2.1:free"',
|
| 42 |
-
]
|
| 43 |
-
inserted = False
|
| 44 |
-
for anchor in anchors:
|
| 45 |
-
if anchor in c:
|
| 46 |
-
c = c.replace(anchor, anchor + "\n" + new_constant, 1)
|
| 47 |
-
print(f"OK: added constant to model_ids.py after {anchor.split(' =')[0]}")
|
| 48 |
-
inserted = True
|
| 49 |
-
break
|
| 50 |
-
if not inserted:
|
| 51 |
-
print("FAIL: no anchor found in model_ids.py for constant insertion")
|
| 52 |
-
raise SystemExit(1)
|
| 53 |
-
|
| 54 |
-
with open(IDS_FILE, "w") as f:
|
| 55 |
-
f.write(c)
|
| 56 |
-
ast.parse(c)
|
| 57 |
-
print("OK: model_ids.py written & syntax OK")
|
| 58 |
-
else:
|
| 59 |
-
print("OK: model_ids.py already has RECURSIVE_FLINT_V1_FREE_MODEL_ID (idempotent)")
|
| 60 |
-
|
| 61 |
-
# ---------- 2. agent.py : imports ----------
|
| 62 |
-
with open(AGENT_FILE) as f:
|
| 63 |
-
ac = f.read()
|
| 64 |
-
|
| 65 |
-
if "RECURSIVE_FLINT_V1_FREE_MODEL_ID" not in ac:
|
| 66 |
-
# patch_xing_nemotron_vl.py injected NEMOTRON_3_5_SUPER_VL_FREE_MODEL_ID into
|
| 67 |
-
# the import block after LING_3_0_FLASH_FREE_MODEL_ID.
|
| 68 |
-
imp_anchor = " LING_3_0_FLASH_FREE_MODEL_ID,\n"
|
| 69 |
-
if imp_anchor in ac:
|
| 70 |
-
ac = ac.replace(
|
| 71 |
-
imp_anchor,
|
| 72 |
-
imp_anchor
|
| 73 |
-
+ " NEMOTRON_3_5_SUPER_VL_FREE_MODEL_ID,\n"
|
| 74 |
-
+ " RECURSIVE_FLINT_V1_FREE_MODEL_ID,\n",
|
| 75 |
-
1,
|
| 76 |
-
)
|
| 77 |
-
print("OK: added RECURSIVE_FLINT_V1 import to agent.py")
|
| 78 |
-
else:
|
| 79 |
-
print("FAIL: import anchor not found in agent.py")
|
| 80 |
-
raise SystemExit(1)
|
| 81 |
-
|
| 82 |
-
# ---------- 3. agent.py : _available_models ----------
|
| 83 |
-
# Insert after the NEMOTRON_3_5_SUPER_VL entry in _available_models.
|
| 84 |
-
line_anchor = (
|
| 85 |
-
' {"id": NEMOTRON_3_5_SUPER_VL_FREE_MODEL_ID, '
|
| 86 |
-
'"label": "nvidia/nemotron-3.5-super-vl:free", "recommended": True},\n'
|
| 87 |
-
)
|
| 88 |
-
new_line = line_anchor + (
|
| 89 |
-
' {"id": RECURSIVE_FLINT_V1_FREE_MODEL_ID, '
|
| 90 |
-
'"label": "recursive/flint-v1.0:free", "recommended": True},\n'
|
| 91 |
-
)
|
| 92 |
-
|
| 93 |
-
if line_anchor in ac:
|
| 94 |
-
ac = ac.replace(line_anchor, new_line, 1)
|
| 95 |
-
print("OK: added recursive/flint-v1.0:free to _available_models()")
|
| 96 |
-
else:
|
| 97 |
-
print("WARN: NEMOTRON VL line anchor not matched, trying fallback...")
|
| 98 |
-
# Fallback: insert after the LING_3_0_FLASH entry (more stable across patches).
|
| 99 |
-
alt_anchor = (
|
| 100 |
-
' {"id": LING_3_0_FLASH_FREE_MODEL_ID, '
|
| 101 |
-
'"label": "inclusionai/ling-3.0-flash:free", "recommended": True},\n'
|
| 102 |
-
)
|
| 103 |
-
if alt_anchor in ac:
|
| 104 |
-
ac = ac.replace(
|
| 105 |
-
alt_anchor,
|
| 106 |
-
alt_anchor + new_line,
|
| 107 |
-
1,
|
| 108 |
-
)
|
| 109 |
-
print("OK: added recursive/flint-v1.0:free to _available_models() (fallback)")
|
| 110 |
-
else:
|
| 111 |
-
print("WARN: fallback anchor also not matched - model may already be present or patches changed")
|
| 112 |
-
|
| 113 |
-
with open(AGENT_FILE, "w") as f:
|
| 114 |
-
f.write(ac)
|
| 115 |
-
ast.parse(ac)
|
| 116 |
-
print("OK: agent.py written & syntax OK")
|
| 117 |
-
else:
|
| 118 |
-
print("OK: agent.py already has RECURSIVE_FLINT_V1_FREE_MODEL_ID (idempotent)")
|
| 119 |
-
|
| 120 |
-
# ---------- 4. llm_params.py : reasoning support for recursive/flint-v1.0 ----------
|
| 121 |
-
# The Flint model has reasoning: { mandatory: true } on OpenRouter. Without
|
| 122 |
-
# sending reasoning=true, the Recursive provider returns 400 "Inference request failed".
|
| 123 |
-
# We patch the openai/ routing branch to inject reasoning=true for this model.
|
| 124 |
-
with open(LLM_PARAMS_FILE) as f:
|
| 125 |
-
lp = f.read()
|
| 126 |
-
|
| 127 |
-
if "flint" not in lp.lower():
|
| 128 |
-
# Find the openai/ routing block and add reasoning support for flint models.
|
| 129 |
-
# The patched llm_params.py has an openai/ branch that strips the prefix.
|
| 130 |
-
# We inject reasoning=true into the returned params for flint models.
|
| 131 |
-
flint_patch = (
|
| 132 |
-
' # Flint requires reasoning to be explicitly enabled (mandatory on OpenRouter)\n'
|
| 133 |
-
' if "flint" in or_model.lower():\n'
|
| 134 |
-
' params = {\n'
|
| 135 |
-
' "model": or_model,\n'
|
| 136 |
-
' "api_base": "https://openrouter.ai/api/v1",\n'
|
| 137 |
-
' "api_key": os.environ.get("OPENROUTER_API_KEY") or api_key or "",\n'
|
| 138 |
-
' "reasoning": {"effort": "low"},\n'
|
| 139 |
-
' }\n'
|
| 140 |
-
' return params\n'
|
| 141 |
-
'\n'
|
| 142 |
-
)
|
| 143 |
-
|
| 144 |
-
# Check what the current openai/ branch looks like and inject before it returns
|
| 145 |
-
# The existing patched llm_params.py has the openai/ branch that looks like:
|
| 146 |
-
# if normalized_model.startswith("openai/"):
|
| 147 |
-
# or_model = normalized_model[len("openai/"):]
|
| 148 |
-
# return {
|
| 149 |
-
# "model": or_model,
|
| 150 |
-
# "api_base": "https://openrouter.ai/api/v1",
|
| 151 |
-
# "api_key": os.environ.get("OPENROUTER_API_KEY") or api_key or "",
|
| 152 |
-
# }
|
| 153 |
-
openai_block = (
|
| 154 |
-
' if normalized_model.startswith("openai/"):\n'
|
| 155 |
-
' or_model = normalized_model[len("openai/"):]\n'
|
| 156 |
-
' return {\n'
|
| 157 |
-
' "model": or_model,\n'
|
| 158 |
-
' "api_base": "https://openrouter.ai/api/v1",\n'
|
| 159 |
-
' "api_key": os.environ.get("OPENROUTER_API_KEY") or api_key or "",\n'
|
| 160 |
-
' }\n'
|
| 161 |
-
)
|
| 162 |
-
|
| 163 |
-
if openai_block in lp:
|
| 164 |
-
lp = lp.replace(
|
| 165 |
-
openai_block,
|
| 166 |
-
openai_block + "\n" + flint_patch
|
| 167 |
-
)
|
| 168 |
-
print("OK: patched llm_params.py to inject reasoning for flint models")
|
| 169 |
-
else:
|
| 170 |
-
# Fallback: try the custom_llm_provider variant (patched by patch_openrouter_fix.py)
|
| 171 |
-
openai_block_v2 = (
|
| 172 |
-
' if normalized_model.startswith("openai/"):\n'
|
| 173 |
-
' or_model = normalized_model[len("openai/"):]\n'
|
| 174 |
-
' return {\n'
|
| 175 |
-
' "model": or_model,\n'
|
| 176 |
-
' "api_base": "https://openrouter.ai/api/v1",\n'
|
| 177 |
-
' "api_key": os.environ.get("OPENROUTER_API_KEY") or api_key or "",\n'
|
| 178 |
-
' "custom_llm_provider": "openrouter",\n'
|
| 179 |
-
' }\n'
|
| 180 |
-
)
|
| 181 |
-
if openai_block_v2 in lp:
|
| 182 |
-
lp = lp.replace(
|
| 183 |
-
openai_block_v2,
|
| 184 |
-
openai_block_v2 + "\n" + flint_patch
|
| 185 |
-
)
|
| 186 |
-
print("OK: patched llm_params.py (v2) to inject reasoning for flint models")
|
| 187 |
-
else:
|
| 188 |
-
print("WARN: could not find openai/ routing block in llm_params.py")
|
| 189 |
-
print(" - Flint model will be registered but may fail with 400 'Inference request failed'")
|
| 190 |
-
print(" - The model has reasoning:{mandatory:true} on OpenRouter")
|
| 191 |
-
|
| 192 |
-
with open(LLM_PARAMS_FILE, "w") as f:
|
| 193 |
-
f.write(lp)
|
| 194 |
-
ast.parse(lp)
|
| 195 |
-
print("OK: llm_params.py written & syntax OK")
|
| 196 |
-
else:
|
| 197 |
-
print("OK: llm_params.py already patched for flint (idempotent)")
|
| 198 |
-
|
| 199 |
-
# ---------- 5. Frontend ChatInput.tsx ----------
|
| 200 |
-
if os.path.exists(FRONTEND_FILE):
|
| 201 |
-
with open(FRONTEND_FILE, "r", encoding="utf-8") as f:
|
| 202 |
-
fc = f.read()
|
| 203 |
-
|
| 204 |
-
if "flint-v1.0" in fc:
|
| 205 |
-
print("OK: recursive/flint-v1.0:free already present in ChatInput.tsx (idempotent)")
|
| 206 |
-
else:
|
| 207 |
-
# Anchor: the deepseek-v4-flash-latest entry at the end of DEFAULT_MODEL_OPTIONS.
|
| 208 |
-
anchor = " {\n id: 'deepseek-v4-flash-latest',\n"
|
| 209 |
-
new_entry = (
|
| 210 |
-
" {\n"
|
| 211 |
-
" id: 'flint-v1.0-free',\n"
|
| 212 |
-
" name: 'recursive/flint-v1.0:free',\n"
|
| 213 |
-
" modelPath: 'openai/recursive/flint-v1.0:free',\n"
|
| 214 |
-
" avatarUrl: 'https://huggingface.co/api/avatars/recursive',\n"
|
| 215 |
-
" recommended: true,\n"
|
| 216 |
-
" },\n"
|
| 217 |
-
)
|
| 218 |
-
|
| 219 |
-
if anchor in fc:
|
| 220 |
-
fc = fc.replace(anchor, new_entry + anchor, 1)
|
| 221 |
-
with open(FRONTEND_FILE, "w", encoding="utf-8") as f:
|
| 222 |
-
f.write(fc)
|
| 223 |
-
print("OK: added recursive/flint-v1.0:free to ChatInput.tsx DEFAULT_MODEL_OPTIONS")
|
| 224 |
-
else:
|
| 225 |
-
print("WARN: deepseek anchor not found in ChatInput.tsx - frontend entry not added")
|
| 226 |
-
else:
|
| 227 |
-
print(f"WARN: {FRONTEND_FILE} not found (frontend patch skipped)")
|
| 228 |
-
|
| 229 |
-
print("DONE: recursive/flint-v1.0:free added with Laguna S 2.1:free path convention + reasoning fix")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_flint_free_frontend.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 1 |
-
"""Patch frontend ChatInput.tsx: add recursive/flint-v1.0:free (OpenRouter).
|
| 2 |
-
|
| 3 |
-
Uses the same `openai/` modelPath convention as Laguna S 2.1:free.
|
| 4 |
-
The frontend modelPath is sent to the backend, which strips the `openai/`
|
| 5 |
-
prefix and routes to OpenRouter via the existing catch-all in _resolve_llm_params.
|
| 6 |
-
|
| 7 |
-
Runs AFTER patch_frontend_xing_nemotron_vl.py (which already rewrote
|
| 8 |
-
DEFAULT_MODEL_OPTIONS). Idempotent.
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
import os
|
| 12 |
-
|
| 13 |
-
FILE = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def patch():
|
| 17 |
-
if not os.path.exists(FILE):
|
| 18 |
-
print(f"SKIP: {FILE} not found")
|
| 19 |
-
return
|
| 20 |
-
|
| 21 |
-
with open(FILE, "r", encoding="utf-8") as f:
|
| 22 |
-
content = f.read()
|
| 23 |
-
|
| 24 |
-
if "flint-v1.0" in content:
|
| 25 |
-
print("OK: recursive/flint-v1.0:free already present in ChatInput.tsx (idempotent)")
|
| 26 |
-
return
|
| 27 |
-
|
| 28 |
-
# Anchor: the deepseek-v4-flash-latest entry at the end of DEFAULT_MODEL_OPTIONS.
|
| 29 |
-
anchor = " {\n id: 'deepseek-v4-flash-latest',\n"
|
| 30 |
-
new_entry = (
|
| 31 |
-
" {\n"
|
| 32 |
-
" id: 'flint-v1.0-free',\n"
|
| 33 |
-
" name: 'recursive/flint-v1.0:free',\n"
|
| 34 |
-
" modelPath: 'openai/recursive/flint-v1.0:free',\n"
|
| 35 |
-
" avatarUrl: 'https://huggingface.co/api/avatars/recursive',\n"
|
| 36 |
-
" recommended: true,\n"
|
| 37 |
-
" },\n"
|
| 38 |
-
)
|
| 39 |
-
|
| 40 |
-
if anchor in content:
|
| 41 |
-
content = content.replace(anchor, new_entry + anchor, 1)
|
| 42 |
-
with open(FILE, "w", encoding="utf-8") as f:
|
| 43 |
-
f.write(content)
|
| 44 |
-
print("OK: added recursive/flint-v1.0:free to ChatInput.tsx DEFAULT_MODEL_OPTIONS")
|
| 45 |
-
else:
|
| 46 |
-
print("FAIL: deepseek anchor not found in ChatInput.tsx")
|
| 47 |
-
return
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
if __name__ == "__main__":
|
| 51 |
-
patch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_frontend.py
DELETED
|
@@ -1,199 +0,0 @@
|
|
| 1 |
-
"""Patch frontend: Replace DEFAULT_MODEL_OPTIONS with OpenRouter models."""
|
| 2 |
-
|
| 3 |
-
import re
|
| 4 |
-
import sys
|
| 5 |
-
|
| 6 |
-
FILE = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 7 |
-
MODEL_FILE = "/source/frontend/src/utils/model.ts"
|
| 8 |
-
|
| 9 |
-
# === Step 1: Patch model.ts ===
|
| 10 |
-
with open(MODEL_FILE, "r") as f:
|
| 11 |
-
model_content = f.read()
|
| 12 |
-
|
| 13 |
-
model_content = model_content.replace(
|
| 14 |
-
"export const KIMI_K27_CODE_MODEL_PATH = 'moonshotai/Kimi-K2.7-Code:novita';",
|
| 15 |
-
"export const KIMI_K27_CODE_MODEL_PATH = 'deepseek-ai/DeepSeek-V4-Pro';"
|
| 16 |
-
)
|
| 17 |
-
model_content = model_content.replace(
|
| 18 |
-
"export const MINIMAX_M3_MODEL_PATH = 'MiniMaxAI/MiniMax-M3:novita';",
|
| 19 |
-
"export const MINIMAX_M3_MODEL_PATH = 'deepseek-ai/DeepSeek-V4-Flash';"
|
| 20 |
-
)
|
| 21 |
-
model_content = model_content.replace(
|
| 22 |
-
"export const GLM_52_MODEL_PATH = 'zai-org/GLM-5.2:novita';",
|
| 23 |
-
"export const GLM_52_MODEL_PATH = 'deepseek/deepseek-v4-flash';"
|
| 24 |
-
)
|
| 25 |
-
model_content = model_content.replace(
|
| 26 |
-
"export const DEEPSEEK_V4_PRO_MODEL_PATH = 'deepseek-ai/DeepSeek-V4-Pro:novita';",
|
| 27 |
-
"export const DEEPSEEK_V4_PRO_MODEL_PATH = 'nvidia/nemotron-3-super-120b-a12b:free';"
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
# Add DeepSeek V4 Flash latest model path
|
| 31 |
-
if "DEEPSEEK_V4_FLASH_LATEST_MODEL_PATH" not in model_content:
|
| 32 |
-
model_content = model_content.replace(
|
| 33 |
-
"export const LAGUNA_S21_MODEL_PATH",
|
| 34 |
-
"export const DEEPSEEK_V4_FLASH_LATEST_MODEL_PATH = '~deepseek/deepseek-v4-flash-latest';\n\nexport const LAGUNA_S21_MODEL_PATH"
|
| 35 |
-
)
|
| 36 |
-
print("OK: Added DEEPSEEK_V4_FLASH_LATEST_MODEL_PATH")
|
| 37 |
-
|
| 38 |
-
# === PATCH: Add Laguna S 2.1 model path ===
|
| 39 |
-
if "LAGUNA_S21_MODEL_PATH" not in model_content:
|
| 40 |
-
model_content = model_content.replace(
|
| 41 |
-
"export const LAGUNA_M1_MODEL_PATH",
|
| 42 |
-
"export const LAGUNA_S21_MODEL_PATH = 'openai/poolside/laguna-s-2.1:free';\n\nexport const LAGUNA_M1_MODEL_PATH"
|
| 43 |
-
)
|
| 44 |
-
print("OK: Added LAGUNA_S21_MODEL_PATH")
|
| 45 |
-
|
| 46 |
-
# === PATCH: Add Space Bunny Alpha model path ===
|
| 47 |
-
if "SPACE_BUNNY_ALPHA_MODEL_PATH" not in model_content:
|
| 48 |
-
model_content = model_content.replace(
|
| 49 |
-
"export const LAGUNA_S21_MODEL_PATH",
|
| 50 |
-
"export const SPACE_BUNNY_ALPHA_MODEL_PATH = 'openai/stealth/space-bunny-alpha';\n\nexport const LAGUNA_S21_MODEL_PATH"
|
| 51 |
-
)
|
| 52 |
-
print("OK: Added SPACE_BUNNY_ALPHA_MODEL_PATH")
|
| 53 |
-
|
| 54 |
-
with open(MODEL_FILE, "w") as f:
|
| 55 |
-
f.write(model_content)
|
| 56 |
-
print("OK: model.ts constants updated")
|
| 57 |
-
|
| 58 |
-
# === Step 2: Patch ChatInput.tsx ===
|
| 59 |
-
with open(FILE, "r", encoding="utf-8") as f:
|
| 60 |
-
content = f.read()
|
| 61 |
-
|
| 62 |
-
# Fix imports
|
| 63 |
-
import_pattern = re.compile(
|
| 64 |
-
r'import\s*\{[^}]+\}\s*from\s*[\'"]@/utils/model[\'"];'
|
| 65 |
-
)
|
| 66 |
-
match = import_pattern.search(content)
|
| 67 |
-
if match:
|
| 68 |
-
content = content.replace(match.group(0), "import { isClaudePath } from '@/utils/model';")
|
| 69 |
-
print("OK: Replaced imports")
|
| 70 |
-
|
| 71 |
-
# Remove Owl Alpha references
|
| 72 |
-
content = content.replace("'Owl Alpha'", "'Claude Opus 4.8'")
|
| 73 |
-
content = content.replace('"Owl Alpha"', '"Claude Opus 4.8"')
|
| 74 |
-
|
| 75 |
-
# Row 3: Kimi → DeepSeek V4 Pro
|
| 76 |
-
content = content.replace("'Kimi K2.7 Code'", "'DeepSeek V4 Pro'")
|
| 77 |
-
content = content.replace('"Kimi K2.7 Code"', '"DeepSeek V4 Pro"')
|
| 78 |
-
|
| 79 |
-
# Row 4: MiniMax → DeepSeek V4 Flash
|
| 80 |
-
content = content.replace("'MiniMax M3'", "'DeepSeek V4 Flash'")
|
| 81 |
-
content = content.replace('"MiniMax M3"', '"DeepSeek V4 Flash"')
|
| 82 |
-
|
| 83 |
-
# Row 5: GLM → DeepSeek V4 Flash
|
| 84 |
-
content = content.replace("'GLM 5.2'", "'DeepSeek V4 Flash'")
|
| 85 |
-
content = content.replace('"GLM 5.2"', '"DeepSeek V4 Flash"')
|
| 86 |
-
|
| 87 |
-
# Row 6: DeepSeek V4 Pro → Nemotron
|
| 88 |
-
content = content.replace(
|
| 89 |
-
" {\n id: 'deepseek-v4-pro',\n name: 'DeepSeek V4 Pro',\n modelPath: DEEPSEEK_V4_PRO_MODEL_PATH,\n avatarUrl: getHfAvatarUrl('deepseek-ai/DeepSeek-V4-Pro'),\n },",
|
| 90 |
-
" {\n id: 'nemotron-120b',\n name: 'nvidia/nemotron-3-super-120b-a12b:free',\n modelPath: DEEPSEEK_V4_PRO_MODEL_PATH,\n avatarUrl: 'https://huggingface.co/api/avatars/nvidia',\n },"
|
| 91 |
-
)
|
| 92 |
-
|
| 93 |
-
# Replace entire DEFAULT_MODEL_OPTIONS array
|
| 94 |
-
pattern = r'const DEFAULT_MODEL_OPTIONS: ModelOption\[\] = \[.*?\];'
|
| 95 |
-
match = re.search(pattern, content, re.DOTALL)
|
| 96 |
-
|
| 97 |
-
if not match:
|
| 98 |
-
print("FAIL: Could not find DEFAULT_MODEL_OPTIONS array!")
|
| 99 |
-
sys.exit(1)
|
| 100 |
-
|
| 101 |
-
NEW_MODELS = """const DEFAULT_MODEL_OPTIONS: ModelOption[] = [
|
| 102 |
-
{
|
| 103 |
-
id: 'laguna-s21',
|
| 104 |
-
name: 'Laguna S 2.1:free',
|
| 105 |
-
modelPath: 'openai/poolside/laguna-s-2.1:free',
|
| 106 |
-
avatarUrl: 'https://huggingface.co/api/avatars/tencent',
|
| 107 |
-
recommended: true,
|
| 108 |
-
},
|
| 109 |
-
{
|
| 110 |
-
id: 'gemma-4-31b',
|
| 111 |
-
name: 'Gemma 4 31B:free',
|
| 112 |
-
modelPath: 'openai/google/gemma-4-31b-it:free',
|
| 113 |
-
avatarUrl: 'https://huggingface.co/api/avatars/google',
|
| 114 |
-
recommended: true,
|
| 115 |
-
},
|
| 116 |
-
{
|
| 117 |
-
id: 'llama-3.3-70b',
|
| 118 |
-
name: 'Llama 3.3 70B:free',
|
| 119 |
-
modelPath: 'openai/meta-llama/llama-3.3-70b-instruct:free',
|
| 120 |
-
avatarUrl: 'https://huggingface.co/api/avatars/meta-llama',
|
| 121 |
-
recommended: true,
|
| 122 |
-
},
|
| 123 |
-
{
|
| 124 |
-
id: 'laguna-m1',
|
| 125 |
-
name: 'Laguna M.1:free',
|
| 126 |
-
modelPath: 'openai/poolside/laguna-m.1:free',
|
| 127 |
-
avatarUrl: 'https://huggingface.co/api/avatars/poolside',
|
| 128 |
-
recommended: true,
|
| 129 |
-
},
|
| 130 |
-
{
|
| 131 |
-
id: 'laguna-s21',
|
| 132 |
-
name: 'Laguna S 2.1:free',
|
| 133 |
-
modelPath: 'openai/poolside/laguna-s-2.1:free',
|
| 134 |
-
avatarUrl: 'https://huggingface.co/api/avatars/poolside',
|
| 135 |
-
recommended: true,
|
| 136 |
-
},
|
| 137 |
-
{
|
| 138 |
-
id: 'space-bunny-alpha',
|
| 139 |
-
name: 'stealth/space-bunny-alpha',
|
| 140 |
-
modelPath: 'openai/stealth/space-bunny-alpha',
|
| 141 |
-
avatarUrl: 'https://huggingface.co/api/avatars/stealth',
|
| 142 |
-
recommended: true,
|
| 143 |
-
},
|
| 144 |
-
{
|
| 145 |
-
id: 'nex-n2-mini',
|
| 146 |
-
name: 'nex-agi/nex-n2-mini',
|
| 147 |
-
modelPath: 'openai/nex-agi/nex-n2-mini',
|
| 148 |
-
avatarUrl: 'https://huggingface.co/api/avatars/nex-agi',
|
| 149 |
-
recommended: true,
|
| 150 |
-
},
|
| 151 |
-
{
|
| 152 |
-
id: 'ling-3.0-flash',
|
| 153 |
-
name: 'inclusionai/ling-3.0-flash:free',
|
| 154 |
-
modelPath: 'openai/inclusionai/ling-3.0-flash:free',
|
| 155 |
-
avatarUrl: 'https://huggingface.co/api/avatars/inclusionai',
|
| 156 |
-
recommended: true,
|
| 157 |
-
},
|
| 158 |
-
{
|
| 159 |
-
id: 'llama-3.1-8b',
|
| 160 |
-
name: 'Llama 3.1 8B',
|
| 161 |
-
modelPath: 'openai/meta-llama/llama-3.1-8b-instruct',
|
| 162 |
-
avatarUrl: 'https://huggingface.co/api/avatars/meta-llama',
|
| 163 |
-
},
|
| 164 |
-
{
|
| 165 |
-
id: 'qwen3-coder-next',
|
| 166 |
-
name: 'Qwen3 Coder Next',
|
| 167 |
-
modelPath: 'Qwen/Qwen3-Coder-Next',
|
| 168 |
-
avatarUrl: 'https://huggingface.co/api/avatars/Qwen',
|
| 169 |
-
},
|
| 170 |
-
{
|
| 171 |
-
id: 'gemini-2.0-flash',
|
| 172 |
-
name: 'Gemini 2.0 Flash',
|
| 173 |
-
modelPath: 'openai/google/gemini-2.0-flash-001',
|
| 174 |
-
avatarUrl: 'https://huggingface.co/api/avatars/google',
|
| 175 |
-
},
|
| 176 |
-
{
|
| 177 |
-
id: 'deepseek-v4-flash-latest',
|
| 178 |
-
name: 'DeepSeek V4 Flash latest',
|
| 179 |
-
modelPath: '~deepseek/deepseek-v4-flash-latest',
|
| 180 |
-
avatarUrl: 'https://huggingface.co/api/avatars/deepseek',
|
| 181 |
-
recommended: true,
|
| 182 |
-
},
|
| 183 |
-
];"""
|
| 184 |
-
|
| 185 |
-
content = content[:match.start()] + NEW_MODELS + content[match.end():]
|
| 186 |
-
|
| 187 |
-
# Update DEFAULT_MODEL_PATH
|
| 188 |
-
dm_path = re.search(r'const DEFAULT_MODEL_PATH\s*=.*?;', content)
|
| 189 |
-
if dm_path:
|
| 190 |
-
content = content.replace(dm_path.group(0), "const DEFAULT_MODEL_PATH = 'openai/poolside/laguna-s-2.1:free';")
|
| 191 |
-
print("OK: Replaced DEFAULT_MODEL_PATH")
|
| 192 |
-
else:
|
| 193 |
-
print("OK: DEFAULT_MODEL_PATH not found")
|
| 194 |
-
|
| 195 |
-
with open(FILE, "w", encoding="utf-8") as f:
|
| 196 |
-
f.write(content)
|
| 197 |
-
|
| 198 |
-
print("OK: Frontend patched - Laguna S 2.1:free DEFAULT")
|
| 199 |
-
print(" Models: HY3:free, Gemma 4 31B:free, Llama 3.3 70B:free, Laguna M.1:free, Laguna S 2.1:free, stealth/space-bunny-alpha, Nex N2 Mini, Ling 3.0 Flash:free, Llama 3.1 8B, DeepSeek V4 Flash latest")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_frontend_apodex_ling_beatapi.py
DELETED
|
@@ -1,110 +0,0 @@
|
|
| 1 |
-
"""Patch frontend ChatInput.tsx: add apodex-1.1-mini:free, ling-3.1-flash and
|
| 2 |
-
the BeatAPI space-bunny-alpha entry to the model picker.
|
| 3 |
-
|
| 4 |
-
- apodex/apodex-1.1-mini:free path: openai/apodex/apodex-1.1-mini:free
|
| 5 |
-
- inclusionai/ling-3.1-flash path: openai/inclusionai/ling-3.1-flash
|
| 6 |
-
- BeatAPI space-bunny-alpha path: beatapi/space-bunny-alpha
|
| 7 |
-
|
| 8 |
-
The first two follow the same OpenRouter `openai/` convention as the existing
|
| 9 |
-
models (including stealth/space-bunny-alpha). The third is a BEATAPI model:
|
| 10 |
-
its path starts with `beatapi/`, NOT `openai/`, so the backend routes it to
|
| 11 |
-
https://api.beatapi.io/v1 with BEATAPI_API_KEY instead of OpenRouter. The name
|
| 12 |
-
is prefixed "BeatAPI" so the picker makes the provider difference visible.
|
| 13 |
-
|
| 14 |
-
Runs AFTER patch_frontend.py and patch_frontend_new_models.py (which already
|
| 15 |
-
replaced DEFAULT_MODEL_OPTIONS). Idempotent.
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
import os
|
| 19 |
-
import re
|
| 20 |
-
|
| 21 |
-
FILE = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 22 |
-
|
| 23 |
-
NEW_ENTRIES = """ {
|
| 24 |
-
id: 'apodex-1.1-mini-free',
|
| 25 |
-
name: 'apodex/apodex-1.1-mini:free',
|
| 26 |
-
modelPath: 'openai/apodex/apodex-1.1-mini:free',
|
| 27 |
-
avatarUrl: 'https://huggingface.co/api/avatars/apodex',
|
| 28 |
-
recommended: true,
|
| 29 |
-
},
|
| 30 |
-
{
|
| 31 |
-
id: 'ling-3.1-flash',
|
| 32 |
-
name: 'inclusionai/ling-3.1-flash',
|
| 33 |
-
modelPath: 'openai/inclusionai/ling-3.1-flash',
|
| 34 |
-
avatarUrl: 'https://huggingface.co/api/avatars/inclusionai',
|
| 35 |
-
recommended: true,
|
| 36 |
-
},
|
| 37 |
-
{
|
| 38 |
-
id: 'beatapi-space-bunny-alpha',
|
| 39 |
-
name: 'BeatAPI space-bunny-alpha',
|
| 40 |
-
modelPath: 'beatapi/space-bunny-alpha',
|
| 41 |
-
avatarUrl: 'https://beatapi.io/favicon.ico',
|
| 42 |
-
recommended: true,
|
| 43 |
-
},
|
| 44 |
-
"""
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def patch():
|
| 48 |
-
if not os.path.exists(FILE):
|
| 49 |
-
print(f"SKIP: {FILE} not found")
|
| 50 |
-
return
|
| 51 |
-
|
| 52 |
-
with open(FILE, "r", encoding="utf-8") as f:
|
| 53 |
-
content = f.read()
|
| 54 |
-
|
| 55 |
-
if (
|
| 56 |
-
"apodex/apodex-1.1-mini:free" in content
|
| 57 |
-
and "beatapi/space-bunny-alpha" in content
|
| 58 |
-
):
|
| 59 |
-
print("OK: new models already present in ChatInput.tsx")
|
| 60 |
-
return
|
| 61 |
-
|
| 62 |
-
# Insert right before the stealth/space-bunny-alpha entry so the three new
|
| 63 |
-
# models sit next to the existing free OpenRouter models.
|
| 64 |
-
anchor = " {\n id: 'space-bunny-alpha',\n"
|
| 65 |
-
if anchor in content:
|
| 66 |
-
content = content.replace(anchor, NEW_ENTRIES + anchor, 1)
|
| 67 |
-
print("OK: added 3 models before the stealth/space-bunny-alpha entry")
|
| 68 |
-
else:
|
| 69 |
-
# Fallback: insert before deepseek-v4-flash-latest, the other stable anchor.
|
| 70 |
-
alt = " {\n id: 'deepseek-v4-flash-latest',\n"
|
| 71 |
-
if alt in content:
|
| 72 |
-
content = content.replace(alt, NEW_ENTRIES + alt, 1)
|
| 73 |
-
print("OK: added 3 models before deepseek-v4-flash-latest (fallback)")
|
| 74 |
-
else:
|
| 75 |
-
m = re.search(
|
| 76 |
-
r"const DEFAULT_MODEL_OPTIONS: ModelOption\[\] = \[.*?\];",
|
| 77 |
-
content,
|
| 78 |
-
re.DOTALL,
|
| 79 |
-
)
|
| 80 |
-
if not m:
|
| 81 |
-
print("FAIL: could not find any model-list anchor")
|
| 82 |
-
return
|
| 83 |
-
head = m.group(0)[: m.group(0).rindex("[") + 1]
|
| 84 |
-
content = (
|
| 85 |
-
content[: m.start()]
|
| 86 |
-
+ head
|
| 87 |
-
+ "\n"
|
| 88 |
-
+ NEW_ENTRIES
|
| 89 |
-
+ content[m.start() + len(head) :]
|
| 90 |
-
)
|
| 91 |
-
print("OK: added 3 models at top of DEFAULT_MODEL_OPTIONS (fallback)")
|
| 92 |
-
|
| 93 |
-
with open(FILE, "w", encoding="utf-8") as f:
|
| 94 |
-
f.write(content)
|
| 95 |
-
print("OK: ChatInput.tsx patched")
|
| 96 |
-
|
| 97 |
-
# Verification: all three paths must be present.
|
| 98 |
-
for needle in (
|
| 99 |
-
"openai/apodex/apodex-1.1-mini:free",
|
| 100 |
-
"openai/inclusionai/ling-3.1-flash",
|
| 101 |
-
"beatapi/space-bunny-alpha",
|
| 102 |
-
):
|
| 103 |
-
if needle not in content:
|
| 104 |
-
print(f"FAIL: missing {needle}")
|
| 105 |
-
raise SystemExit(1)
|
| 106 |
-
print("OK: verified all 3 new model paths present")
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
if __name__ == "__main__":
|
| 110 |
-
patch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_frontend_multi.py
DELETED
|
@@ -1,120 +0,0 @@
|
|
| 1 |
-
"""Patch ChatInput.tsx: support uploading multiple files at once.
|
| 2 |
-
|
| 3 |
-
Changes:
|
| 4 |
-
1. Add `multiple` to the <input type="file"> so the user can select many
|
| 5 |
-
files/images in one pick.
|
| 6 |
-
2. Rewrite handleDatasetFileChange to send ALL selected files in a single
|
| 7 |
-
multipart request (backend returns list[DatasetUploadResponse]).
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
import os
|
| 11 |
-
|
| 12 |
-
FILE = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 13 |
-
|
| 14 |
-
def patch():
|
| 15 |
-
if not os.path.exists(FILE):
|
| 16 |
-
print(f"SKIP: {FILE} not found")
|
| 17 |
-
return
|
| 18 |
-
|
| 19 |
-
with open(FILE, "r", encoding="utf-8") as f:
|
| 20 |
-
content = f.read()
|
| 21 |
-
|
| 22 |
-
# ---- 1. Add `multiple` to the file input ----
|
| 23 |
-
old_input = '<input\n ref={fileInputRef}\n type="file"\n accept={DATASET_UPLOAD_ACCEPT}\n onChange={handleDatasetFileChange}\n style={{ display: \'none\' }}\n />'
|
| 24 |
-
new_input = '<input\n ref={fileInputRef}\n type="file"\n accept={DATASET_UPLOAD_ACCEPT}\n multiple\n onChange={handleDatasetFileChange}\n style={{ display: \'none\' }}\n />'
|
| 25 |
-
if old_input in content:
|
| 26 |
-
content = content.replace(old_input, new_input)
|
| 27 |
-
print("OK: added multiple to file input")
|
| 28 |
-
else:
|
| 29 |
-
print("WARN: file input block not matched exactly")
|
| 30 |
-
|
| 31 |
-
# ---- 2. Rewrite handleDatasetFileChange ----
|
| 32 |
-
old_handler_start = " const handleDatasetFileChange = useCallback("
|
| 33 |
-
start = content.find(old_handler_start)
|
| 34 |
-
if start == -1:
|
| 35 |
-
print("FAIL: handleDatasetFileChange not found")
|
| 36 |
-
return
|
| 37 |
-
# End of the handler: the " );" right after the deps line.
|
| 38 |
-
deps = " [sessionId, onDatasetUploaded],\n );"
|
| 39 |
-
end = content.find(deps, start)
|
| 40 |
-
if end != -1:
|
| 41 |
-
end += len(deps)
|
| 42 |
-
else:
|
| 43 |
-
print("FAIL: could not find end of handler")
|
| 44 |
-
return
|
| 45 |
-
|
| 46 |
-
new_handler = ''' const handleDatasetFileChange = useCallback(
|
| 47 |
-
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
| 48 |
-
const files = Array.from(event.target.files ?? []);
|
| 49 |
-
event.target.value = '';
|
| 50 |
-
if (!files.length) return;
|
| 51 |
-
|
| 52 |
-
if (!sessionId) {
|
| 53 |
-
setDatasetUploadError('Start a session before uploading files.');
|
| 54 |
-
return;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
// Validate each file before uploading the batch.
|
| 58 |
-
for (const file of files) {
|
| 59 |
-
const ext = file.name.split('.').pop()?.toLowerCase() || '';
|
| 60 |
-
if (!DATASET_UPLOAD_EXTENSIONS.has(ext)) {
|
| 61 |
-
setDatasetUploadError('Không hỗ trợ định dạng này. Các định dạng được hỗ trợ: CSV, JSON, JSONL, PDF, ảnh (PNG/JPG/JPEG/WebP/BMP/TIFF/GIF), Excel (XLSX/XLSM/XLS), Word (DOCX/DOC), PowerPoint (PPTX), OpenDocument (ODT/ODS/ODP), và văn bản (TXT/MD/RTF/LOG).');
|
| 62 |
-
return;
|
| 63 |
-
}
|
| 64 |
-
if (file.size > MAX_DATASET_UPLOAD_BYTES) {
|
| 65 |
-
setDatasetUploadError(`File must be 100 MB or smaller. ${file.name} is ${formatBytes(file.size)}.`);
|
| 66 |
-
return;
|
| 67 |
-
}
|
| 68 |
-
if (file.size === 0) {
|
| 69 |
-
setDatasetUploadError(`Uploaded file is empty: ${file.name}`);
|
| 70 |
-
return;
|
| 71 |
-
}
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
// Send all files in one request; backend returns a list.
|
| 75 |
-
const formData = new FormData();
|
| 76 |
-
for (const file of files) {
|
| 77 |
-
formData.append('file', file);
|
| 78 |
-
}
|
| 79 |
-
setIsUploadingDataset(true);
|
| 80 |
-
setDatasetUploadProgress(0);
|
| 81 |
-
setDatasetUploadError(null);
|
| 82 |
-
setDatasetUploadSuccess(null);
|
| 83 |
-
try {
|
| 84 |
-
const res = await apiUpload(`/api/session/${sessionId}/datasets`, formData, {
|
| 85 |
-
onProgress: ({ percent }) => {
|
| 86 |
-
setDatasetUploadProgress(percent !== null && percent < 100 ? percent : null);
|
| 87 |
-
},
|
| 88 |
-
});
|
| 89 |
-
if (!res.ok) {
|
| 90 |
-
setDatasetUploadError(await readApiErrorMessage(res, 'File upload failed.'));
|
| 91 |
-
return;
|
| 92 |
-
}
|
| 93 |
-
const payload = await res.json() as DatasetUploadResponse[];
|
| 94 |
-
if (payload.length) {
|
| 95 |
-
setUploadedDatasets((previous) => [...payload, ...previous]);
|
| 96 |
-
const names = payload.map((u) => u.filename).join(', ');
|
| 97 |
-
setDatasetUploadSuccess(`Đã tải lên ${payload.length} file: ${names}`);
|
| 98 |
-
}
|
| 99 |
-
await onDatasetUploaded?.();
|
| 100 |
-
} catch (error) {
|
| 101 |
-
setDatasetUploadError(
|
| 102 |
-
error instanceof Error ? error.message : 'File upload failed.'
|
| 103 |
-
);
|
| 104 |
-
} finally {
|
| 105 |
-
setIsUploadingDataset(false);
|
| 106 |
-
setDatasetUploadProgress(null);
|
| 107 |
-
}
|
| 108 |
-
},
|
| 109 |
-
[sessionId, onDatasetUploaded],
|
| 110 |
-
);'''
|
| 111 |
-
|
| 112 |
-
content = content[:start] + new_handler + content[end:]
|
| 113 |
-
|
| 114 |
-
with open(FILE, "w", encoding="utf-8") as f:
|
| 115 |
-
f.write(content)
|
| 116 |
-
print("OK: ChatInput.tsx multi-file upload patched")
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
if __name__ == "__main__":
|
| 120 |
-
patch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_frontend_new_models.py
DELETED
|
@@ -1,71 +0,0 @@
|
|
| 1 |
-
"""Patch frontend ChatInput.tsx: add 3 new OpenRouter models to the model picker.
|
| 2 |
-
|
| 3 |
-
New models (openai/ prefix routes to OpenRouter):
|
| 4 |
-
- UPSTAGE Solar Pro 4 (openai/upstage/solar-pro4)
|
| 5 |
-
- NVIDIA Nemotron 3.5 Lightning:free (openai/nvidia/nemotron-3.5-lightning:free)
|
| 6 |
-
- Liquid LFM 2.5 2.6B:free (openai/liquid/lfm-2.5-2.6b:free)
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import os
|
| 10 |
-
|
| 11 |
-
FILE = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 12 |
-
|
| 13 |
-
def patch():
|
| 14 |
-
if not os.path.exists(FILE):
|
| 15 |
-
print(f"SKIP: {FILE} not found")
|
| 16 |
-
return
|
| 17 |
-
|
| 18 |
-
with open(FILE, "r", encoding="utf-8") as f:
|
| 19 |
-
content = f.read()
|
| 20 |
-
|
| 21 |
-
if "upstage-solar-pro4" in content:
|
| 22 |
-
print("OK: models already present")
|
| 23 |
-
return
|
| 24 |
-
|
| 25 |
-
# Unique anchor: the deepseek-v4-flash-latest entry inside DEFAULT_MODEL_OPTIONS.
|
| 26 |
-
# Insert the 3 new model objects immediately BEFORE that entry.
|
| 27 |
-
anchor = " {\n id: 'deepseek-v4-flash-latest',\n"
|
| 28 |
-
new_entries = (
|
| 29 |
-
" {\n"
|
| 30 |
-
" id: 'upstage-solar-pro4',\n"
|
| 31 |
-
" name: 'UPSTAGE Solar Pro 4',\n"
|
| 32 |
-
" modelPath: 'openai/upstage/solar-pro4',\n"
|
| 33 |
-
" avatarUrl: 'https://huggingface.co/api/avatars/upstage',\n"
|
| 34 |
-
" recommended: true,\n"
|
| 35 |
-
" },\n"
|
| 36 |
-
" {\n"
|
| 37 |
-
" id: 'nemotron-3.5-lightning',\n"
|
| 38 |
-
" name: 'NVIDIA Nemotron 3.5 Lightning:free',\n"
|
| 39 |
-
" modelPath: 'openai/nvidia/nemotron-3.5-lightning:free',\n"
|
| 40 |
-
" avatarUrl: 'https://huggingface.co/api/avatars/nvidia',\n"
|
| 41 |
-
" recommended: true,\n"
|
| 42 |
-
" },\n"
|
| 43 |
-
" {\n"
|
| 44 |
-
" id: 'liquid-lfm-2.5-2.6b',\n"
|
| 45 |
-
" name: 'Liquid LFM 2.5 2.6B:free',\n"
|
| 46 |
-
" modelPath: 'openai/liquid/lfm-2.5-2.6b:free',\n"
|
| 47 |
-
" avatarUrl: 'https://huggingface.co/api/avatars/liquid',\n"
|
| 48 |
-
" recommended: true,\n"
|
| 49 |
-
" },\n"
|
| 50 |
-
)
|
| 51 |
-
|
| 52 |
-
if anchor in content:
|
| 53 |
-
content = content.replace(anchor, new_entries + anchor, 1)
|
| 54 |
-
print("OK: added 3 new models before deepseek-v4-flash-latest entry")
|
| 55 |
-
else:
|
| 56 |
-
# Fallback: find the deepseek id line by itself.
|
| 57 |
-
alt = " id: 'deepseek-v4-flash-latest',\n"
|
| 58 |
-
if alt in content:
|
| 59 |
-
content = content.replace(alt, new_entries + " {\n" + alt, 1)
|
| 60 |
-
print("OK: added 3 new models (fallback anchor)")
|
| 61 |
-
else:
|
| 62 |
-
print("FAIL: could not find insertion anchor in DEFAULT_MODEL_OPTIONS")
|
| 63 |
-
return
|
| 64 |
-
|
| 65 |
-
with open(FILE, "w", encoding="utf-8") as f:
|
| 66 |
-
f.write(content)
|
| 67 |
-
print("OK: ChatInput.tsx new models patched")
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
if __name__ == "__main__":
|
| 71 |
-
patch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_frontend_permanent_sessions.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Patch frontend: Disable 'where we here now?' notification.
|
| 3 |
-
|
| 4 |
-
Chỉ tìm và xóa bất kỳ chuỗi 'where we here now' nào trong frontend source.
|
| 5 |
-
An toàn — không thêm code TypeScript mới để tránh lỗi build.
|
| 6 |
-
"""
|
| 7 |
-
import os
|
| 8 |
-
import sys
|
| 9 |
-
|
| 10 |
-
FRONTEND_DIR = "/source/frontend/src"
|
| 11 |
-
|
| 12 |
-
patch_count = 0
|
| 13 |
-
|
| 14 |
-
# Scan all .ts/.tsx files for 'where we here now' and remove the notification
|
| 15 |
-
if os.path.exists(FRONTEND_DIR):
|
| 16 |
-
for root, dirs, files in os.walk(FRONTEND_DIR):
|
| 17 |
-
for fname in files:
|
| 18 |
-
if fname.endswith((".ts", ".tsx")):
|
| 19 |
-
fpath = os.path.join(root, fname)
|
| 20 |
-
try:
|
| 21 |
-
with open(fpath, "r", encoding="utf-8") as f:
|
| 22 |
-
content = f.read()
|
| 23 |
-
if "where we here now" in content.lower():
|
| 24 |
-
# Remove the entire line containing the notification text
|
| 25 |
-
lines = content.split("\n")
|
| 26 |
-
new_lines = []
|
| 27 |
-
removed = False
|
| 28 |
-
for line in lines:
|
| 29 |
-
if "where we here now" in line.lower():
|
| 30 |
-
removed = True
|
| 31 |
-
continue # Skip this line
|
| 32 |
-
new_lines.append(line)
|
| 33 |
-
if removed:
|
| 34 |
-
with open(fpath, "w", encoding="utf-8") as f:
|
| 35 |
-
f.write("\n".join(new_lines))
|
| 36 |
-
patch_count += 1
|
| 37 |
-
print(f" Removed 'where we here now?' from {fpath}")
|
| 38 |
-
except Exception as e:
|
| 39 |
-
print(f" WARN: Could not patch {fpath}: {e}")
|
| 40 |
-
|
| 41 |
-
if patch_count == 0:
|
| 42 |
-
print(" No 'where we here now?' notifications found (already clean)")
|
| 43 |
-
|
| 44 |
-
print(f"Done: patched {patch_count} frontend files")
|
| 45 |
-
sys.exit(0) # Always exit 0 — never fail build
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_frontend_uploads.py
DELETED
|
@@ -1,81 +0,0 @@
|
|
| 1 |
-
"""Patch ChatInput.tsx: allow uploading PDF, images, Excel, Word, PPT,
|
| 2 |
-
OpenDocument and text files in addition to CSV/JSON/JSONL, and surface the
|
| 3 |
-
extracted-text fields returned by the backend.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
|
| 8 |
-
FILE = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 9 |
-
|
| 10 |
-
ACCEPT = (
|
| 11 |
-
".csv,.json,.jsonl,.pdf,"
|
| 12 |
-
".png,.jpg,.jpeg,.webp,.bmp,.tiff,.gif,"
|
| 13 |
-
".xlsx,.xlsm,.xls,"
|
| 14 |
-
".docx,.doc,"
|
| 15 |
-
".pptx,"
|
| 16 |
-
".odt,.ods,.odp,"
|
| 17 |
-
".txt,.md,.rtf,.log"
|
| 18 |
-
)
|
| 19 |
-
|
| 20 |
-
EXTENSIONS_SET = (
|
| 21 |
-
"['csv', 'json', 'jsonl', 'pdf', "
|
| 22 |
-
"'png', 'jpg', 'jpeg', 'webp', 'bmp', 'tiff', 'tif', 'gif', "
|
| 23 |
-
"'xlsx', 'xlsm', 'xls', 'docx', 'doc', 'pptx', "
|
| 24 |
-
"'odt', 'ods', 'odp', 'txt', 'md', 'rtf', 'log']"
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def patch():
|
| 29 |
-
if not os.path.exists(FILE):
|
| 30 |
-
print(f"SKIP: {FILE} not found")
|
| 31 |
-
return
|
| 32 |
-
|
| 33 |
-
with open(FILE, "r", encoding="utf-8") as f:
|
| 34 |
-
content = f.read()
|
| 35 |
-
|
| 36 |
-
# 1. Widen the accept string
|
| 37 |
-
old_accept = "const DATASET_UPLOAD_ACCEPT = '.csv,.json,.jsonl';"
|
| 38 |
-
if old_accept in content:
|
| 39 |
-
content = content.replace(old_accept, f"const DATASET_UPLOAD_ACCEPT = `{ACCEPT}`;")
|
| 40 |
-
print("OK: widened DATASET_UPLOAD_ACCEPT")
|
| 41 |
-
|
| 42 |
-
# 2. Widen the allowed extension set
|
| 43 |
-
old_ext = "const DATASET_UPLOAD_EXTENSIONS = new Set(['csv', 'json', 'jsonl']);"
|
| 44 |
-
if old_ext in content:
|
| 45 |
-
content = content.replace(old_ext, f"const DATASET_UPLOAD_EXTENSIONS = new Set({EXTENSIONS_SET});")
|
| 46 |
-
print("OK: widened DATASET_UPLOAD_EXTENSIONS")
|
| 47 |
-
|
| 48 |
-
# 3. Update the error message
|
| 49 |
-
old_err = "'Only CSV, JSON, and JSONL dataset files are supported.'"
|
| 50 |
-
new_err = (
|
| 51 |
-
"'Không hỗ trợ định dạng này. Các định dạng được hỗ trợ: CSV, JSON, JSONL, "
|
| 52 |
-
"PDF, ảnh (PNG/JPG/JPEG/WebP/BMP/TIFF/GIF), Excel (XLSX/XLSM/XLS), "
|
| 53 |
-
"Word (DOCX/DOC), PowerPoint (PPTX), OpenDocument (ODT/ODS/ODP), "
|
| 54 |
-
"và văn bản (TXT/MD/RTF/LOG).'"
|
| 55 |
-
)
|
| 56 |
-
if old_err in content:
|
| 57 |
-
content = content.replace(old_err, new_err)
|
| 58 |
-
print("OK: updated unsupported-format error message")
|
| 59 |
-
|
| 60 |
-
# 4. Widen the DatasetUploadResponse interface format + add extracted fields
|
| 61 |
-
old_fmt = " format: 'csv' | 'json' | 'jsonl';"
|
| 62 |
-
if old_fmt in content:
|
| 63 |
-
content = content.replace(old_fmt, " format: string;")
|
| 64 |
-
print("OK: widened interface format type")
|
| 65 |
-
|
| 66 |
-
if "extracted_text?: string" not in content:
|
| 67 |
-
content = content.replace(
|
| 68 |
-
" load_dataset_snippet: string;\n",
|
| 69 |
-
" load_dataset_snippet: string;\n"
|
| 70 |
-
" extracted_text?: string;\n"
|
| 71 |
-
" extraction_warning?: string;\n",
|
| 72 |
-
)
|
| 73 |
-
print("OK: added extracted_text/extraction_warning to interface")
|
| 74 |
-
|
| 75 |
-
with open(FILE, "w", encoding="utf-8") as f:
|
| 76 |
-
f.write(content)
|
| 77 |
-
print("OK: ChatInput.tsx patched for multi-format uploads")
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
if __name__ == "__main__":
|
| 81 |
-
patch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_frontend_xing_nemotron_vl.py
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 1 |
-
"""Patch frontend ChatInput.tsx: add xingchen-agi/xing4.0-29b-a4b:free and
|
| 2 |
-
nvidia/nemotron-3.5-super-vl:free with the same openai/ path convention as
|
| 3 |
-
Laguna S 2.1:free.
|
| 4 |
-
|
| 5 |
-
New models (openai/ prefix -> OpenRouter):
|
| 6 |
-
- xingchen-agi/xing4.0-29b-a4b:free modelPath: openai/xingchen-agi/xing4.0-29b-a4b:free
|
| 7 |
-
- nvidia/nemotron-3.5-super-vl:free modelPath: openai/nvidia/nemotron-3.5-super-vl:free
|
| 8 |
-
|
| 9 |
-
Runs AFTER patch_frontend.py and patch_frontend_new_models.py (which already
|
| 10 |
-
replaced DEFAULT_MODEL_OPTIONS). Idempotent.
|
| 11 |
-
"""
|
| 12 |
-
|
| 13 |
-
import os
|
| 14 |
-
|
| 15 |
-
FILE = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def patch():
|
| 19 |
-
if not os.path.exists(FILE):
|
| 20 |
-
print(f"SKIP: {FILE} not found")
|
| 21 |
-
return
|
| 22 |
-
|
| 23 |
-
with open(FILE, "r", encoding="utf-8") as f:
|
| 24 |
-
content = f.read()
|
| 25 |
-
|
| 26 |
-
if "xingchen-agi/xing4.0-29b-a4b" in content:
|
| 27 |
-
print("OK: models already present in ChatInput.tsx")
|
| 28 |
-
return
|
| 29 |
-
|
| 30 |
-
# Insert right before the deepseek-v4-flash-latest entry — a stable anchor
|
| 31 |
-
# that exists after patch_frontend.py rewrites DEFAULT_MODEL_OPTIONS.
|
| 32 |
-
anchor = " {\n id: 'deepseek-v4-flash-latest',\n"
|
| 33 |
-
new_entries = (
|
| 34 |
-
" {\n"
|
| 35 |
-
" id: 'xingchen-xing4-0-29b',\n"
|
| 36 |
-
" name: 'xingchen-agi/xing4.0-29b-a4b:free',\n"
|
| 37 |
-
" modelPath: 'openai/xingchen-agi/xing4.0-29b-a4b:free',\n"
|
| 38 |
-
" avatarUrl: 'https://huggingface.co/api/avatars/xingchen-agi',\n"
|
| 39 |
-
" recommended: true,\n"
|
| 40 |
-
" },\n"
|
| 41 |
-
" {\n"
|
| 42 |
-
" id: 'nemotron-3.5-super-vl',\n"
|
| 43 |
-
" name: 'nvidia/nemotron-3.5-super-vl:free',\n"
|
| 44 |
-
" modelPath: 'openai/nvidia/nemotron-3.5-super-vl:free',\n"
|
| 45 |
-
" avatarUrl: 'https://huggingface.co/api/avatars/nvidia',\n"
|
| 46 |
-
" recommended: true,\n"
|
| 47 |
-
" },\n"
|
| 48 |
-
)
|
| 49 |
-
|
| 50 |
-
if anchor in content:
|
| 51 |
-
content = content.replace(anchor, new_entries + anchor, 1)
|
| 52 |
-
print("OK: added 2 models before deepseek-v4-flash-latest entry")
|
| 53 |
-
else:
|
| 54 |
-
print("FAIL: could not find insertion anchor in DEFAULT_MODEL_OPTIONS")
|
| 55 |
-
return
|
| 56 |
-
|
| 57 |
-
with open(FILE, "w", encoding="utf-8") as f:
|
| 58 |
-
f.write(content)
|
| 59 |
-
print("OK: ChatInput.tsx patched")
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
if __name__ == "__main__":
|
| 63 |
-
patch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|