Spaces:
Running
Running
Restore Space to commit 36e2e7f8
#1
by bep40 - opened
- AUTO_CONTINUE_FEATURE.md +0 -183
- BUILD_TRIGGER +0 -1
- Dockerfile +38 -35
- README.md +327 -20
- configs/__init__.py +0 -0
- configs/cli_agent_config.json +22 -0
- configs/frontend_agent_config.json +4 -4
- fix_space_error_report.json +0 -9
- patch_agent_backend.py +0 -89
- patch_agent_loop.py +0 -74
- 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_chat_input.py +0 -105
- patch_frontend.py +0 -184
- patch_models.py +0 -212
- patch_sse_transport.py +0 -32
- patch_use_agent_chat_full.py +0 -121
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 |
-
trigger
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,57 +1,60 @@
|
|
| 1 |
-
#
|
| 2 |
FROM node:20-alpine AS frontend-builder
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
RUN python3 /tmp/patch_frontend.py
|
| 9 |
-
|
| 10 |
-
# Patch auto-continue V17
|
| 11 |
-
COPY patch_auto_continue_v17.py /tmp/patch_auto_continue_v17.py
|
| 12 |
-
RUN python3 /tmp/patch_auto_continue_v17.py
|
| 13 |
-
|
| 14 |
-
WORKDIR /source/frontend
|
| 15 |
-
RUN npm config set fetch-timeout 120000 && \
|
| 16 |
-
npm config set fetch-retries 3 && \
|
| 17 |
-
npm install && \
|
| 18 |
-
npm run build
|
| 19 |
|
| 20 |
# Stage 2: Production
|
| 21 |
FROM python:3.12-slim
|
|
|
|
|
|
|
| 22 |
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
|
|
|
|
|
| 23 |
RUN useradd -m -u 1000 user
|
|
|
|
| 24 |
WORKDIR /app
|
| 25 |
-
RUN apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/*
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
rm -rf /
|
| 33 |
|
| 34 |
-
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
|
|
|
| 38 |
|
| 39 |
-
#
|
| 40 |
-
COPY
|
| 41 |
-
|
|
|
|
| 42 |
|
| 43 |
-
#
|
| 44 |
-
COPY
|
| 45 |
-
RUN python /tmp/patch_agent_backend.py
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
| 48 |
|
|
|
|
| 49 |
USER user
|
|
|
|
|
|
|
| 50 |
ENV HOME=/home/user \
|
| 51 |
PYTHONUNBUFFERED=1 \
|
| 52 |
PYTHONPATH=/app \
|
| 53 |
PATH="/app/.venv/bin:$PATH"
|
| 54 |
|
|
|
|
| 55 |
EXPOSE 7860
|
|
|
|
|
|
|
| 56 |
WORKDIR /app/backend
|
| 57 |
-
CMD ["
|
|
|
|
| 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,33 +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)
|
| 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 |
---
|
| 23 |
|
| 24 |
-
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
```
|
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,16 +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 |
"mcpServers": {
|
| 11 |
"hf-mcp-server": {
|
| 12 |
"transport": "http",
|
| 13 |
-
"url": "https://
|
| 14 |
}
|
| 15 |
}
|
| 16 |
}
|
|
|
|
| 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,9 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"status": "fixed_uploaded",
|
| 3 |
-
"space": "bep40/ml-intern",
|
| 4 |
-
"space_url": "https://huggingface.co/spaces/bep40/ml-intern",
|
| 5 |
-
"root_cause": "Dockerfile runtime stage used CMD [\"bash\", \"start.sh\"] on python:3.12-slim, which does not include bash by default. This causes container startup failure and Space error after a successful build.",
|
| 6 |
-
"fix": "Changed Dockerfile CMD to [\"sh\", \"start.sh\"] and added ca-certificates to apt packages. start.sh is POSIX-compatible for this use.",
|
| 7 |
-
"files_changed": ["Dockerfile", "README.md"],
|
| 8 |
-
"verification": "Repo files updated and rebuild triggered. Live runtime verification via HF Jobs/sandbox was blocked by namespace credit/duplication limits in this session; check Space build logs if HF still reports error."
|
| 9 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_agent_backend.py
DELETED
|
@@ -1,89 +0,0 @@
|
|
| 1 |
-
"""Patch: agent_loop.py task detection + auto-approve for free models."""
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
import ast
|
| 5 |
-
|
| 6 |
-
AGENT_LOOP = "/app/agent/core/agent_loop.py"
|
| 7 |
-
AGENT_FILE = "/app/backend/routes/agent.py"
|
| 8 |
-
SESSION_FILE = "/app/agent/core/session.py"
|
| 9 |
-
|
| 10 |
-
# ===== PATCH 1: agent_loop.py - detect model stopped mid-task =====
|
| 11 |
-
with open(AGENT_LOOP) as f:
|
| 12 |
-
c = f.read()
|
| 13 |
-
|
| 14 |
-
# Add helper function before class Handlers
|
| 15 |
-
if "_unfinished_plan" not in c:
|
| 16 |
-
fn = (
|
| 17 |
-
"\n\ndef _unfinished_plan(s):\n"
|
| 18 |
-
" p = getattr(s, 'current_plan', None) or []\n"
|
| 19 |
-
" return [it for it in p if it.get('status') in ('pending', 'in_progress')]\n\n"
|
| 20 |
-
)
|
| 21 |
-
c = c.replace("class Handlers:", fn + "class Handlers:")
|
| 22 |
-
print("OK: Added _unfinished_plan()")
|
| 23 |
-
|
| 24 |
-
# Add detection: if model produced no tool calls but content still exists,
|
| 25 |
-
# and plan items are still pending/in_progress, send session_update event
|
| 26 |
-
if "ac_plan" not in c:
|
| 27 |
-
check = (
|
| 28 |
-
"\n # Auto-continue: detect model stopped mid-task\n"
|
| 29 |
-
" if not llm_result.tool_calls_acc and llm_result.content:\n"
|
| 30 |
-
" unfinished = _unfinished_plan(session)\n"
|
| 31 |
-
" if unfinished:\n"
|
| 32 |
-
" await session.send_event(Event(event_type='session_update', data={\n"
|
| 33 |
-
" 'ac_plan': unfinished,\n"
|
| 34 |
-
" }))\n"
|
| 35 |
-
)
|
| 36 |
-
c = c.replace(" # -- End of turn --", check + " # -- End of turn --")
|
| 37 |
-
print("OK: Added mid-task detection")
|
| 38 |
-
|
| 39 |
-
try:
|
| 40 |
-
ast.parse(c)
|
| 41 |
-
with open(AGENT_LOOP, "w") as f:
|
| 42 |
-
f.write(c)
|
| 43 |
-
print("OK: agent_loop.py patched")
|
| 44 |
-
except SyntaxError as e:
|
| 45 |
-
print(f"FAIL: agent_loop.py syntax: {e}")
|
| 46 |
-
|
| 47 |
-
# ===== PATCH 2: Enable auto_approval for ALL sessions by default =====
|
| 48 |
-
# So free models don't block on user approval
|
| 49 |
-
with open(SESSION_FILE) as f:
|
| 50 |
-
c = f.read()
|
| 51 |
-
|
| 52 |
-
if "AUTO_APPROVE" not in c:
|
| 53 |
-
# Find where auto_approval is set on the session object
|
| 54 |
-
# Patch: when creating a new session, set auto_approval.enabled = True
|
| 55 |
-
auto_patch = (
|
| 56 |
-
"\n # === PATCH: Auto-approve for free models ===\n"
|
| 57 |
-
" self.auto_approval = {\n"
|
| 58 |
-
' "enabled": True,\n'
|
| 59 |
-
' "cost_cap_usd": None,\n'
|
| 60 |
-
' "estimated_spend_usd": 0.0,\n'
|
| 61 |
-
' "remaining_usd": None,\n'
|
| 62 |
-
" }\n"
|
| 63 |
-
)
|
| 64 |
-
|
| 65 |
-
# Find session init method
|
| 66 |
-
init_match = "class Session:"
|
| 67 |
-
if init_match in c:
|
| 68 |
-
# Insert after the class definition, find __init__
|
| 69 |
-
init_found = "def __init__"
|
| 70 |
-
if init_found in c:
|
| 71 |
-
# Find first line inside __init__ that's an assignment
|
| 72 |
-
init_start = c.find(init_found)
|
| 73 |
-
body_start = c.find("\n", init_start) + 1
|
| 74 |
-
body_start = c.find(" ", body_start) # first indented line
|
| 75 |
-
c = c[:body_start] + auto_patch + c[body_start:]
|
| 76 |
-
print("OK: Added auto_approve default")
|
| 77 |
-
|
| 78 |
-
try:
|
| 79 |
-
ast.parse(c)
|
| 80 |
-
with open(SESSION_FILE, "w") as f:
|
| 81 |
-
f.write(c)
|
| 82 |
-
print("OK: session.py patched")
|
| 83 |
-
except SyntaxError as e:
|
| 84 |
-
print(f"FAIL: session.py syntax: {e}")
|
| 85 |
-
# Don't block on this patch
|
| 86 |
-
else:
|
| 87 |
-
print("OK: session.py already has auto_approve")
|
| 88 |
-
|
| 89 |
-
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_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_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_frontend.py
DELETED
|
@@ -1,184 +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 |
-
with open(MODEL_FILE, "w") as f:
|
| 47 |
-
f.write(model_content)
|
| 48 |
-
print("OK: model.ts constants updated")
|
| 49 |
-
|
| 50 |
-
# === Step 2: Patch ChatInput.tsx ===
|
| 51 |
-
with open(FILE, "r", encoding="utf-8") as f:
|
| 52 |
-
content = f.read()
|
| 53 |
-
|
| 54 |
-
# Fix imports
|
| 55 |
-
import_pattern = re.compile(
|
| 56 |
-
r'import\s*\{[^}]+\}\s*from\s*[\'"]@/utils/model[\'"];'
|
| 57 |
-
)
|
| 58 |
-
match = import_pattern.search(content)
|
| 59 |
-
if match:
|
| 60 |
-
content = content.replace(match.group(0), "import { isClaudePath } from '@/utils/model';")
|
| 61 |
-
print("OK: Replaced imports")
|
| 62 |
-
|
| 63 |
-
# Remove Owl Alpha references
|
| 64 |
-
content = content.replace("'Owl Alpha'", "'Claude Opus 4.8'")
|
| 65 |
-
content = content.replace('"Owl Alpha"', '"Claude Opus 4.8"')
|
| 66 |
-
|
| 67 |
-
# Row 3: Kimi → DeepSeek V4 Pro
|
| 68 |
-
content = content.replace("'Kimi K2.7 Code'", "'DeepSeek V4 Pro'")
|
| 69 |
-
content = content.replace('"Kimi K2.7 Code"', '"DeepSeek V4 Pro"')
|
| 70 |
-
|
| 71 |
-
# Row 4: MiniMax → DeepSeek V4 Flash
|
| 72 |
-
content = content.replace("'MiniMax M3'", "'DeepSeek V4 Flash'")
|
| 73 |
-
content = content.replace('"MiniMax M3"', '"DeepSeek V4 Flash"')
|
| 74 |
-
|
| 75 |
-
# Row 5: GLM → DeepSeek V4 Flash
|
| 76 |
-
content = content.replace("'GLM 5.2'", "'DeepSeek V4 Flash'")
|
| 77 |
-
content = content.replace('"GLM 5.2"', '"DeepSeek V4 Flash"')
|
| 78 |
-
|
| 79 |
-
# Row 6: DeepSeek V4 Pro → Nemotron
|
| 80 |
-
content = content.replace(
|
| 81 |
-
" {\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 },",
|
| 82 |
-
" {\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 },"
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
# Replace entire DEFAULT_MODEL_OPTIONS array
|
| 86 |
-
pattern = r'const DEFAULT_MODEL_OPTIONS: ModelOption\[\] = \[.*?\];'
|
| 87 |
-
match = re.search(pattern, content, re.DOTALL)
|
| 88 |
-
|
| 89 |
-
if not match:
|
| 90 |
-
print("FAIL: Could not find DEFAULT_MODEL_OPTIONS array!")
|
| 91 |
-
sys.exit(1)
|
| 92 |
-
|
| 93 |
-
NEW_MODELS = """const DEFAULT_MODEL_OPTIONS: ModelOption[] = [
|
| 94 |
-
{
|
| 95 |
-
id: 'tencent-hy3',
|
| 96 |
-
name: 'Tencent HY3:free',
|
| 97 |
-
modelPath: 'openai/tencent/hy3:free',
|
| 98 |
-
avatarUrl: 'https://huggingface.co/api/avatars/tencent',
|
| 99 |
-
recommended: true,
|
| 100 |
-
},
|
| 101 |
-
{
|
| 102 |
-
id: 'gemma-4-31b',
|
| 103 |
-
name: 'Gemma 4 31B:free',
|
| 104 |
-
modelPath: 'openai/google/gemma-4-31b-it:free',
|
| 105 |
-
avatarUrl: 'https://huggingface.co/api/avatars/google',
|
| 106 |
-
recommended: true,
|
| 107 |
-
},
|
| 108 |
-
{
|
| 109 |
-
id: 'llama-3.3-70b',
|
| 110 |
-
name: 'Llama 3.3 70B:free',
|
| 111 |
-
modelPath: 'openai/meta-llama/llama-3.3-70b-instruct:free',
|
| 112 |
-
avatarUrl: 'https://huggingface.co/api/avatars/meta-llama',
|
| 113 |
-
recommended: true,
|
| 114 |
-
},
|
| 115 |
-
{
|
| 116 |
-
id: 'laguna-m1',
|
| 117 |
-
name: 'Laguna M.1:free',
|
| 118 |
-
modelPath: 'openai/poolside/laguna-m.1:free',
|
| 119 |
-
avatarUrl: 'https://huggingface.co/api/avatars/poolside',
|
| 120 |
-
recommended: true,
|
| 121 |
-
},
|
| 122 |
-
{
|
| 123 |
-
id: 'laguna-s21',
|
| 124 |
-
name: 'Laguna S 2.1:free',
|
| 125 |
-
modelPath: 'openai/poolside/laguna-s-2.1:free',
|
| 126 |
-
avatarUrl: 'https://huggingface.co/api/avatars/poolside',
|
| 127 |
-
recommended: true,
|
| 128 |
-
},
|
| 129 |
-
{
|
| 130 |
-
id: 'nex-n2-mini',
|
| 131 |
-
name: 'nex-agi/nex-n2-mini',
|
| 132 |
-
modelPath: 'openai/nex-agi/nex-n2-mini',
|
| 133 |
-
avatarUrl: 'https://huggingface.co/api/avatars/nex-agi',
|
| 134 |
-
recommended: true,
|
| 135 |
-
},
|
| 136 |
-
{
|
| 137 |
-
id: 'ling-3.0-flash',
|
| 138 |
-
name: 'inclusionai/ling-3.0-flash:free',
|
| 139 |
-
modelPath: 'openai/inclusionai/ling-3.0-flash:free',
|
| 140 |
-
avatarUrl: 'https://huggingface.co/api/avatars/inclusionai',
|
| 141 |
-
recommended: true,
|
| 142 |
-
},
|
| 143 |
-
{
|
| 144 |
-
id: 'llama-3.1-8b',
|
| 145 |
-
name: 'Llama 3.1 8B',
|
| 146 |
-
modelPath: 'openai/meta-llama/llama-3.1-8b-instruct',
|
| 147 |
-
avatarUrl: 'https://huggingface.co/api/avatars/meta-llama',
|
| 148 |
-
},
|
| 149 |
-
{
|
| 150 |
-
id: 'qwen3-coder-next',
|
| 151 |
-
name: 'Qwen3 Coder Next',
|
| 152 |
-
modelPath: 'Qwen/Qwen3-Coder-Next',
|
| 153 |
-
avatarUrl: 'https://huggingface.co/api/avatars/Qwen',
|
| 154 |
-
},
|
| 155 |
-
{
|
| 156 |
-
id: 'gemini-2.0-flash',
|
| 157 |
-
name: 'Gemini 2.0 Flash',
|
| 158 |
-
modelPath: 'openai/google/gemini-2.0-flash-001',
|
| 159 |
-
avatarUrl: 'https://huggingface.co/api/avatars/google',
|
| 160 |
-
},
|
| 161 |
-
{
|
| 162 |
-
id: 'deepseek-v4-flash-latest',
|
| 163 |
-
name: 'DeepSeek V4 Flash latest',
|
| 164 |
-
modelPath: '~deepseek/deepseek-v4-flash-latest',
|
| 165 |
-
avatarUrl: 'https://huggingface.co/api/avatars/deepseek',
|
| 166 |
-
recommended: true,
|
| 167 |
-
},
|
| 168 |
-
];"""
|
| 169 |
-
|
| 170 |
-
content = content[:match.start()] + NEW_MODELS + content[match.end():]
|
| 171 |
-
|
| 172 |
-
# Update DEFAULT_MODEL_PATH
|
| 173 |
-
dm_path = re.search(r'const DEFAULT_MODEL_PATH\s*=.*?;', content)
|
| 174 |
-
if dm_path:
|
| 175 |
-
content = content.replace(dm_path.group(0), "const DEFAULT_MODEL_PATH = 'openai/tencent/hy3:free';")
|
| 176 |
-
print("OK: Replaced DEFAULT_MODEL_PATH")
|
| 177 |
-
else:
|
| 178 |
-
print("OK: DEFAULT_MODEL_PATH not found")
|
| 179 |
-
|
| 180 |
-
with open(FILE, "w", encoding="utf-8") as f:
|
| 181 |
-
f.write(content)
|
| 182 |
-
|
| 183 |
-
print("OK: Frontend patched - Tencent HY3:free DEFAULT")
|
| 184 |
-
print(" Models: HY3:free, Gemma 4 31B:free, Llama 3.3 70B:free, Laguna M.1:free, Laguna S 2.1:free, Nex N2 Mini, Ling 3.0 Flash:free, Llama 3.1 8B, DeepSeek V4 Flash latest")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_models.py
DELETED
|
@@ -1,212 +0,0 @@
|
|
| 1 |
-
"""Patch backend: OpenRouter routing + DeepSeek V4 Flash 0731."""
|
| 2 |
-
|
| 3 |
-
import ast
|
| 4 |
-
import os
|
| 5 |
-
import re
|
| 6 |
-
|
| 7 |
-
AGENT_FILE = "/app/backend/routes/agent.py"
|
| 8 |
-
LLM_PARAMS_FILE = "/app/agent/core/llm_params.py"
|
| 9 |
-
IDS_FILE = "/app/agent/core/model_ids.py"
|
| 10 |
-
|
| 11 |
-
with open(IDS_FILE) as f:
|
| 12 |
-
content = f.read()
|
| 13 |
-
|
| 14 |
-
# Remove NO_TOOLS_MODELS if present from previous patches
|
| 15 |
-
if "NO_TOOLS_MODELS" in content:
|
| 16 |
-
content = re.sub(r'NO_TOOLS_MODELS\s*=\s*\{[^}]*\}\n?', '', content)
|
| 17 |
-
print("OK: Removed old NO_TOOLS_MODELS")
|
| 18 |
-
|
| 19 |
-
# Fix model IDs for OpenRouter - use exact full-line patterns to avoid accidental partial matches
|
| 20 |
-
replacements = [
|
| 21 |
-
('KIMI_K27_CODE_MODEL_ID = "moonshotai/Kimi-K2.7-Code:novita"',
|
| 22 |
-
'KIMI_K27_CODE_MODEL_ID = "deepseek-ai/DeepSeek-V4-Pro"'),
|
| 23 |
-
('MINIMAX_M3_MODEL_ID = "MiniMaxAI/MiniMax-M3:novita"',
|
| 24 |
-
'MINIMAX_M3_MODEL_ID = "deepseek-ai/DeepSeek-V4-Flash"'),
|
| 25 |
-
('GLM_52_MODEL_ID = "zai-org/GLM-5.2:novita"',
|
| 26 |
-
'GLM_52_MODEL_ID = "openai/deepseek/deepseek-v4-flash"'),
|
| 27 |
-
('DEEPSEEK_V4_PRO_MODEL_ID = "deepseek-ai/DeepSeek-V4-Pro:novita"',
|
| 28 |
-
'DEEPSEEK_V4_PRO_MODEL_ID = "nvidia/nemotron-3-super-120b-a12b:free"'),
|
| 29 |
-
]
|
| 30 |
-
for old, new in replacements:
|
| 31 |
-
content = content.replace(old, new)
|
| 32 |
-
print("OK: Model ID replacements applied")
|
| 33 |
-
|
| 34 |
-
# Add missing constants BEFORE the specific line DEEPSEEK_V4_PRO_MODEL_ID = "..."
|
| 35 |
-
if "TENCENT_HY3_FREE_MODEL_ID" not in content:
|
| 36 |
-
target_line = 'DEEPSEEK_V4_PRO_MODEL_ID = "nvidia/nemotron-3-super-120b-a12b:free"'
|
| 37 |
-
insert_block = '''TENCENT_HY3_FREE_MODEL_ID = "openai/tencent/hy3:free"
|
| 38 |
-
GEMMA_4_31B_FREE_MODEL_ID = "openai/google/gemma-4-31b-it:free"
|
| 39 |
-
LLAMA_3_3_70B_FREE_MODEL_ID = "openai/meta-llama/llama-3.3-70b-instruct:free"
|
| 40 |
-
LLAMA_3_1_8B_MODEL_ID = "openai/meta-llama/llama-3.1-8b-instruct"
|
| 41 |
-
LAGUNA_M1_FREE_MODEL_ID = "openai/poolside/laguna-m.1:free"
|
| 42 |
-
LAGUNA_S21_FREE_MODEL_ID = "openai/poolside/laguna-s-2.1:free"
|
| 43 |
-
NEX_N2_MINI_MODEL_ID = "openai/nex-agi/nex-n2-mini"
|
| 44 |
-
LING_3_0_FLASH_FREE_MODEL_ID = "openai/inclusionai/ling-3.0-flash:free"
|
| 45 |
-
'''
|
| 46 |
-
# Only replace the first occurrence (the variable definition)
|
| 47 |
-
if target_line in content:
|
| 48 |
-
content = content.replace(target_line, insert_block + target_line)
|
| 49 |
-
print("OK: Added missing constants")
|
| 50 |
-
else:
|
| 51 |
-
print("WARN: Could not find target line")
|
| 52 |
-
|
| 53 |
-
# Replace all references to ~deepseek deepseek models with 0731 version (quoted strings only)
|
| 54 |
-
content = content.replace('"~deepseek/deepseek-v4-flash-latest"', '"deepseek/deepseek-v4-flash-0731"')
|
| 55 |
-
content = content.replace('"~deepseek/deepseek-v4-flash-0731"', '"deepseek/deepseek-v4-flash-0731"')
|
| 56 |
-
|
| 57 |
-
with open(IDS_FILE, "w") as f:
|
| 58 |
-
f.write(content)
|
| 59 |
-
print("OK: model_ids.py written")
|
| 60 |
-
|
| 61 |
-
ast.parse(content) # validate syntax
|
| 62 |
-
print("OK: model_ids.py syntax OK")
|
| 63 |
-
|
| 64 |
-
with open(AGENT_FILE) as f:
|
| 65 |
-
content = f.read()
|
| 66 |
-
|
| 67 |
-
content = content.replace(
|
| 68 |
-
"DEFAULT_MODEL_ID = GLM_52_MODEL_ID",
|
| 69 |
-
'DEFAULT_MODEL_ID = "openai/tencent/hy3:free"'
|
| 70 |
-
)
|
| 71 |
-
content = content.replace(
|
| 72 |
-
"DEFAULT_GPT_MODEL_ID = GPT_55_MODEL_ID",
|
| 73 |
-
"DEFAULT_GPT_MODEL_ID = LAGUNA_S21_FREE_MODEL_ID"
|
| 74 |
-
)
|
| 75 |
-
|
| 76 |
-
# Update imports
|
| 77 |
-
old_import = (
|
| 78 |
-
"from agent.core.model_ids import (\n"
|
| 79 |
-
" CLAUDE_OPUS_48_MODEL_ID,\n"
|
| 80 |
-
" DEEPSEEK_V4_PRO_MODEL_ID,\n"
|
| 81 |
-
" GLM_52_MODEL_ID,\n"
|
| 82 |
-
" GPT_55_MODEL_ID,\n"
|
| 83 |
-
" KIMI_K27_CODE_MODEL_ID,\n"
|
| 84 |
-
" MINIMAX_M3_MODEL_ID,\n"
|
| 85 |
-
" strip_huggingface_model_prefix,\n"
|
| 86 |
-
")"
|
| 87 |
-
)
|
| 88 |
-
new_import = (
|
| 89 |
-
"from agent.core.model_ids import (\n"
|
| 90 |
-
" DEEPSEEK_V4_PRO_MODEL_ID,\n"
|
| 91 |
-
" GLM_52_MODEL_ID,\n"
|
| 92 |
-
" GPT_55_MODEL_ID,\n"
|
| 93 |
-
" KIMI_K27_CODE_MODEL_ID,\n"
|
| 94 |
-
" MINIMAX_M3_MODEL_ID,\n"
|
| 95 |
-
" TENCENT_HY3_FREE_MODEL_ID,\n"
|
| 96 |
-
" GEMMA_4_31B_FREE_MODEL_ID,\n"
|
| 97 |
-
" LLAMA_3_3_70B_FREE_MODEL_ID,\n"
|
| 98 |
-
" LLAMA_3_1_8B_MODEL_ID,\n"
|
| 99 |
-
" LAGUNA_M1_FREE_MODEL_ID,\n"
|
| 100 |
-
" LAGUNA_S21_FREE_MODEL_ID,\n"
|
| 101 |
-
" NEX_N2_MINI_MODEL_ID,\n"
|
| 102 |
-
" LING_3_0_FLASH_FREE_MODEL_ID,\n"
|
| 103 |
-
" strip_huggingface_model_prefix,\n"
|
| 104 |
-
")"
|
| 105 |
-
)
|
| 106 |
-
content = content.replace(old_import, new_import)
|
| 107 |
-
|
| 108 |
-
# Update _available_models to include DeepSeek V4 Flash 0731
|
| 109 |
-
new_func = '''def _available_models() -> list[dict[str, Any]]:
|
| 110 |
-
models = [
|
| 111 |
-
{"id": TENCENT_HY3_FREE_MODEL_ID, "label": "Tencent HY3:free", "recommended": True},
|
| 112 |
-
{"id": GEMMA_4_31B_FREE_MODEL_ID, "label": "Gemma 4 31B:free", "recommended": True},
|
| 113 |
-
{"id": LLAMA_3_3_70B_FREE_MODEL_ID, "label": "Llama 3.3 70B:free", "recommended": True},
|
| 114 |
-
{"id": LAGUNA_M1_FREE_MODEL_ID, "label": "Laguna M.1:free", "recommended": True},
|
| 115 |
-
{"id": DEFAULT_GPT_MODEL_ID, "label": "Laguna S 2.1:free", "recommended": True},
|
| 116 |
-
{"id": NEX_N2_MINI_MODEL_ID, "label": "nex-agi/nex-n2-mini", "recommended": True},
|
| 117 |
-
{"id": LING_3_0_FLASH_FREE_MODEL_ID, "label": "inclusionai/ling-3.0-flash:free", "recommended": True},
|
| 118 |
-
{"id": LLAMA_3_1_8B_MODEL_ID, "label": "Llama 3.1 8B"},
|
| 119 |
-
{"id": KIMI_K27_CODE_MODEL_ID, "label": "DeepSeek V4 Pro"},
|
| 120 |
-
{"id": MINIMAX_M3_MODEL_ID, "label": "DeepSeek V4 Flash"},
|
| 121 |
-
{"id": DEFAULT_MODEL_ID, "label": "DeepSeek V4 Flash"},
|
| 122 |
-
{"id": DEEPSEEK_V4_PRO_MODEL_ID, "label": "Nemotron 3 Super 120B"},
|
| 123 |
-
{"id": "deepseek/deepseek-v4-flash-0731", "label": "DeepSeek V4 Flash 0731", "recommended": True},
|
| 124 |
-
]
|
| 125 |
-
return models'''
|
| 126 |
-
|
| 127 |
-
func_match = re.search(
|
| 128 |
-
r"def _available_models\(\)\s*->\s*list\[dict\[str,\s*Any\]\]:.*?return models",
|
| 129 |
-
content, re.DOTALL
|
| 130 |
-
)
|
| 131 |
-
if func_match:
|
| 132 |
-
content = content[:func_match.start()] + new_func + content[func_match.end():]
|
| 133 |
-
print("OK: Replaced _available_models() with DeepSeek V4 Flash 0731")
|
| 134 |
-
else:
|
| 135 |
-
old_func = (
|
| 136 |
-
"def _available_models() -> list[dict[str, Any]]:\n"
|
| 137 |
-
" models = [\n"
|
| 138 |
-
' {"id": CLAUDE_OPUS_48_MODEL_ID, "label": "Claude Opus 4.8"},\n'
|
| 139 |
-
' {"id": DEFAULT_GPT_MODEL_ID, "label": "GPT-5.5"},\n'
|
| 140 |
-
' {"id": KIMI_K27_CODE_MODEL_ID, "label": "Kimi K2.7 Code"},\n'
|
| 141 |
-
' {"id": MINIMAX_M3_MODEL_ID, "label": "MiniMax M3"},\n'
|
| 142 |
-
' {"id": DEFAULT_MODEL_ID, "label": "GLM 5.2", "recommended": True},\n'
|
| 143 |
-
' {"id": DEEPSEEK_V4_PRO_MODEL_ID, "label": "DeepSeek V4 Pro"},\n'
|
| 144 |
-
" ]\n"
|
| 145 |
-
" return models"
|
| 146 |
-
)
|
| 147 |
-
if old_func in content:
|
| 148 |
-
content = content.replace(old_func, new_func)
|
| 149 |
-
print("OK: String replace of _available_models()")
|
| 150 |
-
else:
|
| 151 |
-
print("WARN: Could not find _available_models() pattern, searching...")
|
| 152 |
-
idx = content.find("def _available_models")
|
| 153 |
-
if idx >= 0:
|
| 154 |
-
print(f"Found at index {idx}, context: {content[idx:idx+400]}")
|
| 155 |
-
|
| 156 |
-
content = content.replace(
|
| 157 |
-
'"openai/gpt-oss-120b:cerebras",',
|
| 158 |
-
'"huggingface/deepseek-ai/DeepSeek-V4-Pro",'
|
| 159 |
-
)
|
| 160 |
-
|
| 161 |
-
try:
|
| 162 |
-
ast.parse(content)
|
| 163 |
-
print("OK: agent.py syntax OK")
|
| 164 |
-
except SyntaxError as e:
|
| 165 |
-
print(f"FAIL: agent.py syntax error: {e}")
|
| 166 |
-
raise
|
| 167 |
-
|
| 168 |
-
with open(AGENT_FILE, "w") as f:
|
| 169 |
-
f.write(content)
|
| 170 |
-
|
| 171 |
-
# Patch llm_params for OpenRouter routing
|
| 172 |
-
with open(LLM_PARAMS_FILE) as f:
|
| 173 |
-
llm_content = f.read()
|
| 174 |
-
|
| 175 |
-
if "normalized_model.startswith" not in llm_content:
|
| 176 |
-
api_key_find = "api_key = _resolve_hf_router_token(session_hf_token)"
|
| 177 |
-
if api_key_find in llm_content:
|
| 178 |
-
line_end = llm_content.find("\n", llm_content.find(api_key_find) + len(api_key_find)) + 1
|
| 179 |
-
routing_insert = (
|
| 180 |
-
' # Route deepseek/ models to OpenRouter\n'
|
| 181 |
-
' if normalized_model.startswith("deepseek/"):\n'
|
| 182 |
-
' return {\n'
|
| 183 |
-
' "model": normalized_model,\n'
|
| 184 |
-
' "api_base": "https://openrouter.ai/api/v1",\n'
|
| 185 |
-
' "api_key": os.environ.get("OPENROUTER_API_KEY") or api_key or "",\n'
|
| 186 |
-
' }\n\n'
|
| 187 |
-
' # Route openai/-prefixed models to OpenRouter\n'
|
| 188 |
-
' if normalized_model.startswith("openai/"):\n'
|
| 189 |
-
' return {\n'
|
| 190 |
-
' "model": normalized_model,\n'
|
| 191 |
-
' "api_base": "https://openrouter.ai/api/v1",\n'
|
| 192 |
-
' "api_key": os.environ.get("OPENROUTER_API_KEY") or api_key or "",\n'
|
| 193 |
-
' }\n\n'
|
| 194 |
-
)
|
| 195 |
-
llm_content = llm_content[:line_end] + routing_insert + llm_content[line_end:]
|
| 196 |
-
print("OK: Patched _resolve_llm_params with deepseek/ routing")
|
| 197 |
-
else:
|
| 198 |
-
print("WARN: Could not find api_key line")
|
| 199 |
-
else:
|
| 200 |
-
print("OK: llm_params.py already patched")
|
| 201 |
-
|
| 202 |
-
try:
|
| 203 |
-
ast.parse(llm_content)
|
| 204 |
-
print("OK: llm_params.py syntax OK")
|
| 205 |
-
except SyntaxError as e:
|
| 206 |
-
print(f"FAIL: llm_params.py syntax error: {e}")
|
| 207 |
-
raise
|
| 208 |
-
|
| 209 |
-
with open(LLM_PARAMS_FILE, "w") as f:
|
| 210 |
-
f.write(llm_content)
|
| 211 |
-
|
| 212 |
-
print("OK: ALL BACKEND PATCHES APPLIED")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_sse_transport.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
"""Patch sse-chat-transport.ts - handle task_incomplete event."""
|
| 2 |
-
|
| 3 |
-
SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts"
|
| 4 |
-
|
| 5 |
-
def patch():
|
| 6 |
-
import os
|
| 7 |
-
if not os.path.exists(SSE_TRANSPORT):
|
| 8 |
-
print(f"SKIP: {SSE_TRANSPORT} not found")
|
| 9 |
-
return
|
| 10 |
-
|
| 11 |
-
with open(SSE_TRANSPORT, "r", encoding="utf-8") as f:
|
| 12 |
-
content = f.read()
|
| 13 |
-
|
| 14 |
-
# Add task_incomplete case in createEventToChunkStream
|
| 15 |
-
if "case 'task_incomplete'" not in content:
|
| 16 |
-
task_incomplete_case = ''' case 'task_incomplete':
|
| 17 |
-
sideChannel.onTaskIncomplete(
|
| 18 |
-
(event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [],
|
| 19 |
-
);
|
| 20 |
-
break;
|
| 21 |
-
'''
|
| 22 |
-
content = content.replace(
|
| 23 |
-
"case 'turn_complete':",
|
| 24 |
-
task_incomplete_case + "\n case 'turn_complete':"
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
-
with open(SSE_TRANSPORT, "w", encoding="utf-8") as f:
|
| 28 |
-
f.write(content)
|
| 29 |
-
print(f"OK: Patched {SSE_TRANSPORT}")
|
| 30 |
-
|
| 31 |
-
if __name__ == "__main__":
|
| 32 |
-
patch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_use_agent_chat_full.py
DELETED
|
@@ -1,121 +0,0 @@
|
|
| 1 |
-
"""Patch useAgentChat.ts - auto-continue feature."""
|
| 2 |
-
|
| 3 |
-
USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 4 |
-
|
| 5 |
-
def patch():
|
| 6 |
-
import os
|
| 7 |
-
if not os.path.exists(USE_AGENT_CHAT):
|
| 8 |
-
print(f"SKIP: {USE_AGENT_CHAT} not found")
|
| 9 |
-
return
|
| 10 |
-
|
| 11 |
-
with open(USE_AGENT_CHAT, "r", encoding="utf-8") as f:
|
| 12 |
-
content = f.read()
|
| 13 |
-
|
| 14 |
-
# 1. Add useState to imports
|
| 15 |
-
if "useState" not in content:
|
| 16 |
-
content = content.replace(
|
| 17 |
-
"import { useCallback, useEffect, useMemo, useRef } from 'react';",
|
| 18 |
-
"import { useCallback, useEffect, useMemo, useRef, useState } from 'react';"
|
| 19 |
-
)
|
| 20 |
-
|
| 21 |
-
# 2. Add state declarations after callbacksRef.current = { ... }
|
| 22 |
-
state_decl = '''
|
| 23 |
-
// Auto-continue state for free models when task incomplete
|
| 24 |
-
const autoContinueTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
| 25 |
-
const [showAutoContinue, setShowAutoContinue] = useState(false);
|
| 26 |
-
const [taskIncompleteInfo, setTaskIncompleteInfo] = useState<{
|
| 27 |
-
incompletePlan: Array<{ id: string; content: string; status: string }>;
|
| 28 |
-
} | null>(null);
|
| 29 |
-
'''
|
| 30 |
-
|
| 31 |
-
if "autoContinueTimerRef" not in content:
|
| 32 |
-
content = content.replace(
|
| 33 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 34 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };" + state_decl
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
# 3. Add onTaskIncomplete to SideChannelCallbacks interface
|
| 38 |
-
if "onTaskIncomplete:" not in content and "onInterrupted:" in content:
|
| 39 |
-
content = content.replace(
|
| 40 |
-
" onInterrupted: () => void;",
|
| 41 |
-
" onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;"
|
| 42 |
-
)
|
| 43 |
-
|
| 44 |
-
# 4. Add onTaskIncomplete handler in sideChannel
|
| 45 |
-
sidechannel_handler = ''' onTaskIncomplete: (incompletePlan) => {
|
| 46 |
-
setTaskIncompleteInfo({ incompletePlan });
|
| 47 |
-
setShowAutoContinue(true);
|
| 48 |
-
// Auto-start 10s timer for free models
|
| 49 |
-
if (autoContinueTimerRef.current) {
|
| 50 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 51 |
-
}
|
| 52 |
-
autoContinueTimerRef.current = setTimeout(() => {
|
| 53 |
-
startAutoContinue();
|
| 54 |
-
}, 10000);
|
| 55 |
-
},'''
|
| 56 |
-
|
| 57 |
-
if "onTaskIncomplete: (incompletePlan)" not in content:
|
| 58 |
-
content = content.replace(
|
| 59 |
-
" onInterrupted: () => { /* no-op — handled by stop() caller */ },",
|
| 60 |
-
sidechannel_handler + "\n onInterrupted: () => { /* no-op — handled by stop() caller */ },"
|
| 61 |
-
)
|
| 62 |
-
|
| 63 |
-
# 5. Add auto-continue functions before return
|
| 64 |
-
auto_funcs = '''
|
| 65 |
-
// -- Auto-continue for free models when task incomplete -----------------
|
| 66 |
-
const startAutoContinue = useCallback(() => {
|
| 67 |
-
setShowAutoContinue(false);
|
| 68 |
-
setTaskIncompleteInfo(null);
|
| 69 |
-
if (autoContinueTimerRef.current) {
|
| 70 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 71 |
-
autoContinueTimerRef.current = null;
|
| 72 |
-
}
|
| 73 |
-
// Build continuation message from incomplete plan
|
| 74 |
-
const incompleteItems = taskIncompleteInfo?.incompletePlan || [];
|
| 75 |
-
const continuationText = incompleteItems.length > 0
|
| 76 |
-
? `Tiếp tục từ các task chưa hoàn thành:\\n${incompleteItems.map(i => `- ${i.content}`).join('\\n')}`
|
| 77 |
-
: 'Tiếp tục nhiệm vụ.';
|
| 78 |
-
chat.sendMessage({
|
| 79 |
-
text: `[TỰ ĐỘNG TIẾP TỤC] ${continuationText}`,
|
| 80 |
-
metadata: { createdAt: new Date().toISOString() },
|
| 81 |
-
});
|
| 82 |
-
}, [taskIncompleteInfo, chat]);
|
| 83 |
-
|
| 84 |
-
const cancelAutoContinue = useCallback(() => {
|
| 85 |
-
setShowAutoContinue(false);
|
| 86 |
-
setTaskIncompleteInfo(null);
|
| 87 |
-
if (autoContinueTimerRef.current) {
|
| 88 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 89 |
-
autoContinueTimerRef.current = null;
|
| 90 |
-
}
|
| 91 |
-
}, []);
|
| 92 |
-
|
| 93 |
-
// Cleanup timer on unmount
|
| 94 |
-
useEffect(() => {
|
| 95 |
-
return () => {
|
| 96 |
-
if (autoContinueTimerRef.current) {
|
| 97 |
-
clearTimeout(autoContinueTimerRef.current);
|
| 98 |
-
}
|
| 99 |
-
};
|
| 100 |
-
}, []);
|
| 101 |
-
'''
|
| 102 |
-
|
| 103 |
-
if "startAutoContinue" not in content:
|
| 104 |
-
content = content.replace(
|
| 105 |
-
'\n return {\n messages: chat.messages,',
|
| 106 |
-
auto_funcs + '\n return {\n messages: chat.messages,'
|
| 107 |
-
)
|
| 108 |
-
|
| 109 |
-
# 6. Update return to include new values
|
| 110 |
-
if "showAutoContinue," not in content:
|
| 111 |
-
content = content.replace(
|
| 112 |
-
"refreshMessages,\n };",
|
| 113 |
-
"refreshMessages,\n showAutoContinue,\n taskIncompleteInfo,\n startAutoContinue,\n cancelAutoContinue,\n };"
|
| 114 |
-
)
|
| 115 |
-
|
| 116 |
-
with open(USE_AGENT_CHAT, "w", encoding="utf-8") as f:
|
| 117 |
-
f.write(content)
|
| 118 |
-
print(f"OK: Complete patch applied to {USE_AGENT_CHAT}")
|
| 119 |
-
|
| 120 |
-
if __name__ == "__main__":
|
| 121 |
-
patch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|