tylor-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.aws-setup.sh +25 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.mcp.json +12 -0
- package/AGENTS.md +93 -0
- package/CLAUDE.md +99 -0
- package/CLAUDE_PLATFORM_AWS_SETUP.md +105 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/assets/tylor_logo.png +0 -0
- package/assets/tylor_threads_concept.png +0 -0
- package/bin/tylor.js +23 -0
- package/hooks/kill-thread-trigger.sh +7 -0
- package/hooks/post-tool-use-code-index.sh +7 -0
- package/hooks/session-checkpoint.sh +7 -0
- package/hooks/session-start.sh +7 -0
- package/install.py +401 -0
- package/install.sh +260 -0
- package/package.json +24 -0
- package/pytest.ini +2 -0
- package/registry.json +26 -0
- package/server/.env.example +24 -0
- package/server/__init__.py +0 -0
- package/server/config.py +89 -0
- package/server/main.py +93 -0
- package/server/personas/analyst.md +15 -0
- package/server/personas/ceo.md +14 -0
- package/server/personas/code_agent.md +15 -0
- package/server/personas/cto.md +14 -0
- package/server/provision.py +260 -0
- package/server/provision_opensearch.py +154 -0
- package/server/requirements.txt +26 -0
- package/server/storage/__init__.py +0 -0
- package/server/storage/dynamo.py +399 -0
- package/server/storage/json_store.py +359 -0
- package/server/storage/opensearch.py +194 -0
- package/server/storage/s3.py +96 -0
- package/server/storage/tests/__init__.py +0 -0
- package/server/storage/tests/test_dynamo.py +452 -0
- package/server/storage/tests/test_json_store.py +226 -0
- package/server/storage/tests/test_opensearch.py +270 -0
- package/server/storage/tests/test_s3.py +125 -0
- package/server/tests/__init__.py +0 -0
- package/server/tests/test_install.py +606 -0
- package/server/tests/test_isolation.py +90 -0
- package/server/tests/test_ui_server.py +385 -0
- package/server/tests/test_ui_shader_background.py +52 -0
- package/server/tests/test_ui_story_6_3.py +105 -0
- package/server/tools/__init__.py +0 -0
- package/server/tools/_mcp.py +4 -0
- package/server/tools/agents.py +160 -0
- package/server/tools/ecc/__init__.py +1 -0
- package/server/tools/ecc/data.py +35 -0
- package/server/tools/ecc/diagrams.py +23 -0
- package/server/tools/ecc/pipeline.py +24 -0
- package/server/tools/ecc/presentation.py +24 -0
- package/server/tools/ecc/web.py +23 -0
- package/server/tools/executor.py +880 -0
- package/server/tools/harness.py +330 -0
- package/server/tools/help.py +162 -0
- package/server/tools/hooks.py +357 -0
- package/server/tools/personas.py +110 -0
- package/server/tools/registry.py +195 -0
- package/server/tools/router.py +117 -0
- package/server/tools/skill_installer.py +230 -0
- package/server/tools/summarizer.py +168 -0
- package/server/tools/tests/__init__.py +0 -0
- package/server/tools/tests/test_agents.py +246 -0
- package/server/tools/tests/test_code_index.py +108 -0
- package/server/tools/tests/test_ecc_tools.py +51 -0
- package/server/tools/tests/test_executor.py +584 -0
- package/server/tools/tests/test_help_agent101.py +149 -0
- package/server/tools/tests/test_hooks.py +124 -0
- package/server/tools/tests/test_kill_thread.py +125 -0
- package/server/tools/tests/test_new_thread_list_threads.py +293 -0
- package/server/tools/tests/test_personas.py +52 -0
- package/server/tools/tests/test_recall_memory.py +55 -0
- package/server/tools/tests/test_registry_client.py +308 -0
- package/server/tools/tests/test_router.py +263 -0
- package/server/tools/tests/test_skill_installer.py +174 -0
- package/server/tools/tests/test_switch_thread.py +163 -0
- package/server/tools/tests/test_thread_command_skills.py +54 -0
- package/server/tools/tests/test_thread_resolver.py +165 -0
- package/server/tools/tests/test_tier1_schema.py +296 -0
- package/server/tools/thread_resolver.py +75 -0
- package/server/tools/tylor.py +374 -0
- package/server/tools/ui.py +38 -0
- package/server/ui_server.py +292 -0
- package/server/validate.py +237 -0
- package/skills/add-skill/SKILL.md +37 -0
- package/skills/afk-status/SKILL.md +20 -0
- package/skills/bmad/SKILL.md +14 -0
- package/skills/help-agent101/SKILL.md +48 -0
- package/skills/kill-thread/SKILL.md +35 -0
- package/skills/list-threads/SKILL.md +35 -0
- package/skills/new-thread/SKILL.md +35 -0
- package/skills/recall/SKILL.md +39 -0
- package/skills/run/SKILL.md +33 -0
- package/skills/set-sandbox/SKILL.md +38 -0
- package/skills/switch-thread/SKILL.md +38 -0
- package/ui/claude-logo.png +0 -0
- package/ui/index.html +1314 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Story 1.4: FastMCP Server Skeleton with Tier 1 Tools
|
|
3
|
+
Run: pytest server/tools/tests/test_tier1_schema.py -v
|
|
4
|
+
"""
|
|
5
|
+
import importlib
|
|
6
|
+
import inspect
|
|
7
|
+
import sys
|
|
8
|
+
import logging
|
|
9
|
+
import asyncio
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
# Ensure project root is on path
|
|
16
|
+
PLUGIN_DIR = Path(__file__).parent.parent.parent.parent
|
|
17
|
+
sys.path.insert(0, str(PLUGIN_DIR))
|
|
18
|
+
|
|
19
|
+
TIER_1_TOOLS = [
|
|
20
|
+
"new_thread",
|
|
21
|
+
"switch_thread",
|
|
22
|
+
"switch_thread_by_name",
|
|
23
|
+
"kill_thread",
|
|
24
|
+
"save_memory",
|
|
25
|
+
"recall_memory",
|
|
26
|
+
"list_threads",
|
|
27
|
+
"list_personas",
|
|
28
|
+
"spawn_agent",
|
|
29
|
+
"load_skill_tools",
|
|
30
|
+
"list_registry",
|
|
31
|
+
"help_agent101",
|
|
32
|
+
"add_skill",
|
|
33
|
+
"set_sandbox",
|
|
34
|
+
"execute_in_sandbox",
|
|
35
|
+
"execute_with_recovery",
|
|
36
|
+
"start_afk",
|
|
37
|
+
"afk_status",
|
|
38
|
+
"pause_afk",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# AC1: Server imports without error
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
def test_main_module_imports_cleanly():
|
|
47
|
+
"""server.main must be importable without side effects or crashes."""
|
|
48
|
+
if "server.main" in sys.modules:
|
|
49
|
+
del sys.modules["server.main"]
|
|
50
|
+
mod = importlib.import_module("server.main")
|
|
51
|
+
assert mod is not None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_mcp_singleton_name():
|
|
55
|
+
"""The FastMCP instance must be named 'agent101'."""
|
|
56
|
+
from server.tools._mcp import mcp
|
|
57
|
+
assert mcp.name == "agent101"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# AC2: All 8 Tier 1 tools are registered
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def test_all_tier1_tools_registered():
|
|
65
|
+
"""All Tier 1 tools must be registered on the mcp instance."""
|
|
66
|
+
import asyncio
|
|
67
|
+
# Ensure startup registration imports every Tier 1 tool module.
|
|
68
|
+
import server.main
|
|
69
|
+
from server.tools._mcp import mcp
|
|
70
|
+
|
|
71
|
+
tools = asyncio.run(mcp.list_tools())
|
|
72
|
+
registered = {t.name for t in tools}
|
|
73
|
+
|
|
74
|
+
missing = [t for t in TIER_1_TOOLS if t not in registered]
|
|
75
|
+
assert not missing, f"Missing Tier 1 tools: {missing}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# AC2: Tool parameter signatures are correct
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def test_new_thread_signature():
|
|
83
|
+
from server.tools.tylor import new_thread
|
|
84
|
+
sig = inspect.signature(new_thread)
|
|
85
|
+
assert "name" in sig.parameters
|
|
86
|
+
assert sig.parameters["name"].annotation in (str, "str")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_switch_thread_signature():
|
|
90
|
+
from server.tools.tylor import switch_thread
|
|
91
|
+
sig = inspect.signature(switch_thread)
|
|
92
|
+
assert "thread_id" in sig.parameters
|
|
93
|
+
assert sig.parameters["thread_id"].annotation in (str, "str")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_switch_thread_by_name_signature():
|
|
97
|
+
from server.tools.tylor import switch_thread_by_name
|
|
98
|
+
sig = inspect.signature(switch_thread_by_name)
|
|
99
|
+
assert "query" in sig.parameters
|
|
100
|
+
assert sig.parameters["query"].annotation in (str, "str")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_kill_thread_is_sync():
|
|
104
|
+
# kill_thread is now sync — summarization dispatched by PostToolUse hook
|
|
105
|
+
from server.tools.tylor import kill_thread
|
|
106
|
+
assert not inspect.iscoroutinefunction(kill_thread)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_recall_memory_signature():
|
|
110
|
+
from server.tools.tylor import recall_memory
|
|
111
|
+
sig = inspect.signature(recall_memory)
|
|
112
|
+
assert "thread_id" in sig.parameters
|
|
113
|
+
assert "query" in sig.parameters
|
|
114
|
+
assert "top_k" in sig.parameters
|
|
115
|
+
assert "fact_type" in sig.parameters # renamed from `type` to avoid shadowing builtin
|
|
116
|
+
# top_k has default of 5
|
|
117
|
+
assert sig.parameters["top_k"].default == 5
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_save_memory_signature():
|
|
121
|
+
from server.tools.tylor import save_memory
|
|
122
|
+
sig = inspect.signature(save_memory)
|
|
123
|
+
assert "thread_id" in sig.parameters
|
|
124
|
+
assert "fact" in sig.parameters
|
|
125
|
+
assert "fact_type" in sig.parameters # renamed from `type` to avoid shadowing builtin
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_spawn_agent_signature():
|
|
129
|
+
from server.tools.agents import spawn_agent
|
|
130
|
+
sig = inspect.signature(spawn_agent)
|
|
131
|
+
assert "thread_id" in sig.parameters
|
|
132
|
+
assert "persona" in sig.parameters
|
|
133
|
+
assert "task" in sig.parameters
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_list_personas_signature():
|
|
137
|
+
from server.tools.agents import list_personas
|
|
138
|
+
sig = inspect.signature(list_personas)
|
|
139
|
+
assert not sig.parameters
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_load_skill_tools_signature():
|
|
143
|
+
from server.tools.registry import load_skill_tools
|
|
144
|
+
sig = inspect.signature(load_skill_tools)
|
|
145
|
+
assert "tool_group" in sig.parameters
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_add_skill_signature():
|
|
149
|
+
from server.tools.skill_installer import add_skill
|
|
150
|
+
sig = inspect.signature(add_skill)
|
|
151
|
+
assert "source_path" in sig.parameters
|
|
152
|
+
assert "overwrite" in sig.parameters
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_set_sandbox_signature():
|
|
156
|
+
from server.tools.executor import set_sandbox
|
|
157
|
+
sig = inspect.signature(set_sandbox)
|
|
158
|
+
assert "path" in sig.parameters
|
|
159
|
+
assert "thread_id" in sig.parameters
|
|
160
|
+
assert sig.parameters["thread_id"].default is None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_execute_in_sandbox_signature():
|
|
164
|
+
from server.tools.executor import execute_in_sandbox
|
|
165
|
+
sig = inspect.signature(execute_in_sandbox)
|
|
166
|
+
assert "command" in sig.parameters
|
|
167
|
+
assert "thread_id" in sig.parameters
|
|
168
|
+
assert "cwd" in sig.parameters
|
|
169
|
+
assert sig.parameters["timeout_seconds"].default == 120
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_execute_with_recovery_signature():
|
|
173
|
+
from server.tools.executor import execute_with_recovery
|
|
174
|
+
sig = inspect.signature(execute_with_recovery)
|
|
175
|
+
assert "command" in sig.parameters
|
|
176
|
+
assert "thread_id" in sig.parameters
|
|
177
|
+
assert "cwd" in sig.parameters
|
|
178
|
+
assert "timeout_seconds" in sig.parameters
|
|
179
|
+
assert "recovery_attempts_used" in sig.parameters
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_afk_tool_signatures():
|
|
183
|
+
from server.tools.executor import afk_status, pause_afk, start_afk
|
|
184
|
+
|
|
185
|
+
start_sig = inspect.signature(start_afk)
|
|
186
|
+
assert "task" in start_sig.parameters
|
|
187
|
+
assert "steps" in start_sig.parameters
|
|
188
|
+
assert "thread_id" in start_sig.parameters
|
|
189
|
+
assert "cwd" in start_sig.parameters
|
|
190
|
+
assert "background" in start_sig.parameters
|
|
191
|
+
|
|
192
|
+
status_sig = inspect.signature(afk_status)
|
|
193
|
+
assert "thread_id" in status_sig.parameters
|
|
194
|
+
|
|
195
|
+
pause_sig = inspect.signature(pause_afk)
|
|
196
|
+
assert "thread_id" in pause_sig.parameters
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# AC2: Tools raise McpError (not crash) when called without storage
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def test_kill_thread_raises_tool_error():
|
|
204
|
+
import asyncio
|
|
205
|
+
from unittest.mock import MagicMock, patch
|
|
206
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
207
|
+
from server.tools.tylor import kill_thread
|
|
208
|
+
mock_db = MagicMock()
|
|
209
|
+
mock_db.get_thread_meta.return_value = None
|
|
210
|
+
with pytest.raises(ToolError):
|
|
211
|
+
with patch("server.tools.tylor._get_db", return_value=mock_db):
|
|
212
|
+
asyncio.run(kill_thread(thread_id="t123"))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_new_thread_validates_short_name():
|
|
216
|
+
# Story 2.3: new_thread is implemented — validates input before any DB write
|
|
217
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
218
|
+
from server.tools.tylor import new_thread
|
|
219
|
+
with pytest.raises(ToolError, match="3\u201364 characters"):
|
|
220
|
+
new_thread(name="ab") # too short
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_list_threads_returns_dict():
|
|
224
|
+
# Story 2.3: list_threads is implemented — returns {threads: []} when DB is mocked empty
|
|
225
|
+
import server.tools.tylor as tylor_mod
|
|
226
|
+
from unittest.mock import MagicMock, patch
|
|
227
|
+
mock_db = MagicMock()
|
|
228
|
+
mock_db.query_all.return_value = []
|
|
229
|
+
with patch.object(tylor_mod, "_get_db", return_value=mock_db):
|
|
230
|
+
from server.tools.tylor import list_threads
|
|
231
|
+
result = list_threads()
|
|
232
|
+
assert "threads" in result
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_list_registry_returns_dict():
|
|
236
|
+
from server.tools.registry import list_registry
|
|
237
|
+
assert "skills" in list_registry()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
# AC3: config.py warns on missing optional keys without crashing
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
def test_config_loads_without_crash():
|
|
245
|
+
"""config.py must be importable without crashing regardless of env state."""
|
|
246
|
+
if "server.config" in sys.modules:
|
|
247
|
+
del sys.modules["server.config"]
|
|
248
|
+
mod = importlib.import_module("server.config")
|
|
249
|
+
assert hasattr(mod, "config")
|
|
250
|
+
assert isinstance(mod.config, dict)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_config_warns_on_missing_platform_key(caplog):
|
|
254
|
+
"""Missing Platform on AWS API key must emit a warning."""
|
|
255
|
+
env_backup = os.environ.pop("ANTHROPIC_PLATFORM_AWS_API_KEY", None)
|
|
256
|
+
aws_env_backup = os.environ.pop("ANTHROPIC_AWS_API_KEY", None)
|
|
257
|
+
try:
|
|
258
|
+
os.environ["ANTHROPIC_PLATFORM_AWS_API_KEY"] = ""
|
|
259
|
+
os.environ["ANTHROPIC_AWS_API_KEY"] = ""
|
|
260
|
+
if "server.config" in sys.modules:
|
|
261
|
+
del sys.modules["server.config"]
|
|
262
|
+
with caplog.at_level(logging.WARNING, logger="server.config"):
|
|
263
|
+
importlib.import_module("server.config")
|
|
264
|
+
assert any("ANTHROPIC_PLATFORM_AWS_API_KEY" in r.message for r in caplog.records)
|
|
265
|
+
finally:
|
|
266
|
+
os.environ.pop("ANTHROPIC_PLATFORM_AWS_API_KEY", None)
|
|
267
|
+
os.environ.pop("ANTHROPIC_AWS_API_KEY", None)
|
|
268
|
+
if env_backup is not None:
|
|
269
|
+
os.environ["ANTHROPIC_PLATFORM_AWS_API_KEY"] = env_backup
|
|
270
|
+
if aws_env_backup is not None:
|
|
271
|
+
os.environ["ANTHROPIC_AWS_API_KEY"] = aws_env_backup
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_config_bedrock_region_defaults_to_us_east_1():
|
|
275
|
+
"""Bedrock region must default to us-east-1 when not set."""
|
|
276
|
+
env_backup = os.environ.pop("BEDROCK_REGION", None)
|
|
277
|
+
try:
|
|
278
|
+
if "server.config" in sys.modules:
|
|
279
|
+
del sys.modules["server.config"]
|
|
280
|
+
mod = importlib.import_module("server.config")
|
|
281
|
+
assert mod.config["bedrock_region"] == "us-east-1"
|
|
282
|
+
finally:
|
|
283
|
+
if env_backup is not None:
|
|
284
|
+
os.environ["BEDROCK_REGION"] = env_backup
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_config_reads_aws_profile_from_env():
|
|
288
|
+
"""AWS_PROFILE env var must be reflected in config."""
|
|
289
|
+
os.environ["AWS_PROFILE"] = "agent101-test"
|
|
290
|
+
try:
|
|
291
|
+
if "server.config" in sys.modules:
|
|
292
|
+
del sys.modules["server.config"]
|
|
293
|
+
mod = importlib.import_module("server.config")
|
|
294
|
+
assert mod.config["aws_profile"] == "agent101-test"
|
|
295
|
+
finally:
|
|
296
|
+
del os.environ["AWS_PROFILE"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
server/tools/thread_resolver.py — fuzzy thread-name resolution.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from mcp.shared.exceptions import McpError
|
|
9
|
+
from mcp.types import ErrorData, INVALID_REQUEST
|
|
10
|
+
from rapidfuzz import process
|
|
11
|
+
|
|
12
|
+
SCORE_CUTOFF = 70
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _invalid_request(message: str) -> McpError:
|
|
16
|
+
return McpError(ErrorData(code=INVALID_REQUEST, message=message))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _thread_choices(threads: list[dict]) -> dict[str, dict]:
|
|
20
|
+
return {
|
|
21
|
+
thread.get("name", ""): thread
|
|
22
|
+
for thread in threads
|
|
23
|
+
if thread.get("name") and thread.get("thread_id")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _format_ambiguous(matches: list[dict]) -> str:
|
|
28
|
+
choices = ", ".join(
|
|
29
|
+
f"[{idx}] {match['name']}"
|
|
30
|
+
for idx, match in enumerate(matches, start=1)
|
|
31
|
+
)
|
|
32
|
+
return f"Did you mean: {choices}?"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_thread_name(
|
|
36
|
+
query: str,
|
|
37
|
+
threads: list[dict[str, Any]],
|
|
38
|
+
score_cutoff: int = SCORE_CUTOFF,
|
|
39
|
+
) -> dict:
|
|
40
|
+
"""Resolve a fuzzy thread-name query to one thread or ambiguous choices."""
|
|
41
|
+
cleaned = query.strip() if query else ""
|
|
42
|
+
if not cleaned:
|
|
43
|
+
raise _invalid_request("No thread found matching '' - run list_threads to see available threads")
|
|
44
|
+
|
|
45
|
+
choices = _thread_choices(threads)
|
|
46
|
+
all_matches = process.extract(
|
|
47
|
+
cleaned,
|
|
48
|
+
choices.keys(),
|
|
49
|
+
score_cutoff=score_cutoff,
|
|
50
|
+
limit=None,
|
|
51
|
+
)
|
|
52
|
+
if not all_matches:
|
|
53
|
+
raise _invalid_request(
|
|
54
|
+
f"No thread found matching '{cleaned}' - run list_threads to see available threads"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
matches = [
|
|
58
|
+
{**choices[name], "score": score}
|
|
59
|
+
for name, score, _index in all_matches
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
if len(matches) > 1:
|
|
63
|
+
return {
|
|
64
|
+
"status": "ambiguous",
|
|
65
|
+
"matches": matches,
|
|
66
|
+
"message": _format_ambiguous(matches),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
thread = choices[all_matches[0][0]]
|
|
70
|
+
return {
|
|
71
|
+
"status": "resolved",
|
|
72
|
+
"thread_id": thread["thread_id"],
|
|
73
|
+
"name": thread["name"],
|
|
74
|
+
"message": f"Switching to thread: {thread['name']}",
|
|
75
|
+
}
|