nah
This commit is contained in:
@@ -34,11 +34,13 @@ if "openai" not in sys.modules:
|
||||
sys.modules["openai"] = openai_stub
|
||||
|
||||
from src.config import Settings, settings
|
||||
from src.context.compactor import ContextCompactor
|
||||
from src.context.engine import ContextEngine
|
||||
from src.models.agent import AgentProfile
|
||||
from src.models.artifacts import ArtifactSummary
|
||||
from src.models.session import SessionState
|
||||
from src.orchestrator.engine import OrchestratorEngine
|
||||
from src.orchestrator.agents.base import BaseAgent
|
||||
|
||||
|
||||
class TestSettingsBudget:
|
||||
@@ -134,6 +136,110 @@ class TestContextEngine:
|
||||
assert "## Artifacts" in package.system_prompt
|
||||
assert "Resumen del archivo" in package.system_prompt
|
||||
|
||||
def test_build_messages_prefers_recent_raw_conversation_over_synthetic_history(self):
|
||||
session = SessionState(
|
||||
immutable_rules=["No romper el proyecto"],
|
||||
task_history=[
|
||||
{
|
||||
"task_id": "prev1",
|
||||
"objective": "Revísame la home y dime qué módulo ves más flojo",
|
||||
"status": "completed",
|
||||
"summary": "User: home → Agent: el módulo más flojo es Desplegables",
|
||||
"facts": [],
|
||||
"key_data": {"sections": ["u30mz"]},
|
||||
"tools_used": ["get_module_config_vars"],
|
||||
}
|
||||
],
|
||||
recent_messages=[
|
||||
{"role": "user", "content": "Revísame la home y dime qué módulo ves más flojo"},
|
||||
{"role": "assistant", "content": "El módulo más flojo es Desplegables."},
|
||||
],
|
||||
)
|
||||
session.begin_task("Céntrate solo en ese módulo y dime qué cambiarías")
|
||||
|
||||
messages = ContextEngine()._build_messages(session)
|
||||
|
||||
assert messages[0]["content"] == "Revísame la home y dime qué módulo ves más flojo"
|
||||
assert messages[1]["content"] == "El módulo más flojo es Desplegables."
|
||||
assert "[HISTORIAL DE CONVERSACIÓN ANTERIOR" not in messages[0]["content"]
|
||||
assert "Desplegables" in messages[-1]["content"]
|
||||
|
||||
def test_build_context_keeps_recent_raw_conversation_across_tasks(self):
|
||||
session = SessionState(
|
||||
immutable_rules=["No romper el proyecto"],
|
||||
recent_messages=[
|
||||
{"role": "user", "content": "Revísame la home y dime qué módulo ves más flojo"},
|
||||
{"role": "assistant", "content": "El módulo más flojo es Desplegables."},
|
||||
],
|
||||
task_history=[
|
||||
{
|
||||
"task_id": "prev1",
|
||||
"objective": "Revísame la home y dime qué módulo ves más flojo",
|
||||
"status": "completed",
|
||||
"summary": "User: home → Agent: el módulo más flojo es Desplegables",
|
||||
"facts": [],
|
||||
"key_data": {"sections": ["u30mz"]},
|
||||
"tools_used": ["get_module_config_vars"],
|
||||
"outcomes": ["El módulo más flojo es Desplegables."],
|
||||
"focus_refs": [
|
||||
{
|
||||
"type": "module",
|
||||
"label": "Desplegables",
|
||||
"id": "u30mz",
|
||||
"role": "primary_focus",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
session.begin_task("Céntrate solo en ese módulo y dime qué cambiarías")
|
||||
agent = AgentProfile(
|
||||
role="acai",
|
||||
name="Acai",
|
||||
system_prompt="Haz el trabajo.",
|
||||
context_sections=["immutable_rules", "task_state"],
|
||||
)
|
||||
|
||||
package = asyncio.run(ContextEngine().build_context(session=session, agent=agent))
|
||||
|
||||
assert package.messages[0]["content"] == "Revísame la home y dime qué módulo ves más flojo"
|
||||
assert package.messages[1]["content"] == "El módulo más flojo es Desplegables."
|
||||
assert "Resolved Follow-up Context" in package.system_prompt
|
||||
assert "Desplegables" in package.messages[-1]["content"]
|
||||
|
||||
def test_classify_followup_mode_detects_transform_requests(self):
|
||||
mode = ContextEngine._classify_followup_mode(
|
||||
"Hazme una segunda versión más comercial, pero sin cambiar el foco."
|
||||
)
|
||||
assert mode == "transform"
|
||||
|
||||
def test_classify_followup_mode_detects_fetch_requests(self):
|
||||
mode = ContextEngine._classify_followup_mode(
|
||||
"Céntrate en ese módulo y revisa la configuración actual."
|
||||
)
|
||||
assert mode == "fetch_more"
|
||||
|
||||
def test_build_context_sets_transform_followup_mode_in_task_state(self):
|
||||
session = SessionState(
|
||||
immutable_rules=["No romper el proyecto"],
|
||||
recent_messages=[
|
||||
{"role": "user", "content": "Dame una propuesta para ese módulo"},
|
||||
{"role": "assistant", "content": "La propuesta actual es esta."},
|
||||
],
|
||||
)
|
||||
session.begin_task("Hazme una segunda versión más comercial, pero sin cambiar el foco.")
|
||||
agent = AgentProfile(
|
||||
role="acai",
|
||||
name="Acai",
|
||||
system_prompt="Haz el trabajo.",
|
||||
context_sections=["immutable_rules", "task_state"],
|
||||
)
|
||||
|
||||
package = asyncio.run(ContextEngine().build_context(session=session, agent=agent))
|
||||
|
||||
assert "**Follow-up Mode**: transform" in package.system_prompt
|
||||
assert "No llames herramientas salvo que falte un dato factual critico" in package.system_prompt
|
||||
|
||||
|
||||
class TestTaskHistoryTrim:
|
||||
def test_trim_respects_entry_limit_and_token_budget(self, monkeypatch):
|
||||
@@ -158,3 +264,209 @@ class TestTaskHistoryTrim:
|
||||
assert len(trimmed) <= 3
|
||||
assert trimmed[-1]["objective"] == "final"
|
||||
assert all(entry["objective"] != "old" for entry in trimmed)
|
||||
|
||||
def test_append_recent_messages_keeps_user_and_raw_turn_messages(self):
|
||||
merged = OrchestratorEngine._append_recent_messages(
|
||||
existing=[
|
||||
{"role": "user", "content": "Pregunta anterior"},
|
||||
{"role": "assistant", "content": "Respuesta anterior"},
|
||||
],
|
||||
message="Nueva pregunta",
|
||||
conversation=[
|
||||
{"role": "assistant", "content": "Voy a revisarlo."},
|
||||
{"role": "tool", "tool_call_id": "tool-1", "content": "resultado tool"},
|
||||
{"role": "assistant", "content": "Respuesta final"},
|
||||
],
|
||||
)
|
||||
|
||||
assert [m["role"] for m in merged] == [
|
||||
"user",
|
||||
"assistant",
|
||||
"user",
|
||||
"assistant",
|
||||
"tool",
|
||||
"assistant",
|
||||
]
|
||||
assert merged[2]["content"] == "Nueva pregunta"
|
||||
assert merged[4]["tool_call_id"] == "tool-1"
|
||||
|
||||
|
||||
class TestConversationCompaction:
|
||||
def test_compactor_preserves_last_user_and_compacts_old_tool_results(self):
|
||||
compactor = ContextCompactor(max_tokens=999999)
|
||||
messages = [
|
||||
{"role": "user", "content": "Contexto anterior " * 10},
|
||||
{"role": "assistant", "content": "Voy a revisar el modulo ahora mismo. " * 6},
|
||||
{"role": "tool", "tool_call_id": "tool-1", "content": "resultado antiguo\n" * 80},
|
||||
{"role": "assistant", "content": "He visto el resultado anterior. " * 6},
|
||||
{"role": "tool", "tool_call_id": "tool-2", "content": "resultado reciente\n" * 80},
|
||||
{"role": "user", "content": "Este es el ultimo mensaje del usuario y debe quedar intacto."},
|
||||
]
|
||||
|
||||
compacted, meta = compactor.compact_conversation(
|
||||
messages,
|
||||
max_tokens=420,
|
||||
recent_raw_limit=1,
|
||||
raw_char_limit=120,
|
||||
)
|
||||
|
||||
assert compacted[-1]["content"] == messages[-1]["content"]
|
||||
assert compacted[2]["content"].startswith("[TOOL RESULT COMPACTADO]")
|
||||
assert compacted[4]["content"].startswith("resultado reciente")
|
||||
assert compacted[1]["content"] == messages[1]["content"]
|
||||
assert meta["messages_compacted"] > 0
|
||||
assert meta["raw_tool_results_kept"] == 1
|
||||
assert meta["tool_messages_compacted"] > 0
|
||||
assert meta["assistant_messages_compacted"] == 0
|
||||
assert meta["user_messages_compacted"] == 0
|
||||
|
||||
def test_engine_reports_conversation_compaction_when_budget_is_small(self, monkeypatch):
|
||||
monkeypatch.setattr(settings, "context_max_tokens", 1400)
|
||||
monkeypatch.setattr(settings, "compaction_threshold_tokens", 1)
|
||||
monkeypatch.setattr(settings, "knowledge_base_max_tokens", 0)
|
||||
monkeypatch.setattr(settings, "tool_raw_output_max_chars", 120)
|
||||
monkeypatch.setattr(settings, "conversation_recent_raw_limit", 1)
|
||||
|
||||
session = SessionState(immutable_rules=["No romper el proyecto"])
|
||||
session.begin_task("Revisar modulo")
|
||||
agent = AgentProfile(
|
||||
role="acai",
|
||||
name="Acai",
|
||||
system_prompt="Haz el trabajo.",
|
||||
context_sections=["immutable_rules", "task_state"],
|
||||
)
|
||||
conversation = [
|
||||
{"role": "assistant", "content": "Respuesta intermedia " * 25},
|
||||
{"role": "tool", "tool_call_id": "tool-1", "content": "resultado antiguo\n" * 80},
|
||||
{"role": "assistant", "content": "Segunda respuesta " * 25},
|
||||
{"role": "tool", "tool_call_id": "tool-2", "content": "resultado reciente\n" * 80},
|
||||
]
|
||||
|
||||
engine = ContextEngine()
|
||||
asyncio.run(
|
||||
engine.build_context(
|
||||
session=session,
|
||||
agent=agent,
|
||||
conversation=conversation,
|
||||
)
|
||||
)
|
||||
debug = engine.get_last_context_debug(session.session_id)
|
||||
|
||||
assert debug is not None
|
||||
assert debug["conversation_compaction"]["messages_compacted"] > 0
|
||||
assert debug["message_tokens"] <= debug["message_tokens_before_compaction"]
|
||||
|
||||
def test_compactor_only_touches_user_messages_as_last_resort(self):
|
||||
compactor = ContextCompactor(max_tokens=999999)
|
||||
messages = [
|
||||
{"role": "user", "content": "Contexto previo del usuario " * 8},
|
||||
{"role": "assistant", "content": "Respuesta previa del asistente " * 6},
|
||||
{"role": "tool", "tool_call_id": "tool-1", "content": "resultado viejo\n" * 80},
|
||||
{"role": "user", "content": "Ultimo mensaje del usuario"},
|
||||
]
|
||||
|
||||
compacted, meta = compactor.compact_conversation(
|
||||
messages,
|
||||
max_tokens=420,
|
||||
recent_raw_limit=0,
|
||||
raw_char_limit=120,
|
||||
)
|
||||
|
||||
assert compacted[0]["content"] == messages[0]["content"]
|
||||
assert compacted[1]["content"] == messages[1]["content"]
|
||||
assert compacted[2]["content"].startswith("[TOOL RESULT COMPACTADO]")
|
||||
assert compacted[3]["content"] == messages[3]["content"]
|
||||
assert meta["tool_messages_compacted"] > 0
|
||||
assert meta["assistant_messages_compacted"] == 0
|
||||
assert meta["user_messages_compacted"] == 0
|
||||
|
||||
|
||||
class TestStructuredFollowups:
|
||||
def test_history_entry_extracts_outcomes_and_focus_refs(self):
|
||||
entry = OrchestratorEngine._build_task_history_entry(
|
||||
task_id="task-1",
|
||||
message="Revísame la home y dime qué módulo ves más flojo",
|
||||
content=(
|
||||
"## El módulo más flojo\n"
|
||||
"Si tuviera que elegir uno, diría que **Desplegables** es el más problemático.\n"
|
||||
"Recomiendo revisarlo primero."
|
||||
),
|
||||
agent_id="acai",
|
||||
facts=[],
|
||||
key_data={"sections": ["u30mz"]},
|
||||
tool_executions=[],
|
||||
artifacts_count=0,
|
||||
)
|
||||
|
||||
assert any("Desplegables" in outcome for outcome in entry["outcomes"])
|
||||
assert any(ref["label"] == "Desplegables" for ref in entry["focus_refs"])
|
||||
|
||||
def test_followup_message_includes_resolved_context(self):
|
||||
session = SessionState(
|
||||
immutable_rules=["No romper el proyecto"],
|
||||
task_history=[
|
||||
{
|
||||
"task_id": "prev1",
|
||||
"objective": "Revísame la home y dime qué módulo ves más flojo",
|
||||
"status": "completed",
|
||||
"summary": "User: home → Agent: el módulo más flojo es Desplegables",
|
||||
"facts": [],
|
||||
"key_data": {"sections": ["u30mz"]},
|
||||
"tools_used": ["get_module_config_vars"],
|
||||
"outcomes": ["Si tuviera que elegir uno, diría que Desplegables es el más problemático."],
|
||||
"focus_refs": [
|
||||
{
|
||||
"type": "module",
|
||||
"label": "Desplegables",
|
||||
"id": "u30mz",
|
||||
"role": "primary_focus",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
session.begin_task("Céntrate solo en ese módulo y dime qué cambiarías")
|
||||
engine = ContextEngine()
|
||||
|
||||
messages = engine._build_messages(session)
|
||||
|
||||
assert "[CONTEXTO RESUELTO DEL TURNO ANTERIOR]" in messages[-1]["content"]
|
||||
assert "Desplegables" in messages[-1]["content"]
|
||||
|
||||
|
||||
class _DummyMCP:
|
||||
is_running = True
|
||||
|
||||
def get_tool_definitions(self):
|
||||
return [
|
||||
{"name": "tool_a"},
|
||||
{"name": "tool_b"},
|
||||
]
|
||||
|
||||
|
||||
class TestToolGating:
|
||||
def test_base_agent_disables_tools_for_transform_followups(self):
|
||||
agent = BaseAgent(
|
||||
profile=AgentProfile(role="acai", name="Acai", allowed_tools=["tool_a", "tool_b"]),
|
||||
model_adapter=None,
|
||||
context_engine=None,
|
||||
mcp_client=_DummyMCP(),
|
||||
memory_store=None,
|
||||
sse_emitter=None,
|
||||
)
|
||||
|
||||
assert agent._get_allowed_tools(followup_mode="transform") == []
|
||||
|
||||
def test_base_agent_keeps_tools_for_non_transform_followups(self):
|
||||
agent = BaseAgent(
|
||||
profile=AgentProfile(role="acai", name="Acai", allowed_tools=["tool_a"]),
|
||||
model_adapter=None,
|
||||
context_engine=None,
|
||||
mcp_client=_DummyMCP(),
|
||||
memory_store=None,
|
||||
sse_emitter=None,
|
||||
)
|
||||
|
||||
tools = agent._get_allowed_tools(followup_mode="fetch_more")
|
||||
|
||||
assert tools == [{"name": "tool_a"}]
|
||||
|
||||
Reference in New Issue
Block a user