casual-mcp 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
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.
- casual_mcp/__init__.py +4 -0
- casual_mcp/cli.py +379 -6
- casual_mcp/main.py +93 -8
- casual_mcp/mcp_tool_chat.py +152 -15
- casual_mcp/models/__init__.py +16 -0
- casual_mcp/models/chat_stats.py +37 -0
- casual_mcp/models/config.py +3 -1
- casual_mcp/models/toolset_config.py +40 -0
- casual_mcp/tool_filter.py +171 -0
- casual_mcp-0.7.0.dist-info/METADATA +193 -0
- casual_mcp-0.7.0.dist-info/RECORD +23 -0
- casual_mcp-0.5.0.dist-info/METADATA +0 -630
- casual_mcp-0.5.0.dist-info/RECORD +0 -20
- {casual_mcp-0.5.0.dist-info → casual_mcp-0.7.0.dist-info}/WHEEL +0 -0
- {casual_mcp-0.5.0.dist-info → casual_mcp-0.7.0.dist-info}/entry_points.txt +0 -0
- {casual_mcp-0.5.0.dist-info → casual_mcp-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {casual_mcp-0.5.0.dist-info → casual_mcp-0.7.0.dist-info}/top_level.txt +0 -0
casual_mcp/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from importlib.metadata import version
|
|
2
2
|
|
|
3
3
|
from . import models
|
|
4
|
+
from .models.chat_stats import ChatStats, TokenUsageStats, ToolCallStats
|
|
4
5
|
|
|
5
6
|
__version__ = version("casual-mcp")
|
|
6
7
|
from .mcp_tool_chat import McpToolChat
|
|
@@ -17,4 +18,7 @@ __all__ = [
|
|
|
17
18
|
"load_mcp_client",
|
|
18
19
|
"render_system_prompt",
|
|
19
20
|
"models",
|
|
21
|
+
"ChatStats",
|
|
22
|
+
"TokenUsageStats",
|
|
23
|
+
"ToolCallStats",
|
|
20
24
|
]
|
casual_mcp/cli.py
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import gc
|
|
3
|
+
import json
|
|
4
|
+
import warnings
|
|
5
|
+
from pathlib import Path
|
|
2
6
|
from typing import Any
|
|
3
7
|
|
|
4
8
|
import mcp
|
|
9
|
+
import questionary
|
|
5
10
|
import typer
|
|
6
11
|
import uvicorn
|
|
7
12
|
from fastmcp import Client
|
|
@@ -9,6 +14,8 @@ from rich.console import Console
|
|
|
9
14
|
from rich.table import Table
|
|
10
15
|
|
|
11
16
|
from casual_mcp.models.mcp_server_config import RemoteServerConfig
|
|
17
|
+
from casual_mcp.models.toolset_config import ExcludeSpec, ToolSpec
|
|
18
|
+
from casual_mcp.tool_filter import extract_server_and_tool
|
|
12
19
|
from casual_mcp.utils import load_config, load_mcp_client
|
|
13
20
|
|
|
14
21
|
app = typer.Typer()
|
|
@@ -68,16 +75,382 @@ def tools() -> None:
|
|
|
68
75
|
config = load_config("casual_mcp_config.json")
|
|
69
76
|
mcp_client = load_mcp_client(config)
|
|
70
77
|
table = Table("Name", "Description")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
for tool in tools:
|
|
78
|
+
tool_list = run_async_with_cleanup(get_tools_and_cleanup(mcp_client))
|
|
79
|
+
for tool in tool_list:
|
|
74
80
|
table.add_row(tool.name, tool.description)
|
|
75
81
|
console.print(table)
|
|
76
82
|
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
async with
|
|
80
|
-
|
|
84
|
+
def run_async_with_cleanup(coro: Any) -> Any:
|
|
85
|
+
"""Run async coroutine with proper subprocess cleanup.
|
|
86
|
+
|
|
87
|
+
This wrapper filters/ignores the "Event loop is closed" warning that occurs
|
|
88
|
+
when subprocess transports don't finish cleanup before the event loop closes.
|
|
89
|
+
It also forces gc.collect() after execution to help clean up remaining transports.
|
|
90
|
+
"""
|
|
91
|
+
with warnings.catch_warnings():
|
|
92
|
+
warnings.filterwarnings("ignore", message="Event loop is closed")
|
|
93
|
+
try:
|
|
94
|
+
return asyncio.run(coro)
|
|
95
|
+
finally:
|
|
96
|
+
# Force garbage collection to clean up any remaining transports
|
|
97
|
+
gc.collect()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def get_tools_and_cleanup(client: Client[Any]) -> list[mcp.Tool]:
|
|
101
|
+
"""Get tools and ensure proper cleanup to avoid subprocess warnings."""
|
|
102
|
+
try:
|
|
103
|
+
async with client:
|
|
104
|
+
return await client.list_tools()
|
|
105
|
+
finally:
|
|
106
|
+
# Give transports time to close cleanly
|
|
107
|
+
await asyncio.sleep(0.1)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _build_server_tool_map(tools: list[mcp.Tool], server_names: set[str]) -> dict[str, list[str]]:
|
|
111
|
+
"""Build a mapping of server names to their tool names."""
|
|
112
|
+
server_tools: dict[str, list[str]] = {s: [] for s in server_names}
|
|
113
|
+
for tool in tools:
|
|
114
|
+
server_name, base_name = extract_server_and_tool(tool.name, server_names)
|
|
115
|
+
if server_name in server_tools:
|
|
116
|
+
server_tools[server_name].append(base_name)
|
|
117
|
+
return server_tools
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _format_tool_spec(spec: ToolSpec) -> str:
|
|
121
|
+
"""Format a tool spec for display.
|
|
122
|
+
|
|
123
|
+
Note: Brackets are escaped for Rich console output.
|
|
124
|
+
"""
|
|
125
|
+
if spec is True:
|
|
126
|
+
return "\\[all tools]"
|
|
127
|
+
elif isinstance(spec, list):
|
|
128
|
+
return ", ".join(spec)
|
|
129
|
+
elif isinstance(spec, ExcludeSpec):
|
|
130
|
+
return f"\\[all except: {', '.join(spec.exclude)}]"
|
|
131
|
+
return str(spec)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.command()
|
|
135
|
+
def toolsets() -> None:
|
|
136
|
+
"""Interactively manage toolsets - create, edit, and delete."""
|
|
137
|
+
config_path = Path("casual_mcp_config.json")
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
config = load_config(config_path)
|
|
141
|
+
|
|
142
|
+
# Build menu choices
|
|
143
|
+
choices: list[Any] = []
|
|
144
|
+
|
|
145
|
+
if config.tool_sets:
|
|
146
|
+
for name, ts in config.tool_sets.items():
|
|
147
|
+
servers = ", ".join(ts.servers.keys()) or "no servers"
|
|
148
|
+
desc = ts.description[:40] + "..." if len(ts.description) > 40 else ts.description
|
|
149
|
+
display = f"{name} - {desc} ({servers})"
|
|
150
|
+
choices.append(questionary.Choice(title=display, value=name))
|
|
151
|
+
|
|
152
|
+
choices.append(questionary.Separator())
|
|
153
|
+
|
|
154
|
+
choices.append(questionary.Choice(title="➕ Create new toolset", value="__create__"))
|
|
155
|
+
choices.append(questionary.Choice(title="❌ Exit", value="__exit__"))
|
|
156
|
+
|
|
157
|
+
selection = questionary.select(
|
|
158
|
+
"Toolsets:" if config.tool_sets else "No toolsets configured:",
|
|
159
|
+
choices=choices,
|
|
160
|
+
).ask()
|
|
161
|
+
|
|
162
|
+
if selection is None or selection == "__exit__":
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
if selection == "__create__":
|
|
166
|
+
_create_toolset(config_path)
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
# Selected an existing toolset - show actions
|
|
170
|
+
_toolset_actions(config_path, selection)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _create_toolset(config_path: Path) -> None:
|
|
174
|
+
"""Prompt for name and create a new toolset."""
|
|
175
|
+
config = load_config(config_path)
|
|
176
|
+
|
|
177
|
+
name = questionary.text("Toolset name:").ask()
|
|
178
|
+
if not name:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
if name in config.tool_sets:
|
|
182
|
+
console.print(f"[red]Toolset '{name}' already exists[/red]")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
_interactive_toolset_edit(config_path, config, name, is_new=True)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _toolset_actions(config_path: Path, name: str) -> None:
|
|
189
|
+
"""Show actions for an existing toolset."""
|
|
190
|
+
config = load_config(config_path)
|
|
191
|
+
ts = config.tool_sets.get(name)
|
|
192
|
+
|
|
193
|
+
if not ts:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# Show toolset details
|
|
197
|
+
console.print(f"\n[bold]{name}[/bold]")
|
|
198
|
+
console.print(f"Description: {ts.description}")
|
|
199
|
+
console.print("Servers:")
|
|
200
|
+
for server, spec in ts.servers.items():
|
|
201
|
+
console.print(f" {server}: {_format_tool_spec(spec)}")
|
|
202
|
+
console.print()
|
|
203
|
+
|
|
204
|
+
action = questionary.select(
|
|
205
|
+
"Action:",
|
|
206
|
+
choices=[
|
|
207
|
+
questionary.Choice(title="✏️ Edit", value="edit"),
|
|
208
|
+
questionary.Choice(title="🗑️ Delete", value="delete"),
|
|
209
|
+
questionary.Choice(title="← Back", value="back"),
|
|
210
|
+
],
|
|
211
|
+
).ask()
|
|
212
|
+
|
|
213
|
+
if action is None or action == "back":
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
if action == "edit":
|
|
217
|
+
_interactive_toolset_edit(config_path, config, name, is_new=False)
|
|
218
|
+
elif action == "delete":
|
|
219
|
+
_delete_toolset(config_path, name)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _delete_toolset(config_path: Path, name: str) -> None:
|
|
223
|
+
"""Delete a toolset after confirmation."""
|
|
224
|
+
confirmed = questionary.confirm(f"Delete toolset '{name}'?", default=False).ask()
|
|
225
|
+
if not confirmed:
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
with config_path.open("r", encoding="utf-8") as f:
|
|
229
|
+
raw = json.load(f)
|
|
230
|
+
|
|
231
|
+
# Check if tool_sets exists and contains the toolset
|
|
232
|
+
if "tool_sets" not in raw:
|
|
233
|
+
console.print("[red]No toolsets found in config[/red]")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
if name not in raw["tool_sets"]:
|
|
237
|
+
console.print(f"[red]Toolset '{name}' not found in config[/red]")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
del raw["tool_sets"][name]
|
|
241
|
+
|
|
242
|
+
# Remove tool_sets key if empty
|
|
243
|
+
if not raw["tool_sets"]:
|
|
244
|
+
del raw["tool_sets"]
|
|
245
|
+
|
|
246
|
+
with config_path.open("w", encoding="utf-8") as f:
|
|
247
|
+
json.dump(raw, f, indent=4)
|
|
248
|
+
|
|
249
|
+
console.print(f"[green]Deleted toolset '{name}'[/green]")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _format_server_status(server: str, spec: ToolSpec | None, tool_count: int) -> str:
|
|
253
|
+
"""Format server status for display in the menu."""
|
|
254
|
+
if spec is None:
|
|
255
|
+
return f"{server} ({tool_count} tools) - [dim]not included[/dim]"
|
|
256
|
+
elif spec is True:
|
|
257
|
+
return f"{server} ({tool_count} tools) - [green]all tools[/green]"
|
|
258
|
+
elif isinstance(spec, list):
|
|
259
|
+
return f"{server} ({tool_count} tools) - [cyan]{len(spec)} included[/cyan]"
|
|
260
|
+
elif isinstance(spec, ExcludeSpec):
|
|
261
|
+
return f"{server} ({tool_count} tools) - [yellow]{len(spec.exclude)} excluded[/yellow]"
|
|
262
|
+
return f"{server} ({tool_count} tools)"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _interactive_toolset_edit(
|
|
266
|
+
config_path: Path,
|
|
267
|
+
config: Any,
|
|
268
|
+
name: str,
|
|
269
|
+
is_new: bool,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Interactive toolset creation/editing with arrow-key navigation."""
|
|
272
|
+
from casual_mcp.models.config import Config
|
|
273
|
+
|
|
274
|
+
config = Config.model_validate(config.model_dump())
|
|
275
|
+
|
|
276
|
+
# Get available tools from servers
|
|
277
|
+
mcp_client = load_mcp_client(config)
|
|
278
|
+
tools = run_async_with_cleanup(get_tools_and_cleanup(mcp_client))
|
|
279
|
+
server_names = set(config.servers.keys())
|
|
280
|
+
server_tools = _build_server_tool_map(tools, server_names)
|
|
281
|
+
|
|
282
|
+
# Get existing config if editing
|
|
283
|
+
existing = config.tool_sets.get(name)
|
|
284
|
+
existing_description = existing.description if existing else ""
|
|
285
|
+
existing_servers: dict[str, Any] = dict(existing.servers) if existing else {}
|
|
286
|
+
|
|
287
|
+
# Working copy of server configs
|
|
288
|
+
new_servers: dict[str, Any] = dict(existing_servers)
|
|
289
|
+
|
|
290
|
+
if is_new:
|
|
291
|
+
console.print(f"[green]Creating new toolset '{name}'[/green]\n")
|
|
292
|
+
else:
|
|
293
|
+
console.print(f"[yellow]Editing toolset '{name}'[/yellow]\n")
|
|
294
|
+
|
|
295
|
+
# Get description
|
|
296
|
+
description = questionary.text(
|
|
297
|
+
"Description:",
|
|
298
|
+
default=existing_description,
|
|
299
|
+
).ask()
|
|
300
|
+
|
|
301
|
+
if description is None:
|
|
302
|
+
raise typer.Abort()
|
|
303
|
+
|
|
304
|
+
# Main loop - configure servers one at a time
|
|
305
|
+
sorted_servers = sorted(server_names)
|
|
306
|
+
|
|
307
|
+
while True:
|
|
308
|
+
console.print("\n[bold]Server Configuration:[/bold]")
|
|
309
|
+
console.print("[dim]Configure each server, then select 'Save and exit' when done.[/dim]\n")
|
|
310
|
+
|
|
311
|
+
# Build menu choices showing current status
|
|
312
|
+
choices = []
|
|
313
|
+
for server in sorted_servers:
|
|
314
|
+
spec = new_servers.get(server)
|
|
315
|
+
tool_count = len(server_tools[server])
|
|
316
|
+
status = _format_server_status(server, spec, tool_count)
|
|
317
|
+
# Strip Rich markup for questionary display
|
|
318
|
+
plain_status = (
|
|
319
|
+
status.replace("[dim]", "")
|
|
320
|
+
.replace("[/dim]", "")
|
|
321
|
+
.replace("[green]", "")
|
|
322
|
+
.replace("[/green]", "")
|
|
323
|
+
.replace("[cyan]", "")
|
|
324
|
+
.replace("[/cyan]", "")
|
|
325
|
+
.replace("[yellow]", "")
|
|
326
|
+
.replace("[/yellow]", "")
|
|
327
|
+
)
|
|
328
|
+
choices.append(questionary.Choice(title=plain_status, value=server))
|
|
329
|
+
|
|
330
|
+
choices.append(questionary.Separator())
|
|
331
|
+
choices.append(questionary.Choice(title="💾 Save and exit", value="__save__"))
|
|
332
|
+
choices.append(questionary.Choice(title="❌ Cancel", value="__cancel__"))
|
|
333
|
+
|
|
334
|
+
selection = questionary.select(
|
|
335
|
+
"Select a server to configure:",
|
|
336
|
+
choices=choices,
|
|
337
|
+
).ask()
|
|
338
|
+
|
|
339
|
+
if selection is None or selection == "__cancel__":
|
|
340
|
+
raise typer.Abort()
|
|
341
|
+
|
|
342
|
+
if selection == "__save__":
|
|
343
|
+
break
|
|
344
|
+
|
|
345
|
+
# Configure the selected server
|
|
346
|
+
server = selection
|
|
347
|
+
available = server_tools[server]
|
|
348
|
+
existing_spec = new_servers.get(server)
|
|
349
|
+
|
|
350
|
+
# Determine current mode for default selection
|
|
351
|
+
if existing_spec is None:
|
|
352
|
+
default_mode = "Don't include"
|
|
353
|
+
elif existing_spec is True:
|
|
354
|
+
default_mode = "All tools"
|
|
355
|
+
elif isinstance(existing_spec, list):
|
|
356
|
+
default_mode = "Include specific tools"
|
|
357
|
+
elif isinstance(existing_spec, ExcludeSpec):
|
|
358
|
+
default_mode = "Exclude specific tools"
|
|
359
|
+
else:
|
|
360
|
+
default_mode = "Don't include"
|
|
361
|
+
|
|
362
|
+
mode = questionary.select(
|
|
363
|
+
f"Configure {server} ({len(available)} tools):",
|
|
364
|
+
choices=[
|
|
365
|
+
"Don't include",
|
|
366
|
+
"All tools",
|
|
367
|
+
"Include specific tools",
|
|
368
|
+
"Exclude specific tools",
|
|
369
|
+
],
|
|
370
|
+
default=default_mode,
|
|
371
|
+
).ask()
|
|
372
|
+
|
|
373
|
+
if mode is None:
|
|
374
|
+
continue # Go back to server list
|
|
375
|
+
|
|
376
|
+
if mode == "Don't include":
|
|
377
|
+
new_servers.pop(server, None)
|
|
378
|
+
elif mode == "All tools":
|
|
379
|
+
new_servers[server] = True
|
|
380
|
+
elif mode == "Include specific tools":
|
|
381
|
+
# Determine which tools were previously selected
|
|
382
|
+
if isinstance(existing_spec, list):
|
|
383
|
+
pre_selected = set(existing_spec)
|
|
384
|
+
else:
|
|
385
|
+
pre_selected = set()
|
|
386
|
+
|
|
387
|
+
tool_choices = [
|
|
388
|
+
questionary.Choice(title=tool, value=tool, checked=tool in pre_selected)
|
|
389
|
+
for tool in available
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
console.print("\n[dim]Use space to select, enter to confirm[/dim]")
|
|
393
|
+
selected_tools = questionary.checkbox(
|
|
394
|
+
f"Select tools to include from {server}:",
|
|
395
|
+
choices=tool_choices,
|
|
396
|
+
).ask()
|
|
397
|
+
|
|
398
|
+
if selected_tools is None:
|
|
399
|
+
continue # Go back to server list
|
|
400
|
+
|
|
401
|
+
if selected_tools:
|
|
402
|
+
new_servers[server] = selected_tools
|
|
403
|
+
else:
|
|
404
|
+
new_servers.pop(server, None) # No tools = don't include
|
|
405
|
+
elif mode == "Exclude specific tools":
|
|
406
|
+
# Determine which tools were previously excluded
|
|
407
|
+
if isinstance(existing_spec, ExcludeSpec):
|
|
408
|
+
pre_excluded = set(existing_spec.exclude)
|
|
409
|
+
else:
|
|
410
|
+
pre_excluded = set()
|
|
411
|
+
|
|
412
|
+
tool_choices = [
|
|
413
|
+
questionary.Choice(title=tool, value=tool, checked=tool in pre_excluded)
|
|
414
|
+
for tool in available
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
console.print("\n[dim]Use space to select, enter to confirm[/dim]")
|
|
418
|
+
excluded_tools = questionary.checkbox(
|
|
419
|
+
f"Select tools to exclude from {server}:",
|
|
420
|
+
choices=tool_choices,
|
|
421
|
+
).ask()
|
|
422
|
+
|
|
423
|
+
if excluded_tools is None:
|
|
424
|
+
continue # Go back to server list
|
|
425
|
+
|
|
426
|
+
if excluded_tools:
|
|
427
|
+
new_servers[server] = {"exclude": excluded_tools}
|
|
428
|
+
else:
|
|
429
|
+
new_servers[server] = True # No exclusions = all tools
|
|
430
|
+
|
|
431
|
+
# Check if any servers are configured
|
|
432
|
+
if not new_servers:
|
|
433
|
+
console.print("[yellow]No servers configured - toolset will be empty[/yellow]")
|
|
434
|
+
confirm = questionary.confirm("Save empty toolset?", default=False).ask()
|
|
435
|
+
if not confirm:
|
|
436
|
+
raise typer.Abort()
|
|
437
|
+
|
|
438
|
+
# Save to config
|
|
439
|
+
with config_path.open("r", encoding="utf-8") as f:
|
|
440
|
+
raw = json.load(f)
|
|
441
|
+
|
|
442
|
+
if "tool_sets" not in raw:
|
|
443
|
+
raw["tool_sets"] = {}
|
|
444
|
+
|
|
445
|
+
raw["tool_sets"][name] = {
|
|
446
|
+
"description": description,
|
|
447
|
+
"servers": new_servers,
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
with config_path.open("w", encoding="utf-8") as f:
|
|
451
|
+
json.dump(raw, f, indent=4)
|
|
452
|
+
|
|
453
|
+
console.print(f"\n[green]Saved toolset '{name}'[/green]")
|
|
81
454
|
|
|
82
455
|
|
|
83
456
|
if __name__ == "__main__":
|
casual_mcp/main.py
CHANGED
|
@@ -8,8 +8,10 @@ from pydantic import BaseModel, Field
|
|
|
8
8
|
|
|
9
9
|
from casual_mcp import McpToolChat
|
|
10
10
|
from casual_mcp.logging import configure_logging, get_logger
|
|
11
|
+
from casual_mcp.models.toolset_config import ToolSetConfig
|
|
11
12
|
from casual_mcp.provider_factory import ProviderFactory
|
|
12
13
|
from casual_mcp.tool_cache import ToolCache
|
|
14
|
+
from casual_mcp.tool_filter import ToolSetValidationError
|
|
13
15
|
from casual_mcp.utils import load_config, load_mcp_client, render_system_prompt
|
|
14
16
|
|
|
15
17
|
# Load environment variables
|
|
@@ -45,28 +47,93 @@ class GenerateRequest(BaseModel):
|
|
|
45
47
|
model: str = Field(title="Model to use")
|
|
46
48
|
system_prompt: str | None = Field(default=None, title="System Prompt to use")
|
|
47
49
|
prompt: str = Field(title="User Prompt")
|
|
50
|
+
include_stats: bool = Field(default=False, title="Include usage statistics in response")
|
|
51
|
+
tool_set: str | None = Field(default=None, title="Name of toolset to use")
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
class ChatRequest(BaseModel):
|
|
51
55
|
model: str = Field(title="Model to use")
|
|
52
56
|
system_prompt: str | None = Field(default=None, title="System Prompt to use")
|
|
53
57
|
messages: list[ChatMessage] = Field(title="Previous messages to supply to the LLM")
|
|
58
|
+
include_stats: bool = Field(default=False, title="Include usage statistics in response")
|
|
59
|
+
tool_set: str | None = Field(default=None, title="Name of toolset to use")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def resolve_tool_set(tool_set_name: str | None) -> ToolSetConfig | None:
|
|
63
|
+
"""Resolve a tool set name to its configuration.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
tool_set_name: Name of the tool set, or None
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
ToolSetConfig if found, None if tool_set_name is None
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
HTTPException: If tool set name is provided but not found
|
|
73
|
+
"""
|
|
74
|
+
if tool_set_name is None:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
if tool_set_name not in config.tool_sets:
|
|
78
|
+
raise HTTPException(
|
|
79
|
+
status_code=400,
|
|
80
|
+
detail=f"Toolset '{tool_set_name}' not found. "
|
|
81
|
+
f"Available: {list(config.tool_sets.keys())}",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return config.tool_sets[tool_set_name]
|
|
54
85
|
|
|
55
86
|
|
|
56
87
|
@app.post("/chat")
|
|
57
88
|
async def chat(req: ChatRequest) -> dict[str, Any]:
|
|
58
|
-
|
|
59
|
-
|
|
89
|
+
tool_set_config = resolve_tool_set(req.tool_set)
|
|
90
|
+
chat_instance = await get_chat(req.model, req.system_prompt)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
messages = await chat_instance.chat(req.messages, tool_set=tool_set_config)
|
|
94
|
+
except ToolSetValidationError as e:
|
|
95
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
96
|
+
|
|
97
|
+
if not messages:
|
|
98
|
+
error_result: dict[str, Any] = {"messages": [], "response": ""}
|
|
99
|
+
if req.include_stats:
|
|
100
|
+
error_result["stats"] = chat_instance.get_stats()
|
|
101
|
+
raise HTTPException(
|
|
102
|
+
status_code=500,
|
|
103
|
+
detail={"error": "No response generated", **error_result},
|
|
104
|
+
)
|
|
60
105
|
|
|
61
|
-
|
|
106
|
+
result: dict[str, Any] = {"messages": messages, "response": messages[-1].content}
|
|
107
|
+
if req.include_stats:
|
|
108
|
+
result["stats"] = chat_instance.get_stats()
|
|
109
|
+
return result
|
|
62
110
|
|
|
63
111
|
|
|
64
112
|
@app.post("/generate")
|
|
65
113
|
async def generate(req: GenerateRequest) -> dict[str, Any]:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
114
|
+
tool_set_config = resolve_tool_set(req.tool_set)
|
|
115
|
+
chat_instance = await get_chat(req.model, req.system_prompt)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
messages = await chat_instance.generate(
|
|
119
|
+
req.prompt, req.session_id, tool_set=tool_set_config
|
|
120
|
+
)
|
|
121
|
+
except ToolSetValidationError as e:
|
|
122
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
123
|
+
|
|
124
|
+
if not messages:
|
|
125
|
+
error_result: dict[str, Any] = {"messages": [], "response": ""}
|
|
126
|
+
if req.include_stats:
|
|
127
|
+
error_result["stats"] = chat_instance.get_stats()
|
|
128
|
+
raise HTTPException(
|
|
129
|
+
status_code=500,
|
|
130
|
+
detail={"error": "No response generated", **error_result},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
result: dict[str, Any] = {"messages": messages, "response": messages[-1].content}
|
|
134
|
+
if req.include_stats:
|
|
135
|
+
result["stats"] = chat_instance.get_stats()
|
|
136
|
+
return result
|
|
70
137
|
|
|
71
138
|
|
|
72
139
|
@app.get("/generate/session/{session_id}")
|
|
@@ -78,6 +145,18 @@ async def get_generate_session(session_id: str) -> list[ChatMessage]:
|
|
|
78
145
|
return session
|
|
79
146
|
|
|
80
147
|
|
|
148
|
+
@app.get("/toolsets")
|
|
149
|
+
async def list_toolsets() -> dict[str, dict[str, Any]]:
|
|
150
|
+
"""List all available toolsets."""
|
|
151
|
+
return {
|
|
152
|
+
name: {
|
|
153
|
+
"description": ts.description,
|
|
154
|
+
"servers": list(ts.servers.keys()),
|
|
155
|
+
}
|
|
156
|
+
for name, ts in config.tool_sets.items()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
81
160
|
async def get_chat(model: str, system: str | None = None) -> McpToolChat:
|
|
82
161
|
# Get Provider from Model Config
|
|
83
162
|
model_config = config.models[model]
|
|
@@ -91,4 +170,10 @@ async def get_chat(model: str, system: str | None = None) -> McpToolChat:
|
|
|
91
170
|
else:
|
|
92
171
|
system = default_system_prompt
|
|
93
172
|
|
|
94
|
-
return McpToolChat(
|
|
173
|
+
return McpToolChat(
|
|
174
|
+
mcp_client,
|
|
175
|
+
provider,
|
|
176
|
+
system,
|
|
177
|
+
tool_cache=tool_cache,
|
|
178
|
+
server_names=set(config.servers.keys()),
|
|
179
|
+
)
|