mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
922ce030dd
Review pass over the native-interrupt work. The load-bearing fix is the resume envelope: the TypeScript tool read only the shape the Python bridge produces, so a valid time pick came back to the model as "user did not pick a time" and the meeting was never scheduled. Reproduced at the wire on a real model, fixed with a normaliser that accepts both the wrapped and the raw payload, and covered by a unit test that fails when the read is reverted. The Python tool had the mirror-image gap and is fixed and tested the same way. Also in this pass: - Both demo pages read the interrupt payload without throwing during render, and a rejected resume is surfaced instead of leaving a green "Booked" badge. - The e2e specs asserted the pre-pause assistant bubble, so a broken resume passed; they now assert the last bubble, plus a cancel-path narration guard. - Fixture legs that gated on turn number now gate on whether the tool has already answered, matching the reference integration, and two bare D4 keys that shadowed D6 prompts are narrowed. - The reasoning parameter is Responses-API only and the reasoning model id no longer reaches the Anthropic or Bedrock branches. - Stale prose corrected in the parity notes, the setup docs, the shared human-in-the-loop pages, both probe docblocks and the agent server docstring.
134 lines
4.7 KiB
Python
134 lines
4.7 KiB
Python
"""Pytest configuration for strands showcase unit tests.
|
|
|
|
Ensures ``src/`` (so ``agents.agent`` imports) is on ``sys.path``.
|
|
Shared tools are accessible via the ``tools`` symlink at the project root.
|
|
|
|
Also installs minimal stubs for ``ag_ui_strands`` and ``strands`` so the
|
|
unit tests can run in environments where those heavy runtime deps aren't
|
|
installed. Tests that need real behavior monkey-patch the specific
|
|
symbols they touch.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import types
|
|
|
|
_HERE = os.path.dirname(__file__)
|
|
_PKG_ROOT = os.path.abspath(os.path.join(_HERE, "..", ".."))
|
|
|
|
# src/ holds agent_server.py and agents/
|
|
sys.path.insert(0, os.path.join(_PKG_ROOT, "src"))
|
|
# tools/ symlink at project root points to shared/python/tools
|
|
sys.path.insert(0, _PKG_ROOT)
|
|
|
|
|
|
class _Permissive:
|
|
"""Base stub that accepts any ``__init__`` args and exposes a writable
|
|
``_agents_by_thread`` attribute so ``build_showcase_agent`` can swap
|
|
the per-thread dict in place."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.args = args
|
|
self.kwargs = kwargs
|
|
self._agents_by_thread: dict = {}
|
|
|
|
|
|
def _install_stub_modules() -> None:
|
|
"""Install minimal stub modules so ``agents.agent`` can be imported.
|
|
|
|
Unit tests only exercise pure-Python logic (cap hook counter, dict
|
|
injection). We stub out strands / ag_ui_strands symbols with plain
|
|
placeholder classes; tests that care about behavior monkey-patch the
|
|
relevant attributes.
|
|
"""
|
|
if "ag_ui_strands" not in sys.modules:
|
|
m = types.ModuleType("ag_ui_strands")
|
|
m.StrandsAgent = _Permissive # type: ignore[attr-defined]
|
|
m.StrandsAgentConfig = _Permissive # type: ignore[attr-defined]
|
|
m.ToolBehavior = _Permissive # type: ignore[attr-defined]
|
|
|
|
class _FakeFastAPI:
|
|
"""Accepts the decorators agent_server applies (``@app.get`` etc.)."""
|
|
|
|
def _decorator(self, *a, **k):
|
|
def _wrap(fn):
|
|
return fn
|
|
|
|
return _wrap
|
|
|
|
get = post = put = delete = patch = _decorator
|
|
|
|
# agent_server.py also calls ``app.add_middleware(...)`` to
|
|
# install HealthMiddleware; accept that on the stub too.
|
|
def add_middleware(self, *a, **k):
|
|
return None
|
|
|
|
m.create_strands_app = lambda *a, **k: _FakeFastAPI() # type: ignore[attr-defined]
|
|
sys.modules["ag_ui_strands"] = m
|
|
|
|
if "strands" not in sys.modules:
|
|
m = types.ModuleType("strands")
|
|
m.Agent = _Permissive # type: ignore[attr-defined]
|
|
|
|
def _tool_decorator(func=None, **_kwargs):
|
|
if callable(func):
|
|
return func
|
|
|
|
def _wrap(f):
|
|
return f
|
|
|
|
return _wrap
|
|
|
|
m.tool = _tool_decorator # type: ignore[attr-defined]
|
|
sys.modules["strands"] = m
|
|
|
|
if "strands.hooks" not in sys.modules:
|
|
m = types.ModuleType("strands.hooks")
|
|
for name in (
|
|
"AfterToolCallEvent",
|
|
"BeforeInvocationEvent",
|
|
"BeforeToolCallEvent",
|
|
"HookProvider",
|
|
"HookRegistry",
|
|
):
|
|
setattr(m, name, type(name, (), {}))
|
|
sys.modules["strands.hooks"] = m
|
|
|
|
if "strands.models" not in sys.modules:
|
|
sys.modules["strands.models"] = types.ModuleType("strands.models")
|
|
|
|
if "strands.models.openai" not in sys.modules:
|
|
m = types.ModuleType("strands.models.openai")
|
|
m.OpenAIModel = _Permissive # type: ignore[attr-defined]
|
|
sys.modules["strands.models.openai"] = m
|
|
|
|
# The reasoning demos build their model from the Responses API surface, and
|
|
# the interrupt tool takes a ToolContext. Both are imported at agent-server
|
|
# import time, so a missing stub pulls the real packages in and the import
|
|
# fails on an unrelated stub (httpx) instead.
|
|
if "strands.models.openai_responses" not in sys.modules:
|
|
m = types.ModuleType("strands.models.openai_responses")
|
|
m.OpenAIResponsesModel = _Permissive # type: ignore[attr-defined]
|
|
sys.modules["strands.models.openai_responses"] = m
|
|
|
|
if "strands.types" not in sys.modules:
|
|
sys.modules["strands.types"] = types.ModuleType("strands.types")
|
|
|
|
if "strands.types.tools" not in sys.modules:
|
|
m = types.ModuleType("strands.types.tools")
|
|
m.ToolContext = _Permissive # type: ignore[attr-defined]
|
|
sys.modules["strands.types.tools"] = m
|
|
|
|
if "uvicorn" not in sys.modules:
|
|
m = types.ModuleType("uvicorn")
|
|
m.run = lambda *a, **k: None # type: ignore[attr-defined]
|
|
sys.modules["uvicorn"] = m
|
|
|
|
if "dotenv" not in sys.modules:
|
|
m = types.ModuleType("dotenv")
|
|
m.load_dotenv = lambda *a, **k: None # type: ignore[attr-defined]
|
|
sys.modules["dotenv"] = m
|
|
|
|
|
|
_install_stub_modules()
|