87a7ce35d7
CI / benchmark-regression (push) Has been skipped
CI / helm (push) Successful in 38s
CI / lint (push) Successful in 1m14s
CI / build (push) Successful in 1m10s
CI / push-validation (push) Successful in 49s
CI / quality (push) Successful in 1m35s
CI / typecheck (push) Successful in 1m39s
CI / security (push) Successful in 1m45s
CI / integration_tests (push) Successful in 3m44s
CI / e2e_tests (push) Successful in 4m29s
CI / unit_tests (push) Successful in 5m10s
CI / docker (push) Successful in 1m55s
CI / coverage (push) Successful in 11m8s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h20m21s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 1m35s
CI / docker (pull_request) Successful in 1m26s
CI / unit_tests (pull_request) Successful in 6m40s
CI / push-validation (pull_request) Successful in 1m24s
CI / quality (pull_request) Successful in 4m4s
CI / integration_tests (pull_request) Successful in 5m44s
CI / e2e_tests (pull_request) Failing after 6m5s
CI / helm (pull_request) Successful in 1m10s
CI / build (pull_request) Successful in 2m39s
CI / lint (pull_request) Successful in 2m58s
CI / typecheck (pull_request) Successful in 3m56s
CI / security (pull_request) Successful in 3m56s
CI / coverage (pull_request) Successful in 10m52s
CI / status-check (pull_request) Failing after 3s
Replace the M3 stub in `agents session tell` with real orchestrator actor
invocation via `SessionWorkflow.tell()`. The stub echoed a canned
`Acknowledged: ...` response without calling any LLM or actor; this
commit wires up the full pipeline:
Architecture:
- New `SessionWorkflow` (Application layer) orchestrates LLM invocation
for session tell. Accepts a `ProviderRegistry` and `ToolRegistry`;
falls back to `FakeListLLM` when no provider is configured.
- `LangChainSessionCaller` implements the `LLMCaller` protocol, building
history-aware LangChain message lists from the session conversation and
invoking the LLM via `ToolCallingRuntime.run_tool_loop()`.
- `TellResult` (Pydantic BaseModel) carries the assistant response plus
token usage (input_tokens, output_tokens, cost_usd, duration_ms).
Domain / service layer:
- `SessionActorNotConfiguredError` added to the session domain model;
raised when tell is invoked with no actor on the session and no
`--actor` override. Clear message; CLI exits with code 1.
- `SessionService.get_messages()` abstract method added (+ implementation
in `PersistentSessionService`) to load ordered message history.
A2A facade:
- `A2aLocalFacade` gains `message/send` and `message/stream` standard
A2A operation handlers that route to `SessionWorkflow.tell()`.
Total supported operations count: 42 → 44.
CLI:
- `session tell` command delegates to `_build_session_workflow()`
(patchable factory) instead of directly calling `SessionService`.
- Non-streaming: `SessionWorkflow.tell()` returns `TellResult`; output
includes a Usage panel (Rich/Plain) or a `usage` object (JSON/YAML).
- Streaming: `SessionWorkflow.tell_stream()` yields tokens; CLI prints
them via `console.print` (not raw `sys.stdout.write`).
- Token usage recorded via `SessionService.update_token_usage()` in
both paths.
Tests:
- New Behave feature `session_tell_llm.feature` (4 scenarios): real LLM
response persisted; streaming yields tokens; no-actor exits code 1;
`--actor` override resolves correctly.
- New Robot suite `session_tell_llm.robot` (4 tests): end-to-end with
stub LLM injected via monkey-patching `_resolve_llm`.
- Updated all existing `session tell` test steps to patch
`_build_session_workflow` returning a mock `TellResult` so tests
remain isolated from the LLM layer.
- Updated operation-count assertions (42 → 44) in
`a2a_cli_facade_integration`, `consolidated_misc`,
`m6_autonomy_acceptance` feature files and steps.
Cycle 7 (Review ID 8088) fixes:
- Blocker 4: Split `session_workflow.py` (was 801 lines) into two files:
`session_workflow.py` (467 lines) and `session_caller.py` (318 lines).
Extracted: `LangChainSessionCaller`, `extract_content`,
`extract_token_usage`, `estimate_cost`, `estimate_tokens`,
`history_to_langchain_messages`, and stub classes.
- Blocker 5: Route CLI streaming path through
`_facade_dispatch("message/stream", ...)` instead of directly
calling `workflow.tell_stream()`. The facade's
`_handle_message_stream` falls back to non-streaming with
`streamed: false` (acceptable for this milestone per the spec).
- Updated streaming test assertion to validate full response presence
(no longer checks token-by-token word positions, since the facade
fallback returns a complete message).
ISSUES CLOSED: #5784
214 lines
7.0 KiB
Python
214 lines
7.0 KiB
Python
"""Helper script for session_tell_llm.robot integration tests.
|
|
|
|
Each subcommand is a self-contained check that prints a sentinel on success.
|
|
Tests verify SessionWorkflow.tell() / tell_stream() behaviour with a stub LLM.
|
|
|
|
No real API keys or network access required — all LLM calls are intercepted
|
|
by a monkey-patched ``_resolve_llm`` that returns a minimal stub.
|
|
"""
|
|
|
|
# ruff: noqa: E402, I001
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
# Ensure local source tree is importable
|
|
_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.domain.models.core.session import (
|
|
MessageRole,
|
|
SessionActorNotConfiguredError,
|
|
)
|
|
from cleveragents.infrastructure.database.models import Base
|
|
from cleveragents.infrastructure.database.repositories import (
|
|
SessionMessageRepository,
|
|
SessionRepository,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stub LLM — returns deterministic responses without LLM API calls
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_STUB_TEXT = "This is a real response from the stub LLM actor."
|
|
|
|
|
|
class _StubLLM:
|
|
"""Minimal LLM stub that behaves like a LangChain chat model."""
|
|
|
|
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": 10, "output_tokens": 20}
|
|
}
|
|
|
|
return _Resp()
|
|
|
|
def stream(self, messages: Any, **kwargs: Any) -> Iterator[Any]:
|
|
for word in _STUB_TEXT.split():
|
|
|
|
class _Chunk:
|
|
def __init__(self, t: str) -> None:
|
|
self.content = t + " "
|
|
|
|
yield _Chunk(word)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixture helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_db_session_factory() -> Any:
|
|
"""Create an in-memory SQLite engine and return a SQLAlchemy session factory."""
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
Base.metadata.create_all(engine)
|
|
return sessionmaker(bind=engine, expire_on_commit=False)
|
|
|
|
|
|
def _make_workflow(
|
|
actor_name: str | None = "openai/gpt-4",
|
|
llm_factory: Any = None,
|
|
) -> tuple[SessionWorkflow, Any]:
|
|
"""Build a real SessionWorkflow backed by an in-memory SQLite database."""
|
|
db_factory = _make_db_session_factory()
|
|
db_session = db_factory()
|
|
|
|
def get_db() -> Any:
|
|
return db_session
|
|
|
|
session_repo = SessionRepository(get_db)
|
|
message_repo = SessionMessageRepository(get_db)
|
|
svc = PersistentSessionService(session_repo=session_repo, message_repo=message_repo)
|
|
|
|
# Create a real session in the DB
|
|
session = svc.create(actor_name=actor_name)
|
|
|
|
factory = llm_factory if llm_factory is not None else lambda _actor: _StubLLM()
|
|
wf = SessionWorkflow(session_service=svc, llm_factory=factory)
|
|
# No monkey-patching needed — llm_factory provides clean DI (addresses M8)
|
|
|
|
return wf, session
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Subcommands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def tell_persists() -> None:
|
|
"""Verify tell() returns real LLM response and persists messages."""
|
|
wf, session = _make_workflow(actor_name="openai/gpt-4")
|
|
result = wf.tell(
|
|
session_id=session.session_id,
|
|
prompt="What can you help me with?",
|
|
)
|
|
assert result.assistant_message == _STUB_TEXT, (
|
|
f"Expected stub response, got: {result.assistant_message!r}"
|
|
)
|
|
assert result.input_tokens > 0, "input_tokens should be > 0"
|
|
assert result.output_tokens > 0, "output_tokens should be > 0"
|
|
# Verify messages were persisted via the service
|
|
messages = wf._session_service.get_messages(session.session_id)
|
|
roles = [m.role for m in messages]
|
|
assert MessageRole.USER in roles, "User message not persisted"
|
|
assert MessageRole.ASSISTANT in roles, "Assistant message not persisted"
|
|
print("session-tell-persists-ok")
|
|
|
|
|
|
def tell_no_actor() -> None:
|
|
"""Verify tell() raises SessionActorNotConfiguredError when no actor set."""
|
|
wf, session = _make_workflow(actor_name=None)
|
|
try:
|
|
wf.tell(session_id=session.session_id, prompt="Hello")
|
|
print(
|
|
"FAIL: expected SessionActorNotConfiguredError not raised",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
except SessionActorNotConfiguredError as exc:
|
|
assert "actor" in str(exc).lower(), (
|
|
f"Error message should mention 'actor', got: {exc}"
|
|
)
|
|
print("session-tell-no-actor-ok")
|
|
|
|
|
|
def tell_stream() -> None:
|
|
"""Verify tell_stream() yields tokens from the stub LLM."""
|
|
wf, session = _make_workflow(actor_name="openai/gpt-4")
|
|
tokens: list[str] = []
|
|
for token in wf.tell_stream(
|
|
session_id=session.session_id,
|
|
prompt="Hello",
|
|
):
|
|
tokens.append(token)
|
|
assert len(tokens) > 0, "Expected tokens to be yielded, got none"
|
|
full_response = "".join(tokens).strip()
|
|
assert len(full_response) > 0, f"Expected non-empty stream, got: {full_response!r}"
|
|
print("session-tell-stream-ok")
|
|
|
|
|
|
def tell_actor_override() -> None:
|
|
"""Verify tell() uses actor_override instead of session actor."""
|
|
captured_actor: list[str] = []
|
|
|
|
def _tracking_resolve(actor_name: str) -> Any:
|
|
captured_actor.append(actor_name)
|
|
return _StubLLM()
|
|
|
|
wf, session = _make_workflow(
|
|
actor_name="anthropic/claude-3-haiku",
|
|
llm_factory=_tracking_resolve,
|
|
)
|
|
|
|
result = wf.tell(
|
|
session_id=session.session_id,
|
|
prompt="Hello",
|
|
actor_override="openai/gpt-4",
|
|
)
|
|
assert captured_actor, "Actor resolver was never called"
|
|
assert captured_actor[0] == "openai/gpt-4", (
|
|
f"Expected 'openai/gpt-4' actor, got {captured_actor[0]!r}"
|
|
)
|
|
assert result.assistant_message is not None
|
|
print("session-tell-actor-override-ok")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
COMMANDS = {
|
|
"tell-persists": tell_persists,
|
|
"tell-no-actor": tell_no_actor,
|
|
"tell-stream": tell_stream,
|
|
"tell-actor-override": tell_actor_override,
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
|
|
print(
|
|
f"Usage: {sys.argv[0]} <command>\nCommands: {', '.join(COMMANDS)}",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
COMMANDS[sys.argv[1]]()
|