From cf98a96a327da7e64da7541e73b3efcc7907cf5b Mon Sep 17 00:00:00 2001 From: "hamza.khyari" Date: Mon, 18 May 2026 11:40:15 +0000 Subject: [PATCH] fix(tui): correct PersistentSessionService constructor and return type in TUI LLM dispatch - Fix robot/helper_tui_llm_dispatch.py: use SessionRepository + SessionMessageRepository instead of wrong session_factory kwarg, use llm_factory injection instead of # type: ignore monkey-patch - Fix _build_tui_facade() return type: Any -> A2aLocalFacade | None - Rebase onto latest master to eliminate scope bloat (50 -> 6 files) Refs: #11230 --- robot/helper_tui_llm_dispatch.py | 158 ++++++++++++++++--------------- src/cleveragents/tui/commands.py | 7 +- 2 files changed, 85 insertions(+), 80 deletions(-) diff --git a/robot/helper_tui_llm_dispatch.py b/robot/helper_tui_llm_dispatch.py index 9948277e5..c04bdb5c2 100644 --- a/robot/helper_tui_llm_dispatch.py +++ b/robot/helper_tui_llm_dispatch.py @@ -2,9 +2,11 @@ Each subcommand is a self-contained check that prints a sentinel on success. -Uses a FakeListLLM stub and real SQLite DB — no API keys required. +Uses a stub LLM and real SQLite DB — no API keys required. +Follows the same fixture pattern as helper_session_tell_llm.py. """ +# ruff: noqa: E402 from __future__ import annotations import sys @@ -16,49 +18,75 @@ _SRC = str(Path(__file__).resolve().parents[1] / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from cleveragents.application.services.session_service import PersistentSessionService +from cleveragents.application.services.session_workflow import SessionWorkflow +from cleveragents.infrastructure.database.models import Base +from cleveragents.infrastructure.database.repositories import ( + SessionMessageRepository, + SessionRepository, +) + # --------------------------------------------------------------------------- -# Shared stub helpers +# Stub LLM — deterministic responses without real API calls +# --------------------------------------------------------------------------- + +_STUB_TEXT = "Hello from the TUI integration stub LLM." + + +class _StubLLM: + """Minimal LLM stub compatible with LangChain chat model interface.""" + + def invoke(self, messages: Any, **kwargs: Any) -> Any: + class _Resp: + content = _STUB_TEXT + + def __init__(self) -> None: + self.tool_calls: list[Any] = [] + self.response_metadata: dict[str, Any] = { + "usage": {"input_tokens": 5, "output_tokens": 10} + } + + return _Resp() + + +# --------------------------------------------------------------------------- +# Fixture helpers — mirrors helper_session_tell_llm.py pattern # --------------------------------------------------------------------------- -def _make_fake_llm() -> Any: - """Return a FakeListLLM stub with canned responses.""" - try: - from langchain_community.llms import FakeListLLM - - return FakeListLLM(responses=["Hello from FakeListLLM"] * 20) - except ImportError: - return None - - -def _make_in_memory_db() -> Any: - """Return a fresh in-memory SQLAlchemy session factory.""" - from sqlalchemy import create_engine - from sqlalchemy.orm import sessionmaker - - from cleveragents.infrastructure.database.models import Base - - engine = create_engine("sqlite:///:memory:") +def _make_db() -> tuple[Any, Any]: + """Create in-memory SQLite DB; return (session_factory, get_db callable).""" + engine = create_engine("sqlite:///:memory:", echo=False) Base.metadata.create_all(engine) - return sessionmaker(bind=engine) + factory = sessionmaker(bind=engine, expire_on_commit=False) + db_session = factory() + + def get_db() -> Any: + return db_session + + return factory, get_db -def _make_session_service(session_factory: Any) -> Any: - from cleveragents.application.services.session_service import ( - PersistentSessionService, +def _make_service(get_db: Any) -> PersistentSessionService: + """Build a PersistentSessionService from repository instances.""" + return PersistentSessionService( + session_repo=SessionRepository(get_db), + message_repo=SessionMessageRepository(get_db), ) - return PersistentSessionService(session_factory=session_factory) - -def _make_workflow(session_service: Any, fake_llm: Any) -> Any: - from cleveragents.application.services.session_workflow import SessionWorkflow - - workflow = SessionWorkflow(session_service=session_service) - if fake_llm is not None: - # Patch _resolve_llm to return the stub so no API keys are needed - workflow._resolve_llm = lambda *_a, **_kw: fake_llm # type: ignore[method-assign] - return workflow +def _make_workflow( + service: PersistentSessionService, +) -> SessionWorkflow: + """Build a SessionWorkflow using the stub LLM via llm_factory injection.""" + stub = _StubLLM() + return SessionWorkflow( + session_service=service, + llm_factory=lambda _actor_name: stub, + ) # --------------------------------------------------------------------------- @@ -70,8 +98,8 @@ def cmd_create_session() -> None: """Verify _create_tui_session() creates a real DB-backed session.""" from cleveragents.tui import commands as cmd_mod - session_factory = _make_in_memory_db() - service = _make_session_service(session_factory) + _, get_db = _make_db() + service = _make_service(get_db) container = MagicMock() container.session_service.return_value = service @@ -81,7 +109,6 @@ def cmd_create_session() -> None: assert sid != "default", "Expected a real session ID, got 'default'" assert len(sid) > 0, "session_id must not be empty" - # Verify the session actually exists in the DB session_obj = service.get(sid) assert session_obj is not None, "Session not found in database" assert session_obj.session_id == sid @@ -94,8 +121,8 @@ def cmd_build_facade() -> None: from cleveragents.a2a.facade import A2aLocalFacade from cleveragents.tui import commands as cmd_mod - session_factory = _make_in_memory_db() - service = _make_session_service(session_factory) + _, get_db = _make_db() + service = _make_service(get_db) container = MagicMock() container.session_service.return_value = service container.plan_lifecycle_service.side_effect = RuntimeError("not needed") @@ -116,52 +143,32 @@ def cmd_build_facade() -> None: def cmd_dispatch_message() -> None: - """Verify CleverAgentsTuiApp dispatches normal text and renders response.""" - from unittest.mock import MagicMock - + """Verify facade.dispatch(message/send) with stub LLM persists and responds.""" from cleveragents.a2a.facade import A2aLocalFacade - from cleveragents.a2a.models import A2aRequest, A2aResponse + from cleveragents.a2a.models import A2aRequest - # Build a real facade backed by in-memory DB + FakeListLLM - session_factory = _make_in_memory_db() - service = _make_session_service(session_factory) - fake_llm = _make_fake_llm() - workflow = _make_workflow(service, fake_llm) + _, get_db = _make_db() + service = _make_service(get_db) + workflow = _make_workflow(service) facade = A2aLocalFacade( - services={ - "session_service": service, - "session_workflow": workflow, - } + services={"session_service": service, "session_workflow": workflow} ) - # Create a real session session = service.create(actor_name="openai/gpt-4o") session_id = session.session_id - # Build a minimal mock persona state - persona = MagicMock() - persona.name = "default" - persona.actor = "openai/gpt-4o" - persona.scoped_projects = [] - persona.scoped_plans = [] - persona_state = MagicMock() - persona_state.active_persona.return_value = persona - persona_state.current_preset.return_value = "balanced" - - # We test the facade dispatch directly (no Textual event loop needed) request = A2aRequest( method="message/send", params={"session_id": session_id, "message": "hello integration"}, ) - response: A2aResponse = facade.dispatch(request) + response = facade.dispatch(request) assert response.error is None, f"Unexpected error: {response.error}" assert response.result is not None, "Expected result dict" assistant_msg: str = response.result.get("assistant_message", "") assert len(assistant_msg) > 0, "Expected non-empty assistant message" - # Verify messages persisted in DB messages = service.get_messages(session_id) roles = [m["role"] for m in messages] assert "user" in roles, "User message not persisted" @@ -171,33 +178,28 @@ def cmd_dispatch_message() -> None: def cmd_no_actor_error() -> None: - """Verify that SessionActorNotConfiguredError is handled gracefully.""" + """Verify facade returns error response when no actor is configured.""" from cleveragents.a2a.facade import A2aLocalFacade from cleveragents.a2a.models import A2aRequest - session_factory = _make_in_memory_db() - service = _make_session_service(session_factory) + _, get_db = _make_db() + service = _make_service(get_db) + workflow = SessionWorkflow(session_service=service) - # Create session with NO actor configured - session = service.create(actor_name=None) - session_id = session.session_id - - from cleveragents.application.services.session_workflow import SessionWorkflow - - workflow = SessionWorkflow(session_service=service, provider_registry=None) facade = A2aLocalFacade( services={"session_service": service, "session_workflow": workflow} ) + session = service.create(actor_name=None) + session_id = session.session_id + request = A2aRequest( method="message/send", params={"session_id": session_id, "message": "hello"}, ) response = facade.dispatch(request) - # Facade should return an error response (not raise) for no-actor assert response.error is not None, "Expected error response for no-actor session" - print("tui-no-actor-error-ok") diff --git a/src/cleveragents/tui/commands.py b/src/cleveragents/tui/commands.py index 3c94a1cbe..c60b0f747 100644 --- a/src/cleveragents/tui/commands.py +++ b/src/cleveragents/tui/commands.py @@ -7,9 +7,12 @@ from collections import defaultdict from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any from cleveragents.application.container import get_container + +if TYPE_CHECKING: + from cleveragents.a2a.facade import A2aLocalFacade from cleveragents.tui.app import CleverAgentsTuiApp, textual_available from cleveragents.tui.persona.registry import PersonaRegistry from cleveragents.tui.persona.state import PersonaState @@ -223,7 +226,7 @@ class TuiCommandRouter: return f"Import failed: {exc}" -def _build_tui_facade() -> Any: +def _build_tui_facade() -> A2aLocalFacade | None: """Build and wire an :class:`~cleveragents.a2a.facade.A2aLocalFacade` for the TUI. Follows the same pattern as ``_facade_dispatch`` in