Spaces:
Running
Running
Auto-continue feature for free models
#2
by bep40 - opened
- Dockerfile +6 -2
- patch_agent_loop.py +7 -28
- patch_chat_input.py +25 -22
- patch_sse_transport.py +32 -0
- patch_use_agent_chat_full.py +8 -6
Dockerfile
CHANGED
|
@@ -14,6 +14,9 @@ RUN python3 /tmp/patch_use_agent_chat_full.py
|
|
| 14 |
COPY patch_chat_input.py /tmp/patch_chat_input.py
|
| 15 |
RUN python3 /tmp/patch_chat_input.py
|
| 16 |
|
|
|
|
|
|
|
|
|
|
| 17 |
WORKDIR /source/frontend
|
| 18 |
RUN npm config set fetch-timeout 120000 && \
|
| 19 |
npm config set fetch-retries 3 && \
|
|
@@ -43,8 +46,8 @@ COPY configs/frontend_agent_config.json ./configs/frontend_agent_config.json
|
|
| 43 |
COPY patch_models.py /tmp/patch_models.py
|
| 44 |
RUN python /tmp/patch_models.py
|
| 45 |
|
| 46 |
-
COPY
|
| 47 |
-
RUN python /tmp/
|
| 48 |
|
| 49 |
RUN mkdir -p /app/session_logs && chown -R user:user /app
|
| 50 |
|
|
@@ -56,4 +59,5 @@ ENV HOME=/home/user \
|
|
| 56 |
|
| 57 |
EXPOSE 7860
|
| 58 |
WORKDIR /app/backend
|
|
|
|
| 59 |
CMD ["sh", "start.sh"]
|
|
|
|
| 14 |
COPY patch_chat_input.py /tmp/patch_chat_input.py
|
| 15 |
RUN python3 /tmp/patch_chat_input.py
|
| 16 |
|
| 17 |
+
COPY patch_sse_transport.py /tmp/patch_sse_transport.py
|
| 18 |
+
RUN python3 /tmp/patch_sse_transport.py
|
| 19 |
+
|
| 20 |
WORKDIR /source/frontend
|
| 21 |
RUN npm config set fetch-timeout 120000 && \
|
| 22 |
npm config set fetch-retries 3 && \
|
|
|
|
| 46 |
COPY patch_models.py /tmp/patch_models.py
|
| 47 |
RUN python /tmp/patch_models.py
|
| 48 |
|
| 49 |
+
COPY patch_agent_loop.py /tmp/patch_agent_loop.py
|
| 50 |
+
RUN python /tmp/patch_agent_loop.py || echo "agent_loop patch not applied (may need adjustment)"
|
| 51 |
|
| 52 |
RUN mkdir -p /app/session_logs && chown -R user:user /app
|
| 53 |
|
|
|
|
| 59 |
|
| 60 |
EXPOSE 7860
|
| 61 |
WORKDIR /app/backend
|
| 62 |
+
# python:slim does not include bash by default; start.sh is POSIX-compatible.
|
| 63 |
CMD ["sh", "start.sh"]
|
patch_agent_loop.py
CHANGED
|
@@ -11,28 +11,18 @@ def patch():
|
|
| 11 |
with open(AGENT_LOOP, "r", encoding="utf-8") as f:
|
| 12 |
content = f.read()
|
| 13 |
|
| 14 |
-
# Add import re if needed for detection
|
| 15 |
# Add _check_task_incomplete function before class Handlers
|
| 16 |
-
|
| 17 |
check_func = '''
|
| 18 |
|
| 19 |
def _check_task_incomplete(session: Session, llm_result: LLMResult) -> bool:
|
| 20 |
-
"""Check if model stopped streaming but task is incomplete.
|
| 21 |
-
|
| 22 |
-
Returns True when:
|
| 23 |
-
- LLM finished without tool calls
|
| 24 |
-
- But there are unfinished plan items
|
| 25 |
-
"""
|
| 26 |
if llm_result.tool_calls_acc:
|
| 27 |
return False # Has tool calls, task continues via tool execution
|
| 28 |
|
| 29 |
plan = getattr(session, "current_plan", None) or []
|
| 30 |
unfinished = [item for item in plan if item.get("status") in ("pending", "in_progress")]
|
| 31 |
|
| 32 |
-
|
| 33 |
-
pending_actions = getattr(session, "pending_actions", None)
|
| 34 |
-
|
| 35 |
-
return len(unfinished) > 0 or bool(pending_actions)
|
| 36 |
|
| 37 |
|
| 38 |
def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
|
|
@@ -42,19 +32,16 @@ def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
|
|
| 42 |
|
| 43 |
'''
|
| 44 |
|
| 45 |
-
# Insert after LLMResult dataclass definition and before Handlers class
|
| 46 |
if "_check_task_incomplete" not in content:
|
| 47 |
content = content.replace(
|
| 48 |
"class Handlers:",
|
| 49 |
check_func + "\n\nclass Handlers:"
|
| 50 |
)
|
| 51 |
|
| 52 |
-
#
|
| 53 |
-
# Find where LLM result is processed and check for incomplete task
|
| 54 |
check_call = '''
|
| 55 |
# === Check for incomplete task after LLM response ===
|
| 56 |
if not llm_result.tool_calls_acc and llm_result.content:
|
| 57 |
-
# Model finished without tool calls - check if task is done
|
| 58 |
unfinished = _unfinished_plan_items(session)
|
| 59 |
if unfinished:
|
| 60 |
await session.send_event(
|
|
@@ -68,24 +55,16 @@ def _unfinished_plan_items(session: Session) -> list[dict[str, str]]:
|
|
| 68 |
)
|
| 69 |
'''
|
| 70 |
|
| 71 |
-
# Insert this check after processing tool calls but before final_response check
|
| 72 |
-
# Look for pattern: iteration increment and session updates
|
| 73 |
if "task_incomplete" not in content:
|
| 74 |
-
# Find
|
| 75 |
insert_marker = " # -- End of turn --"
|
| 76 |
if insert_marker in content:
|
| 77 |
-
content = content.replace(
|
| 78 |
-
insert_marker,
|
| 79 |
-
check_call + "\n" + insert_marker
|
| 80 |
-
)
|
| 81 |
else:
|
| 82 |
-
# Try another location
|
| 83 |
result_marker = "final_response = llm_result.content or None"
|
| 84 |
if result_marker in content:
|
| 85 |
-
content = content.replace(
|
| 86 |
-
result_marker,
|
| 87 |
-
result_marker + "\n" + check_call
|
| 88 |
-
)
|
| 89 |
|
| 90 |
with open(AGENT_LOOP, "w", encoding="utf-8") as f:
|
| 91 |
f.write(content)
|
|
|
|
| 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]]:
|
|
|
|
| 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(
|
|
|
|
| 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)
|
patch_chat_input.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
-
"""Patch ChatInput.tsx -
|
| 2 |
|
| 3 |
CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx"
|
| 4 |
|
| 5 |
def patch():
|
| 6 |
-
import os
|
| 7 |
-
import re
|
| 8 |
if not os.path.exists(CHAT_INPUT):
|
| 9 |
print(f"SKIP: {CHAT_INPUT} not found")
|
| 10 |
return
|
|
@@ -12,22 +11,24 @@ def patch():
|
|
| 12 |
with open(CHAT_INPUT, "r", encoding="utf-8") as f:
|
| 13 |
content = f.read()
|
| 14 |
|
| 15 |
-
# Add Button to MUI imports
|
| 16 |
-
if "Button" not in content:
|
| 17 |
content = content.replace(
|
| 18 |
"import { Box, IconButton, Stack, Tooltip } from '@mui/material';",
|
| 19 |
"import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';"
|
| 20 |
)
|
| 21 |
|
| 22 |
-
# Add
|
| 23 |
if "DatasetUploadResponse" not in content:
|
| 24 |
content = content.replace(
|
| 25 |
"import { apiFetch } from '@/utils/api';",
|
| 26 |
"import { apiFetch } from '@/utils/api';\nimport type { DatasetUploadResponse } from '@/types/agent';"
|
| 27 |
)
|
| 28 |
|
| 29 |
-
#
|
| 30 |
-
|
|
|
|
|
|
|
| 31 |
sessionId: string;
|
| 32 |
initialModelPath: string | null | undefined;
|
| 33 |
onSend: (text: string) => Promise<void>;
|
|
@@ -40,19 +41,21 @@ def patch():
|
|
| 40 |
taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
|
| 41 |
startAutoContinue?: () => void;
|
| 42 |
cancelAutoContinue?: () => void;
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', content, re.DOTALL)
|
| 48 |
if match:
|
| 49 |
-
content = content.replace(match.group(0),
|
| 50 |
-
print("OK: Replaced ChatInputProps interface")
|
| 51 |
else:
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
# Add auto-continue buttons before the flex
|
| 55 |
-
buttons_code = '''
|
|
|
|
| 56 |
{showAutoContinue && taskIncompleteInfo && (
|
| 57 |
<Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
|
| 58 |
<Button
|
|
@@ -74,15 +77,15 @@ def patch():
|
|
| 74 |
Tạm dừng
|
| 75 |
</Button>
|
| 76 |
</Stack>
|
| 77 |
-
)}
|
|
|
|
| 78 |
|
| 79 |
if "Tự động tiếp tục" not in content:
|
| 80 |
# Insert before <Box sx={{ flex: 1 }}
|
| 81 |
content = content.replace(
|
| 82 |
-
"<Box sx={{ flex: 1
|
| 83 |
-
buttons_code + "
|
| 84 |
)
|
| 85 |
-
print("OK: Added auto-continue buttons")
|
| 86 |
|
| 87 |
with open(CHAT_INPUT, "w", encoding="utf-8") as f:
|
| 88 |
f.write(content)
|
|
|
|
| 1 |
+
"""Patch ChatInput.tsx for auto-continue buttons."""
|
| 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
|
|
|
|
| 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. Add DatasetUploadResponse import if needed
|
| 22 |
if "DatasetUploadResponse" not in content:
|
| 23 |
content = content.replace(
|
| 24 |
"import { apiFetch } from '@/utils/api';",
|
| 25 |
"import { apiFetch } from '@/utils/api';\nimport type { DatasetUploadResponse } from '@/types/agent';"
|
| 26 |
)
|
| 27 |
|
| 28 |
+
# 3. Add props to interface
|
| 29 |
+
if "showAutoContinue" not in content:
|
| 30 |
+
props_code = '''
|
| 31 |
+
interface ChatInputProps {
|
| 32 |
sessionId: string;
|
| 33 |
initialModelPath: string | null | undefined;
|
| 34 |
onSend: (text: string) => Promise<void>;
|
|
|
|
| 41 |
taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null;
|
| 42 |
startAutoContinue?: () => void;
|
| 43 |
cancelAutoContinue?: () => void;
|
| 44 |
+
}
|
| 45 |
+
'''
|
| 46 |
+
# Find and replace existing interface
|
| 47 |
+
match = re.search(r'interface ChatInputProps[^}]*[}][^}]*[}]', content, re.DOTALL)
|
|
|
|
| 48 |
if match:
|
| 49 |
+
content = content.replace(match.group(0), props_code)
|
|
|
|
| 50 |
else:
|
| 51 |
+
content = content.replace(
|
| 52 |
+
"export default function ChatInput",
|
| 53 |
+
props_code + "\nexport default function ChatInput"
|
| 54 |
+
)
|
| 55 |
|
| 56 |
+
# 4. Add auto-continue buttons before the flex box
|
| 57 |
+
buttons_code = '''
|
| 58 |
+
{/* Auto-continue controls for free models */}
|
| 59 |
{showAutoContinue && taskIncompleteInfo && (
|
| 60 |
<Stack direction="row" spacing={1} sx={{ mb: 1, px: 1 }}>
|
| 61 |
<Button
|
|
|
|
| 77 |
Tạm dừng
|
| 78 |
</Button>
|
| 79 |
</Stack>
|
| 80 |
+
)}
|
| 81 |
+
'''
|
| 82 |
|
| 83 |
if "Tự động tiếp tục" not in content:
|
| 84 |
# Insert before <Box sx={{ flex: 1 }}
|
| 85 |
content = content.replace(
|
| 86 |
+
"<Box sx={{ flex: 1 ",
|
| 87 |
+
buttons_code + " <Box sx={{ flex: 1 "
|
| 88 |
)
|
|
|
|
| 89 |
|
| 90 |
with open(CHAT_INPUT, "w", encoding="utf-8") as f:
|
| 91 |
f.write(content)
|
patch_sse_transport.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 4 |
|
|
@@ -25,12 +25,13 @@ def patch():
|
|
| 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 |
if "autoContinueTimerRef" not in content:
|
| 31 |
content = content.replace(
|
| 32 |
"callbacksRef.current = { onReady, onError, onSessionDead };",
|
| 33 |
-
"callbacksRef.current = { onReady, onError, onSessionDead };
|
| 34 |
)
|
| 35 |
|
| 36 |
# 3. Add onTaskIncomplete to SideChannelCallbacks interface
|
|
@@ -41,7 +42,7 @@ def patch():
|
|
| 41 |
)
|
| 42 |
|
| 43 |
# 4. Add onTaskIncomplete handler in sideChannel
|
| 44 |
-
sidechannel_handler = ''' onTaskIncomplete: (incompletePlan
|
| 45 |
setTaskIncompleteInfo({ incompletePlan });
|
| 46 |
setShowAutoContinue(true);
|
| 47 |
// Auto-start 10s timer for free models
|
|
@@ -53,7 +54,7 @@ def patch():
|
|
| 53 |
}, 10000);
|
| 54 |
},'''
|
| 55 |
|
| 56 |
-
if "onTaskIncomplete: (incompletePlan
|
| 57 |
content = content.replace(
|
| 58 |
" onInterrupted: () => { /* no-op — handled by stop() caller */ },",
|
| 59 |
sidechannel_handler + "\n onInterrupted: () => { /* no-op — handled by stop() caller */ },"
|
|
@@ -96,7 +97,8 @@ def patch():
|
|
| 96 |
clearTimeout(autoContinueTimerRef.current);
|
| 97 |
}
|
| 98 |
};
|
| 99 |
-
}, []);
|
|
|
|
| 100 |
|
| 101 |
if "startAutoContinue" not in content:
|
| 102 |
content = content.replace(
|
|
|
|
| 1 |
+
"""Patch useAgentChat.ts - auto-continue feature."""
|
| 2 |
|
| 3 |
USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts"
|
| 4 |
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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 */ },"
|
|
|
|
| 97 |
clearTimeout(autoContinueTimerRef.current);
|
| 98 |
}
|
| 99 |
};
|
| 100 |
+
}, []);
|
| 101 |
+
'''
|
| 102 |
|
| 103 |
if "startAutoContinue" not in content:
|
| 104 |
content = content.replace(
|