Spaces:
Running
Running
sync: 192 file da Baida98/AI@6bb6a205 (2026-08-26 18:57 UTC) [deploy-all] (#122)
Browse files- sync: 192 file da Baida98/AI@6bb6a205 (2026-08-26 18:57 UTC) [deploy-all] (86f3e70e61fe2bc5cdbbef3bc438db0f2883d90b)
- agents/html_fast_path.py +60 -0
- agents/unified_loop.py +49 -25
- api/agent.py +50 -16
- tests/test_html_fast_path.py +36 -0
agents/html_fast_path.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Classificazione locale del fast path per mini-app HTML a file singolo.
|
| 2 |
+
|
| 3 |
+
Il classificatore è deliberatamente conservativo: in caso di dubbio restituisce
|
| 4 |
+
False. Non usa LLM, rete o stato globale e quindi non aggiunge latenza misurabile.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass(frozen=True)
|
| 13 |
+
class HtmlFastPathDecision:
|
| 14 |
+
eligible: bool
|
| 15 |
+
reason: str
|
| 16 |
+
path: str = "index.html"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
_HTML_RE = re.compile(r"\b(?:html5?|html|pagina\s+web|single[- ]page|landing\s+page)\b", re.I)
|
| 20 |
+
_CREATE_RE = re.compile(r"\b(?:crea|genera|scrivi|realizza|implementa|build|create|generate|make)\b", re.I)
|
| 21 |
+
_SINGLE_FILE_RE = re.compile(
|
| 22 |
+
r"\b(?:un\s+solo\s+file|singolo\s+file|one\s+file|single\s+file|file\s+unico)\b", re.I
|
| 23 |
+
)
|
| 24 |
+
_PATH_RE = re.compile(r"(?<![\w./-])([\w./-]+\.html)(?![\w.-])", re.I)
|
| 25 |
+
_FORBIDDEN_RE = re.compile(
|
| 26 |
+
r"\b(?:deploy|pubblica|publish|rilascia|release|github|git|npm|pnpm|yarn|install|"
|
| 27 |
+
r"api|backend|server|database|db|auth|login|pagamento|payment|webhook|secret|token|"
|
| 28 |
+
r"shell|bash|terminal|esegui\s+comandi|execute\s+commands|multi[- ]file|pi[uù]\s+file|"
|
| 29 |
+
r"react|vue|angular|next(?:\.js)?|vite|typescript|python|sql)\b",
|
| 30 |
+
re.I,
|
| 31 |
+
)
|
| 32 |
+
_EXTERNAL_RE = re.compile(r"\b(?:fetch|axios|websocket|stripe|supabase|firebase|oauth)\b|https?://", re.I)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def classify_html_fast_path(goal: str) -> HtmlFastPathDecision:
|
| 36 |
+
"""Return an eligible decision only for a safe, self-contained HTML request."""
|
| 37 |
+
text = " ".join(str(goal or "").split())
|
| 38 |
+
if not text:
|
| 39 |
+
return HtmlFastPathDecision(False, "empty_goal")
|
| 40 |
+
if len(text) > 500:
|
| 41 |
+
return HtmlFastPathDecision(False, "goal_too_long")
|
| 42 |
+
if not _HTML_RE.search(text):
|
| 43 |
+
return HtmlFastPathDecision(False, "not_html_goal")
|
| 44 |
+
if not _CREATE_RE.search(text):
|
| 45 |
+
return HtmlFastPathDecision(False, "not_creation_goal")
|
| 46 |
+
if not _SINGLE_FILE_RE.search(text):
|
| 47 |
+
return HtmlFastPathDecision(False, "single_file_not_explicit")
|
| 48 |
+
if _FORBIDDEN_RE.search(text):
|
| 49 |
+
return HtmlFastPathDecision(False, "contains_project_or_sensitive_operation")
|
| 50 |
+
if _EXTERNAL_RE.search(text):
|
| 51 |
+
return HtmlFastPathDecision(False, "external_dependency_or_network")
|
| 52 |
+
|
| 53 |
+
paths = _PATH_RE.findall(text)
|
| 54 |
+
path = paths[0] if paths else "index.html"
|
| 55 |
+
if "/" in path or path.startswith("."):
|
| 56 |
+
return HtmlFastPathDecision(False, "nested_path_not_allowed", path)
|
| 57 |
+
return HtmlFastPathDecision(True, "self_contained_single_html", path)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
__all__ = ["HtmlFastPathDecision", "classify_html_fast_path"]
|
agents/unified_loop.py
CHANGED
|
@@ -550,9 +550,29 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
|
|
| 550 |
# F17+B7: planner per task di progettazione/implementazione â soglia ridotta a 10 chars
|
| 551 |
# Bug: "crea app react" (14 chars) non attivava mai il planner (soglia era 50).
|
| 552 |
# _NEEDS_PLAN_RE filtra già query semplici â len guard serve solo per 1-8 char input.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
_should_plan = (
|
| 554 |
self.planner
|
| 555 |
and not tool_results
|
|
|
|
| 556 |
and bool(self._NEEDS_PLAN_RE.search(state.goal[:200]))
|
| 557 |
and len(state.goal) > 10
|
| 558 |
)
|
|
@@ -569,7 +589,7 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
|
|
| 569 |
}
|
| 570 |
_logger.info("S-FMT-ORCH fast-fix: piano sintetico iniettato, skip ARCHITECT")
|
| 571 |
_t0_plan = asyncio.get_running_loop().time() # Sprint 5 ITEM 13: plan_ms timing
|
| 572 |
-
if _should_plan:
|
| 573 |
if on_step:
|
| 574 |
await _maybe_await(on_step({
|
| 575 |
"loop": 0, "action": "plan", "status": "started",
|
|
@@ -578,7 +598,10 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
|
|
| 578 |
}))
|
| 579 |
# S640: timeout planner + S-FMT-ORCH fast-fix bypass
|
| 580 |
# Se _fast_fix_plan disponibile, salta ARCHITECT (~15s risparmiati)
|
| 581 |
-
if
|
|
|
|
|
|
|
|
|
|
| 582 |
plan = _fast_fix_plan
|
| 583 |
_logger.info("S-FMT-ORCH fast-fix: ARCHITECT bypassato")
|
| 584 |
else:
|
|
@@ -3777,29 +3800,6 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
|
|
| 3777 |
except Exception:
|
| 3778 |
pass # decision_memory non disponibile — continua normalmente
|
| 3779 |
|
| 3780 |
-
# B5-ORDER: una spiegazione completa e non operativa è già un intento
|
| 3781 |
-
# sufficiente. Deve bypassare le guardie di ambiguità, che sono riservate a
|
| 3782 |
-
# comandi realmente vaghi; le guardie in _is_pure_explanation() proteggono
|
| 3783 |
-
# file, mutazioni, dati realtime e richieste troppo lunghe.
|
| 3784 |
-
# Sprint 5 ITEM 13: classify_ms — tempo routing/classificazione goal (sync, <1ms)
|
| 3785 |
-
_t0_classify = _time.monotonic()
|
| 3786 |
-
if self._is_pure_explanation(goal):
|
| 3787 |
-
try:
|
| 3788 |
-
from api.state import record_timing as _rtcB5
|
| 3789 |
-
_rtcB5("classify_ms", (_time.monotonic() - _t0_classify) * 1000)
|
| 3790 |
-
except Exception:
|
| 3791 |
-
pass
|
| 3792 |
-
await self._transition_state(state, AgentState.THINKING, on_step)
|
| 3793 |
-
_r = await _finish(await self._run_fallback(state, on_step))
|
| 3794 |
-
_r["timing_ms"] = int((_time.monotonic() - _t_run) * 1000)
|
| 3795 |
-
_r["effective_max_steps"] = state.max_steps
|
| 3796 |
-
if _sid_token is not None:
|
| 3797 |
-
try: _sid_var.reset(_sid_token)
|
| 3798 |
-
except Exception: pass
|
| 3799 |
-
if self._session_files:
|
| 3800 |
-
asyncio.ensure_future(self._vfs_git_backup())
|
| 3801 |
-
return _r
|
| 3802 |
-
|
| 3803 |
# P29-B1: gate ambiguità strutturale — _is_goal_ambiguous() era P28-B2 dead code (mai chiamata).
|
| 3804 |
# Zero LLM, <0.1ms. Lingua-aware via self._run_lang (P28-B1). Fires dopo blacklist e prima del routing.
|
| 3805 |
if _is_goal_ambiguous(goal):
|
|
@@ -3963,6 +3963,9 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
|
|
| 3963 |
return _r_bl
|
| 3964 |
|
| 3965 |
|
|
|
|
|
|
|
|
|
|
| 3966 |
# S402: Fast Path â greeting/ack/identità semplice â bypass tutto l'overhead
|
| 3967 |
if self._is_simple_query(goal):
|
| 3968 |
try:
|
|
@@ -4012,6 +4015,27 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
|
|
| 4012 |
# già â "direct tools + fallback" â ma il codice faceva solo _run_fallback senza tool).
|
| 4013 |
# Bug: query meteo/news/cerca non chiamavano mai i tool reali â LLM allucinava i dati
|
| 4014 |
# â ResponseVerifier girava su risposta inventata â retry â 20-60s inutili.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4015 |
# P36: Hybrid Execution Router — Python code analysis fast path.
|
| 4016 |
# Se goal contiene keyword analisi + codice Python nel context/goal,
|
| 4017 |
# chiama python_analyze direttamente (<5ms) saltando planner+LLM (5-15s).
|
|
|
|
| 550 |
# F17+B7: planner per task di progettazione/implementazione â soglia ridotta a 10 chars
|
| 551 |
# Bug: "crea app react" (14 chars) non attivava mai il planner (soglia era 50).
|
| 552 |
# _NEEDS_PLAN_RE filtra già query semplici â len guard serve solo per 1-8 char input.
|
| 553 |
+
try:
|
| 554 |
+
from agents.html_fast_path import classify_html_fast_path
|
| 555 |
+
_html_fast_decision = classify_html_fast_path(state.goal)
|
| 556 |
+
except Exception as _html_cls_exc:
|
| 557 |
+
_logger.debug("[html-fast-path] classifier unavailable: %s", type(_html_cls_exc).__name__)
|
| 558 |
+
_html_fast_decision = None
|
| 559 |
+
_html_fast_plan = None
|
| 560 |
+
if _html_fast_decision is not None and _html_fast_decision.eligible and not tool_results:
|
| 561 |
+
_html_fast_plan = {
|
| 562 |
+
"summary": "Piano locale mini-app HTML a file singolo",
|
| 563 |
+
"goal": state.goal,
|
| 564 |
+
"subtasks": [
|
| 565 |
+
{"id": 1, "description": f"Scrivi {_html_fast_decision.path}: {state.goal}", "tool": "write_file", "requires": []},
|
| 566 |
+
{"id": 2, "description": f"Rileggi {_html_fast_decision.path} e verifica la scrittura", "tool": "read_file", "requires": [1]},
|
| 567 |
+
],
|
| 568 |
+
"complexity": "low",
|
| 569 |
+
"source": "local_html_fast_path",
|
| 570 |
+
}
|
| 571 |
+
_logger.info("[html-fast-path] planner bypass: %s", _html_fast_decision.path)
|
| 572 |
_should_plan = (
|
| 573 |
self.planner
|
| 574 |
and not tool_results
|
| 575 |
+
and _html_fast_plan is None
|
| 576 |
and bool(self._NEEDS_PLAN_RE.search(state.goal[:200]))
|
| 577 |
and len(state.goal) > 10
|
| 578 |
)
|
|
|
|
| 589 |
}
|
| 590 |
_logger.info("S-FMT-ORCH fast-fix: piano sintetico iniettato, skip ARCHITECT")
|
| 591 |
_t0_plan = asyncio.get_running_loop().time() # Sprint 5 ITEM 13: plan_ms timing
|
| 592 |
+
if _should_plan or _html_fast_plan is not None:
|
| 593 |
if on_step:
|
| 594 |
await _maybe_await(on_step({
|
| 595 |
"loop": 0, "action": "plan", "status": "started",
|
|
|
|
| 598 |
}))
|
| 599 |
# S640: timeout planner + S-FMT-ORCH fast-fix bypass
|
| 600 |
# Se _fast_fix_plan disponibile, salta ARCHITECT (~15s risparmiati)
|
| 601 |
+
if _html_fast_plan is not None:
|
| 602 |
+
plan = _html_fast_plan
|
| 603 |
+
_logger.info("[html-fast-path] ARCHITECT bypassato")
|
| 604 |
+
elif _fast_fix_plan is not None:
|
| 605 |
plan = _fast_fix_plan
|
| 606 |
_logger.info("S-FMT-ORCH fast-fix: ARCHITECT bypassato")
|
| 607 |
else:
|
|
|
|
| 3800 |
except Exception:
|
| 3801 |
pass # decision_memory non disponibile — continua normalmente
|
| 3802 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3803 |
# P29-B1: gate ambiguità strutturale — _is_goal_ambiguous() era P28-B2 dead code (mai chiamata).
|
| 3804 |
# Zero LLM, <0.1ms. Lingua-aware via self._run_lang (P28-B1). Fires dopo blacklist e prima del routing.
|
| 3805 |
if _is_goal_ambiguous(goal):
|
|
|
|
| 3963 |
return _r_bl
|
| 3964 |
|
| 3965 |
|
| 3966 |
+
# Sprint 5 ITEM 13: classify_ms â tempo routing/classificazione goal (sync, <1ms)
|
| 3967 |
+
_t0_classify = _time.monotonic()
|
| 3968 |
+
|
| 3969 |
# S402: Fast Path â greeting/ack/identità semplice â bypass tutto l'overhead
|
| 3970 |
if self._is_simple_query(goal):
|
| 3971 |
try:
|
|
|
|
| 4015 |
# già â "direct tools + fallback" â ma il codice faceva solo _run_fallback senza tool).
|
| 4016 |
# Bug: query meteo/news/cerca non chiamavano mai i tool reali â LLM allucinava i dati
|
| 4017 |
# â ResponseVerifier girava su risposta inventata â retry â 20-60s inutili.
|
| 4018 |
+
# B5: query spiegazione pura → _run_fallback diretta (-20-30s risparmio)
|
| 4019 |
+
# Scenari: "cos'è X", "spiegami Y", "how does Z work?", "explain W"
|
| 4020 |
+
# Fail-open: se regex troppo larga → path normale (nessuna perdita)
|
| 4021 |
+
if self._is_pure_explanation(goal):
|
| 4022 |
+
try:
|
| 4023 |
+
from api.state import record_timing as _rtcB5
|
| 4024 |
+
_rtcB5("classify_ms", (_time.monotonic() - _t0_classify) * 1000)
|
| 4025 |
+
except Exception:
|
| 4026 |
+
pass
|
| 4027 |
+
await self._transition_state(state, AgentState.THINKING, on_step)
|
| 4028 |
+
_r = await _finish(await self._run_fallback(state, on_step))
|
| 4029 |
+
_r["timing_ms"] = int((_time.monotonic() - _t_run) * 1000)
|
| 4030 |
+
_r["effective_max_steps"] = state.max_steps
|
| 4031 |
+
if _sid_token is not None:
|
| 4032 |
+
try: _sid_var.reset(_sid_token)
|
| 4033 |
+
except Exception: pass
|
| 4034 |
+
if self._session_files:
|
| 4035 |
+
asyncio.ensure_future(self._vfs_git_backup())
|
| 4036 |
+
return _r
|
| 4037 |
+
|
| 4038 |
+
|
| 4039 |
# P36: Hybrid Execution Router — Python code analysis fast path.
|
| 4040 |
# Se goal contiene keyword analisi + codice Python nel context/goal,
|
| 4041 |
# chiama python_analyze direttamente (<5ms) saltando planner+LLM (5-15s).
|
api/agent.py
CHANGED
|
@@ -849,8 +849,8 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
|
|
| 849 |
# tool. È il backstop per client SSE che non applicano il fast path UI.
|
| 850 |
_literal_response = task.get('literal_response')
|
| 851 |
if isinstance(_literal_response, str) and _literal_response:
|
| 852 |
-
_agent_tasks[task_id]['status'] = '
|
| 853 |
-
asyncio.create_task(sb_update_status(task_id, '
|
| 854 |
literal_event = json.dumps(_sanitize_for_json({
|
| 855 |
'event': 'task_done', 'taskId': task_id, 'result': _literal_response,
|
| 856 |
}))
|
|
@@ -899,7 +899,7 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
|
|
| 899 |
sb_events = await sb_get_events(task_id)
|
| 900 |
if sb_events:
|
| 901 |
task_status = task.get('status', 'UNKNOWN')
|
| 902 |
-
terminal = task_status in ('SUCCESS', 'ERROR', 'CANCELLED')
|
| 903 |
# Replay buffer from resume point
|
| 904 |
for evt_str in sb_events[_resume_from:]:
|
| 905 |
yield evt_str
|
|
@@ -1005,8 +1005,8 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
|
|
| 1005 |
# È emesso direttamente nello stream affinché i client SSE non possano bypassarlo.
|
| 1006 |
_literal_response = task.get('literal_response')
|
| 1007 |
if isinstance(_literal_response, str) and _literal_response:
|
| 1008 |
-
_agent_tasks[task_id]['status'] = '
|
| 1009 |
-
asyncio.create_task(sb_update_status(task_id, '
|
| 1010 |
_sse('task_done', {'taskId': task_id, 'result': _literal_response})
|
| 1011 |
return
|
| 1012 |
|
|
@@ -1453,8 +1453,13 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
|
|
| 1453 |
except Exception as _artifact_exc:
|
| 1454 |
_logger.warning('[agent] artifact fallback failed: %s', type(_artifact_exc).__name__)
|
| 1455 |
|
| 1456 |
-
|
| 1457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1458 |
# ARCH-K2.2: pubblica lifecycle event via Kernel
|
| 1459 |
if _KERNEL_AVAILABLE and _kernel is not None:
|
| 1460 |
asyncio.create_task(_kernel.publish_event(
|
|
@@ -1470,17 +1475,46 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
|
|
| 1470 |
# confermato il task. Su errore/cancellazione lo staging rimane
|
| 1471 |
# intenzionalmente non committato.
|
| 1472 |
_sse('vfs_sync_complete', _vfs_commit)
|
| 1473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1474 |
asyncio.create_task(_tg_done(task_id, task.get('goal', ''), _result_text[:500], _task_started_ms)).add_done_callback(_log_task_exc)
|
| 1475 |
|
| 1476 |
-
#
|
|
|
|
| 1477 |
if _run_quality_check:
|
| 1478 |
_qg_result = str(result.get('output', result) if isinstance(result, dict) else result)
|
| 1479 |
-
if len(_qg_result) > 500 and _qg_result.count('```') >= 2:
|
| 1480 |
-
|
| 1481 |
-
|
| 1482 |
-
|
| 1483 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1484 |
|
| 1485 |
|
| 1486 |
except asyncio.CancelledError:
|
|
@@ -1495,8 +1529,8 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
|
|
| 1495 |
_sse('task_cancelled', {'taskId': task_id})
|
| 1496 |
|
| 1497 |
except (ImportError, ModuleNotFoundError):
|
| 1498 |
-
_agent_tasks[task_id]['status'] = '
|
| 1499 |
-
asyncio.create_task(sb_update_status(task_id, '
|
| 1500 |
_sse('step_done', {'taskId': task_id, 'step': {'name': 'Ragionamento', 'index': 0}})
|
| 1501 |
_sse('task_done', {'taskId': task_id, 'result': (
|
| 1502 |
f'Goal ricevuto: {task["goal"]}\n\n'
|
|
|
|
| 849 |
# tool. È il backstop per client SSE che non applicano il fast path UI.
|
| 850 |
_literal_response = task.get('literal_response')
|
| 851 |
if isinstance(_literal_response, str) and _literal_response:
|
| 852 |
+
_agent_tasks[task_id]['status'] = 'COMPLETED'
|
| 853 |
+
asyncio.create_task(sb_update_status(task_id, 'COMPLETED')).add_done_callback(_log_task_exc)
|
| 854 |
literal_event = json.dumps(_sanitize_for_json({
|
| 855 |
'event': 'task_done', 'taskId': task_id, 'result': _literal_response,
|
| 856 |
}))
|
|
|
|
| 899 |
sb_events = await sb_get_events(task_id)
|
| 900 |
if sb_events:
|
| 901 |
task_status = task.get('status', 'UNKNOWN')
|
| 902 |
+
terminal = task_status in ('COMPLETED', 'SUCCESS', 'ERROR', 'CANCELLED')
|
| 903 |
# Replay buffer from resume point
|
| 904 |
for evt_str in sb_events[_resume_from:]:
|
| 905 |
yield evt_str
|
|
|
|
| 1005 |
# È emesso direttamente nello stream affinché i client SSE non possano bypassarlo.
|
| 1006 |
_literal_response = task.get('literal_response')
|
| 1007 |
if isinstance(_literal_response, str) and _literal_response:
|
| 1008 |
+
_agent_tasks[task_id]['status'] = 'COMPLETED'
|
| 1009 |
+
asyncio.create_task(sb_update_status(task_id, 'COMPLETED')).add_done_callback(_log_task_exc)
|
| 1010 |
_sse('task_done', {'taskId': task_id, 'result': _literal_response})
|
| 1011 |
return
|
| 1012 |
|
|
|
|
| 1453 |
except Exception as _artifact_exc:
|
| 1454 |
_logger.warning('[agent] artifact fallback failed: %s', type(_artifact_exc).__name__)
|
| 1455 |
|
| 1456 |
+
# Lifecycle separato: l'esecuzione primaria è completa quando VFS e risultato
|
| 1457 |
+
# sono confermati; la verifica qualità successiva non deve tenere il task RUNNING.
|
| 1458 |
+
_agent_tasks[task_id]['status'] = 'COMPLETED'
|
| 1459 |
+
_agent_tasks[task_id]['quality_status'] = (
|
| 1460 |
+
'QUALITY_CHECK_PENDING' if _run_quality_check else 'NOT_REQUIRED'
|
| 1461 |
+
)
|
| 1462 |
+
asyncio.create_task(sb_update_status(task_id, 'COMPLETED')).add_done_callback(_log_task_exc)
|
| 1463 |
# ARCH-K2.2: pubblica lifecycle event via Kernel
|
| 1464 |
if _KERNEL_AVAILABLE and _kernel is not None:
|
| 1465 |
asyncio.create_task(_kernel.publish_event(
|
|
|
|
| 1475 |
# confermato il task. Su errore/cancellazione lo staging rimane
|
| 1476 |
# intenzionalmente non committato.
|
| 1477 |
_sse('vfs_sync_complete', _vfs_commit)
|
| 1478 |
+
if _run_quality_check:
|
| 1479 |
+
_sse('quality_check_pending', {
|
| 1480 |
+
'taskId': task_id,
|
| 1481 |
+
'status': 'QUALITY_CHECK_PENDING',
|
| 1482 |
+
'primaryStatus': 'COMPLETED',
|
| 1483 |
+
})
|
| 1484 |
+
_sse('task_done', {
|
| 1485 |
+
'taskId': task_id,
|
| 1486 |
+
'result': _result_text[:8000],
|
| 1487 |
+
'status': 'COMPLETED',
|
| 1488 |
+
'qualityStatus': _agent_tasks[task_id].get('quality_status', 'NOT_REQUIRED'),
|
| 1489 |
+
})
|
| 1490 |
asyncio.create_task(_tg_done(task_id, task.get('goal', ''), _result_text[:500], _task_started_ms)).add_done_callback(_log_task_exc)
|
| 1491 |
|
| 1492 |
+
# Quality check non bloccante: aggiorna solo quality_status, mai lo stato
|
| 1493 |
+
# primario del task e mai il commit VFS già confermato.
|
| 1494 |
if _run_quality_check:
|
| 1495 |
_qg_result = str(result.get('output', result) if isinstance(result, dict) else result)
|
| 1496 |
+
if len(_qg_result) > 500 and _qg_result.count('```') >= 2:
|
| 1497 |
+
async def _run_quality_background() -> None:
|
| 1498 |
+
try:
|
| 1499 |
+
await _run_quality_check(
|
| 1500 |
+
task_id, task['goal'], _qg_result,
|
| 1501 |
+
on_event=lambda ev: _sse(ev.get('type', 'test_result'), ev),
|
| 1502 |
+
)
|
| 1503 |
+
_agent_tasks.get(task_id, {})['quality_status'] = 'QUALITY_CHECK_DONE'
|
| 1504 |
+
_sse('quality_check_done', {
|
| 1505 |
+
'taskId': task_id,
|
| 1506 |
+
'status': 'QUALITY_CHECK_DONE',
|
| 1507 |
+
'primaryStatus': 'COMPLETED',
|
| 1508 |
+
})
|
| 1509 |
+
except Exception as _q_exc:
|
| 1510 |
+
_agent_tasks.get(task_id, {})['quality_status'] = 'QUALITY_CHECK_ERROR'
|
| 1511 |
+
_sse('quality_check_done', {
|
| 1512 |
+
'taskId': task_id,
|
| 1513 |
+
'status': 'QUALITY_CHECK_ERROR',
|
| 1514 |
+
'primaryStatus': 'COMPLETED',
|
| 1515 |
+
'error': type(_q_exc).__name__,
|
| 1516 |
+
})
|
| 1517 |
+
asyncio.create_task(_run_quality_background()).add_done_callback(_log_task_exc)
|
| 1518 |
|
| 1519 |
|
| 1520 |
except asyncio.CancelledError:
|
|
|
|
| 1529 |
_sse('task_cancelled', {'taskId': task_id})
|
| 1530 |
|
| 1531 |
except (ImportError, ModuleNotFoundError):
|
| 1532 |
+
_agent_tasks[task_id]['status'] = 'COMPLETED'
|
| 1533 |
+
asyncio.create_task(sb_update_status(task_id, 'COMPLETED')).add_done_callback(_log_task_exc)
|
| 1534 |
_sse('step_done', {'taskId': task_id, 'step': {'name': 'Ragionamento', 'index': 0}})
|
| 1535 |
_sse('task_done', {'taskId': task_id, 'result': (
|
| 1536 |
f'Goal ricevuto: {task["goal"]}\n\n'
|
tests/test_html_fast_path.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agents.html_fast_path import classify_html_fast_path
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_accepts_self_contained_single_html():
|
| 5 |
+
decision = classify_html_fast_path(
|
| 6 |
+
"Crea una mini-app lista spesa in un solo file index.html con aggiunta e filtro"
|
| 7 |
+
)
|
| 8 |
+
assert decision.eligible is True
|
| 9 |
+
assert decision.path == "index.html"
|
| 10 |
+
assert decision.reason == "self_contained_single_html"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_accepts_explicit_html_file_without_network():
|
| 14 |
+
decision = classify_html_fast_path(
|
| 15 |
+
"Genera una pagina web HTML in un solo file todo.html con CSS e JavaScript inline"
|
| 16 |
+
)
|
| 17 |
+
assert decision.eligible is True
|
| 18 |
+
assert decision.path == "todo.html"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_rejects_deploy_and_external_dependencies():
|
| 22 |
+
assert not classify_html_fast_path(
|
| 23 |
+
"Crea una mini-app HTML in un solo file index.html e pubblicala su GitHub"
|
| 24 |
+
).eligible
|
| 25 |
+
assert not classify_html_fast_path(
|
| 26 |
+
"Crea una pagina HTML single-file che usa fetch https://api.example.com"
|
| 27 |
+
).eligible
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_rejects_multi_file_or_sensitive_tasks():
|
| 31 |
+
assert not classify_html_fast_path(
|
| 32 |
+
"Crea una app React multi-file con backend API"
|
| 33 |
+
).eligible
|
| 34 |
+
assert not classify_html_fast_path(
|
| 35 |
+
"Crea una pagina HTML in un solo file con login e database"
|
| 36 |
+
).eligible
|