fix(tui): correct PersistentSessionService constructor and return type in TUI LLM dispatch
CI / push-validation (pull_request) Successful in 46s
CI / helm (pull_request) Successful in 51s
CI / build (pull_request) Successful in 1m16s
CI / lint (pull_request) Successful in 1m48s
CI / typecheck (pull_request) Successful in 1m55s
CI / security (pull_request) Successful in 1m56s
CI / quality (pull_request) Successful in 2m3s
CI / integration_tests (pull_request) Failing after 5m43s
CI / unit_tests (pull_request) Failing after 6m54s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
CI / push-validation (pull_request) Successful in 46s
CI / helm (pull_request) Successful in 51s
CI / build (pull_request) Successful in 1m16s
CI / lint (pull_request) Successful in 1m48s
CI / typecheck (pull_request) Successful in 1m55s
CI / security (pull_request) Successful in 1m56s
CI / quality (pull_request) Successful in 2m3s
CI / integration_tests (pull_request) Failing after 5m43s
CI / unit_tests (pull_request) Failing after 6m54s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
- 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
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user