dot-mcp 0.1.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.
- dot_mcp/__init__.py +3 -0
- dot_mcp/agent_test.py +43 -0
- dot_mcp/cli.py +244 -0
- dot_mcp/local_agent.py +107 -0
- dot_mcp/orchestrator.py +883 -0
- dot_mcp/server.py +1054 -0
- dot_mcp/ui/workspace.html +84 -0
- dot_mcp-0.1.0.dist-info/METADATA +196 -0
- dot_mcp-0.1.0.dist-info/RECORD +12 -0
- dot_mcp-0.1.0.dist-info/WHEEL +4 -0
- dot_mcp-0.1.0.dist-info/entry_points.txt +3 -0
- dot_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
dot_mcp/__init__.py
ADDED
dot_mcp/agent_test.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Quick client test: list tools + call list_dir, get_state, remember/recall."""
|
|
3
|
+
import asyncio, json, os, sys
|
|
4
|
+
from mcp.client.streamable_http import streamable_http_client
|
|
5
|
+
from mcp.client.session import ClientSession
|
|
6
|
+
|
|
7
|
+
URL = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:33041/mcp/"
|
|
8
|
+
|
|
9
|
+
def _secret() -> str:
|
|
10
|
+
if os.environ.get("MCP_PATH_SECRET", "").strip():
|
|
11
|
+
return os.environ["MCP_PATH_SECRET"].strip()
|
|
12
|
+
base = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")))
|
|
13
|
+
for name in ("owner_secret", "path_secret"): # latter is legacy
|
|
14
|
+
try:
|
|
15
|
+
return open(os.path.join(base, "dot-mcp", name), encoding="utf-8").read().strip()
|
|
16
|
+
except Exception:
|
|
17
|
+
pass
|
|
18
|
+
return ""
|
|
19
|
+
|
|
20
|
+
async def main():
|
|
21
|
+
try:
|
|
22
|
+
import httpx
|
|
23
|
+
except ImportError:
|
|
24
|
+
import httpx2 as httpx # .venv/LiteLLM-era rename
|
|
25
|
+
sec = _secret()
|
|
26
|
+
client = httpx.AsyncClient(headers={"Authorization": f"Bearer {sec}"}) if sec else None
|
|
27
|
+
async with streamable_http_client(URL, http_client=client) as streams:
|
|
28
|
+
rs, ws = streams[0], streams[1]
|
|
29
|
+
async with ClientSession(rs, ws) as s:
|
|
30
|
+
await s.initialize()
|
|
31
|
+
tools = await s.list_tools()
|
|
32
|
+
names = [t.name for t in tools.tools]
|
|
33
|
+
print("TOOLS:", names)
|
|
34
|
+
r = await s.call_tool("get_state", {})
|
|
35
|
+
print("STATE:", r.content[0].text[:500] if r.content else r)
|
|
36
|
+
r = await s.call_tool("list_dir", {"path": "~", "limit": 5})
|
|
37
|
+
print("LS:", r.content[0].text[:800] if r.content else r)
|
|
38
|
+
r = await s.call_tool("remember", {"note": "agent test ok", "tag": "test"})
|
|
39
|
+
print("REMEMBER:", r.content[0].text[:300] if r.content else r)
|
|
40
|
+
r = await s.call_tool("recall", {"query": "agent test", "limit": 3})
|
|
41
|
+
print("RECALL:", r.content[0].text[:800] if r.content else r)
|
|
42
|
+
|
|
43
|
+
asyncio.run(main())
|
dot_mcp/cli.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""dot-mcp CLI: `dot-mcp start`, `dot-mcp status`, `dot-mcp stop`.
|
|
3
|
+
|
|
4
|
+
Works both from a source checkout (src/ layout) and as an installed package
|
|
5
|
+
(`pip install dot-mcp`). Defaults: port 33041, data in ~/.dot-mcp.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import argparse, json, os, secrets, subprocess, sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from dot_mcp import orchestrator as ui
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _repo_root() -> Path:
|
|
15
|
+
"""Source checkout root if present (for .venv + logs), else the data dir."""
|
|
16
|
+
here = Path(__file__).resolve() # <repo>/src/dot_mcp/cli.py
|
|
17
|
+
repo = here.parent.parent.parent # <repo> in a source checkout
|
|
18
|
+
if (repo / "pyproject.toml").exists() or (repo / "dot_mcp.py").exists() or (repo / "src").is_dir():
|
|
19
|
+
return repo
|
|
20
|
+
data = Path(os.environ.get("DOT_MCP_DATA", str(Path.home() / ".dot-mcp")))
|
|
21
|
+
data.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
return data
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
REPO = _repo_root()
|
|
26
|
+
STATE_DIR = Path(os.environ.get("DOT_MCP_STATE_DIR",
|
|
27
|
+
os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))) ) / "dot-mcp"
|
|
28
|
+
SECRET_FILE = STATE_DIR / "owner_secret"
|
|
29
|
+
DEFAULT_PORT = 33041
|
|
30
|
+
DEFAULT_DATA = str(Path.home() / ".dot-mcp")
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from rich.console import Console
|
|
34
|
+
from rich.panel import Panel
|
|
35
|
+
from rich.table import Table
|
|
36
|
+
from rich import box
|
|
37
|
+
console = Console()
|
|
38
|
+
HAS_RICH = True
|
|
39
|
+
except Exception: # plain fallback
|
|
40
|
+
HAS_RICH = False
|
|
41
|
+
class _C:
|
|
42
|
+
def print(self, *a, **k):
|
|
43
|
+
print(*[str(x).replace("[", "").replace("]", "") for x in a])
|
|
44
|
+
console = _C() # type: ignore
|
|
45
|
+
Panel = Table = None # type: ignore
|
|
46
|
+
|
|
47
|
+
ACCENT = "cyan"
|
|
48
|
+
WARN = "yellow"
|
|
49
|
+
ERR = "red"
|
|
50
|
+
OK = "green"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def banner(sub: str = "") -> None:
|
|
54
|
+
title = "[bold cyan]dot-mcp[/] [dim]personal MCP agent[/]"
|
|
55
|
+
if sub:
|
|
56
|
+
title += f" [bold white]{sub}[/]"
|
|
57
|
+
if HAS_RICH:
|
|
58
|
+
console.print(Panel(title, box=box.ROUNDED, border_style=ACCENT))
|
|
59
|
+
else:
|
|
60
|
+
console.print(f"== dot-mcp {sub} ==")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_secret() -> str:
|
|
64
|
+
"""Per-machine owner secret: read from disk, else generate + save (0600)."""
|
|
65
|
+
try:
|
|
66
|
+
s = SECRET_FILE.read_text(encoding="utf-8").strip()
|
|
67
|
+
if s:
|
|
68
|
+
return s
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
s = secrets.token_urlsafe(32)
|
|
72
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
os.chmod(STATE_DIR, 0o700)
|
|
74
|
+
tmp = SECRET_FILE.with_suffix(".tmp")
|
|
75
|
+
tmp.write_text(s + "\n", encoding="utf-8")
|
|
76
|
+
os.replace(tmp, SECRET_FILE)
|
|
77
|
+
os.chmod(SECRET_FILE, 0o600)
|
|
78
|
+
return s
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def server_python() -> tuple[str, str | None]:
|
|
82
|
+
"""(python, src_dir). Source checkout: .venv if present + src on path.
|
|
83
|
+
Installed package: this interpreter (dot_mcp is importable)."""
|
|
84
|
+
src = REPO / "src"
|
|
85
|
+
if (src / "dot_mcp" / "server.py").exists():
|
|
86
|
+
venv = REPO / ".venv" / "bin" / "python"
|
|
87
|
+
py = str(venv) if (venv.is_file() and os.access(venv, os.X_OK)) else sys.executable
|
|
88
|
+
return py, str(src)
|
|
89
|
+
return sys.executable, None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _start_oauth_client(data_dir: str) -> dict:
|
|
93
|
+
"""Read the pre-provisioned OAuth client the server writes at boot."""
|
|
94
|
+
try:
|
|
95
|
+
st = json.loads((Path(data_dir) / "oauth.json").read_text(encoding="utf-8"))
|
|
96
|
+
sc = st.get("static_client")
|
|
97
|
+
if isinstance(sc, dict) and sc.get("client_id"):
|
|
98
|
+
return sc
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
return {}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cmd_start(args: argparse.Namespace) -> int:
|
|
105
|
+
port = args.port or DEFAULT_PORT
|
|
106
|
+
data_dir = args.data_dir or DEFAULT_DATA
|
|
107
|
+
# Owner secret: stable per machine (env wins, else file, else generate+save).
|
|
108
|
+
secret = (os.environ.get("MCP_PATH_SECRET", "").strip() or get_secret())
|
|
109
|
+
py, src_dir = server_python()
|
|
110
|
+
env = dict(os.environ, MCP_PORT=str(port), MCP_HOST="127.0.0.1",
|
|
111
|
+
DOT_MCP_DATA=data_dir, MCP_PATH_SECRET=secret)
|
|
112
|
+
if src_dir: # source checkout: make `dot_mcp` importable for -m
|
|
113
|
+
env["PYTHONPATH"] = src_dir + os.pathsep + env.get("PYTHONPATH", "")
|
|
114
|
+
try:
|
|
115
|
+
v = subprocess.run([py, "-c", "import mcp; print('mcp-ok')"],
|
|
116
|
+
capture_output=True, text=True, timeout=30, env=env).stdout.strip()
|
|
117
|
+
assert "mcp-ok" in v
|
|
118
|
+
except Exception:
|
|
119
|
+
print(f"no mcp package in {py} — run ./setup.sh first", file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
ctx = ui.StartCtx(port=port, data_dir=data_dir, secret=secret, py=py,
|
|
122
|
+
env=env, repo=REPO)
|
|
123
|
+
|
|
124
|
+
if args.json:
|
|
125
|
+
# script mode: full flow, JSON ONLY to stdout, server left running
|
|
126
|
+
import contextlib
|
|
127
|
+
import io
|
|
128
|
+
buf = io.StringIO()
|
|
129
|
+
with contextlib.redirect_stdout(buf):
|
|
130
|
+
res = ui.plain_flow(ctx, STATE_DIR, console, leave_running=True)
|
|
131
|
+
oauth = _start_oauth_client(data_dir)
|
|
132
|
+
sys.stdout.write(ui.json_result(res.ok, ctx, res, oauth) + "\n")
|
|
133
|
+
return 0 if res.ok else 1
|
|
134
|
+
|
|
135
|
+
banner("start")
|
|
136
|
+
if ui.port_in_use(port):
|
|
137
|
+
console.print(f"[{ERR}]port {port} is already in use — run dot-mcp status or dot-mcp stop first[/]")
|
|
138
|
+
return 1
|
|
139
|
+
|
|
140
|
+
# Run the full flow quietly (no spinner, no live output) — then show results once.
|
|
141
|
+
# leave_running=True: the verified server stays up as a detached background
|
|
142
|
+
# process, so we can return to the shell instead of blocking it.
|
|
143
|
+
import contextlib, io
|
|
144
|
+
buf = io.StringIO()
|
|
145
|
+
with contextlib.redirect_stdout(buf):
|
|
146
|
+
res = ui.plain_flow(ctx, STATE_DIR, console, leave_running=True)
|
|
147
|
+
if not res.ok:
|
|
148
|
+
return 1
|
|
149
|
+
|
|
150
|
+
# Show all info in one shot
|
|
151
|
+
host = res.host
|
|
152
|
+
public_mcp = res.public_url
|
|
153
|
+
oauth = _start_oauth_client(data_dir)
|
|
154
|
+
t = Table("", "", box=box.SIMPLE, show_header=False)
|
|
155
|
+
t.add_row("Public URL", public_mcp)
|
|
156
|
+
t.add_row("Owner secret", secret)
|
|
157
|
+
t.add_row("OAuth Client ID", oauth.get("client_id", "—"))
|
|
158
|
+
t.add_row("OAuth Client secret", oauth.get("client_secret", "—"))
|
|
159
|
+
console.print(t)
|
|
160
|
+
console.print(Panel(
|
|
161
|
+
f"[bold green]Running.[/] Paste the public URL into your connector (auth = OAuth).",
|
|
162
|
+
title="ready", border_style=OK))
|
|
163
|
+
console.print("[dim]Server running in the background — dots status / dots stop[/]\n")
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
168
|
+
port = args.port or DEFAULT_PORT
|
|
169
|
+
try:
|
|
170
|
+
import urllib.request
|
|
171
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=5) as r:
|
|
172
|
+
info = json.loads(r.read().decode())
|
|
173
|
+
console.print(f"[{OK}]● running[/] port {port} · tools {info.get('tools')} · home {info.get('home')}")
|
|
174
|
+
return 0
|
|
175
|
+
except Exception:
|
|
176
|
+
console.print(f"[{ERR}]○ stopped[/] port {port} (no server)")
|
|
177
|
+
return 1
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def cmd_stop(_: argparse.Namespace) -> int:
|
|
181
|
+
import signal as _sig
|
|
182
|
+
killed = 0
|
|
183
|
+
me = os.getpid()
|
|
184
|
+
try:
|
|
185
|
+
# matches both `dot_mcp.py` (legacy) and `-m dot_mcp.server` (packaged)
|
|
186
|
+
out = subprocess.run(["pgrep", "-f", "dot_mcp[.]"],
|
|
187
|
+
capture_output=True, text=True).stdout
|
|
188
|
+
for pid in out.split():
|
|
189
|
+
try:
|
|
190
|
+
pid_i = int(pid)
|
|
191
|
+
except ValueError:
|
|
192
|
+
continue
|
|
193
|
+
if pid_i == me:
|
|
194
|
+
continue
|
|
195
|
+
try:
|
|
196
|
+
with open(f"/proc/{pid_i}/cmdline", "rb") as fh:
|
|
197
|
+
cmd = fh.read().replace(b"\x00", b" ").decode(errors="replace")
|
|
198
|
+
if "dot_mcp.cli" in cmd: # never kill our own CLI
|
|
199
|
+
continue
|
|
200
|
+
except Exception:
|
|
201
|
+
continue
|
|
202
|
+
try:
|
|
203
|
+
os.kill(pid_i, _sig.SIGTERM)
|
|
204
|
+
killed += 1
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
pidfile = REPO / "logs" / "tunnl.pid"
|
|
210
|
+
try:
|
|
211
|
+
if pidfile.is_file():
|
|
212
|
+
os.kill(int(pidfile.read_text().strip()), _sig.SIGTERM)
|
|
213
|
+
pidfile.unlink()
|
|
214
|
+
console.print("Tunnel stopped.")
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
console.print(f"Stopped {killed} server(s). " if killed else "No server running.")
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def main(argv: list[str] | None = None) -> int:
|
|
222
|
+
ap = argparse.ArgumentParser(prog="dot-mcp", description="dot-mcp personal agent CLI")
|
|
223
|
+
sub = ap.add_subparsers(dest="cmd")
|
|
224
|
+
p = sub.add_parser("start", help="run MCP server + tunnel (runs in background)")
|
|
225
|
+
p.add_argument("--port", type=int, default=0, help="override port (default 33041)")
|
|
226
|
+
p.add_argument("--data-dir", default="", help="override data dir (default ~/.dot-mcp)")
|
|
227
|
+
p.add_argument("--json", action="store_true", help="script mode: JSON result, server left running")
|
|
228
|
+
q = sub.add_parser("status", help="check if the server is running")
|
|
229
|
+
q.add_argument("--port", type=int, default=0, help="override port (default 33041)")
|
|
230
|
+
sub.add_parser("stop", help="stop server + tunnel")
|
|
231
|
+
args = ap.parse_args(argv)
|
|
232
|
+
if args.cmd == "start":
|
|
233
|
+
return cmd_start(args)
|
|
234
|
+
if args.cmd == "status":
|
|
235
|
+
return cmd_status(args)
|
|
236
|
+
if args.cmd == "stop":
|
|
237
|
+
return cmd_stop(args)
|
|
238
|
+
banner()
|
|
239
|
+
console.print("Commands: [bold]start[/] · [bold]status[/] · [bold]stop[/]\nExample: [cyan]dot-mcp start[/]")
|
|
240
|
+
return 0
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
raise SystemExit(main())
|
dot_mcp/local_agent.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Local-only dot agent: ChatGPT model (API key) + MCP server on this machine.
|
|
3
|
+
No tunnel, no sub needed. Server: http://127.0.0.1:33041/mcp
|
|
4
|
+
|
|
5
|
+
export OPENAI_API_KEY=sk-...
|
|
6
|
+
.venv/bin/python local_agent.py # chat loop
|
|
7
|
+
.venv/bin/python local_agent.py --list # list MCP tools only (no key needed)
|
|
8
|
+
OPENAI_MODEL=gpt-4o-mini .venv/bin/python local_agent.py
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import asyncio, json, os, sys
|
|
12
|
+
|
|
13
|
+
MCP_URL = os.environ.get("MCP_URL", "http://127.0.0.1:33041/mcp/")
|
|
14
|
+
MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
|
|
15
|
+
|
|
16
|
+
def _owner_secret() -> str:
|
|
17
|
+
if os.environ.get("MCP_PATH_SECRET", "").strip():
|
|
18
|
+
return os.environ["MCP_PATH_SECRET"].strip()
|
|
19
|
+
base = os.environ.get("DOT_MCP_STATE_DIR", "") or os.path.join(
|
|
20
|
+
os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")))
|
|
21
|
+
for name in ("owner_secret", "path_secret"): # latter is legacy
|
|
22
|
+
try:
|
|
23
|
+
s = open(os.path.join(base, "dot-mcp", name), encoding="utf-8").read().strip()
|
|
24
|
+
if s:
|
|
25
|
+
return s
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
return ""
|
|
29
|
+
|
|
30
|
+
def _http_client():
|
|
31
|
+
try:
|
|
32
|
+
import httpx
|
|
33
|
+
except ImportError:
|
|
34
|
+
import httpx2 as httpx # .venv/LiteLLM-era rename
|
|
35
|
+
sec = _owner_secret()
|
|
36
|
+
if not sec: return None
|
|
37
|
+
# NOTE: no custom User-Agent — httpx2 in .venv duplicates it and the v2 server rejects the request.
|
|
38
|
+
return httpx.AsyncClient(headers={"Authorization": f"Bearer {sec}"})
|
|
39
|
+
SYSTEM = ("You are the user's dot on their own machine. "
|
|
40
|
+
"At start call get_state, poll_inbox, recall. "
|
|
41
|
+
"Save important facts with remember, progress with add_update, "
|
|
42
|
+
"visible results with notify_user. "
|
|
43
|
+
"Relative paths resolve from $HOME.")
|
|
44
|
+
|
|
45
|
+
async def mcp_tools():
|
|
46
|
+
from mcp.client.streamable_http import streamable_http_client
|
|
47
|
+
from mcp.client.session import ClientSession
|
|
48
|
+
async with streamable_http_client(MCP_URL, http_client=_http_client()) as streams:
|
|
49
|
+
rs, ws = streams[0], streams[1]
|
|
50
|
+
async with ClientSession(rs, ws) as s:
|
|
51
|
+
await s.initialize()
|
|
52
|
+
t = await s.list_tools()
|
|
53
|
+
return [{"name": x.name, "desc": x.description or "",
|
|
54
|
+
"schema": getattr(x, "input_schema", None) or getattr(x, "inputSchema", None)} for x in t.tools]
|
|
55
|
+
|
|
56
|
+
async def main():
|
|
57
|
+
if "--list" in sys.argv:
|
|
58
|
+
for t in await mcp_tools():
|
|
59
|
+
print(f"- {t['name']}: {t['desc'][:80]}")
|
|
60
|
+
return
|
|
61
|
+
key = os.environ.get("OPENAI_API_KEY", "")
|
|
62
|
+
if not key:
|
|
63
|
+
print("need OPENAI_API_KEY"); raise SystemExit(1)
|
|
64
|
+
from openai import OpenAI
|
|
65
|
+
client = OpenAI(base_url=os.environ.get("OPENAI_BASE_URL") or None)
|
|
66
|
+
defs = await mcp_tools()
|
|
67
|
+
tools = [{"type": "function", "function": {
|
|
68
|
+
"name": d["name"], "description": d["desc"][:500],
|
|
69
|
+
"parameters": d["schema"] or {"type": "object", "properties": {}}}}
|
|
70
|
+
for d in defs]
|
|
71
|
+
from mcp.client.streamable_http import streamable_http_client
|
|
72
|
+
from mcp.client.session import ClientSession
|
|
73
|
+
async with streamable_http_client(MCP_URL, http_client=_http_client()) as streams:
|
|
74
|
+
rs, ws = streams[0], streams[1]
|
|
75
|
+
async with ClientSession(rs, ws) as s:
|
|
76
|
+
await s.initialize()
|
|
77
|
+
msgs = [{"role": "system", "content": SYSTEM}]
|
|
78
|
+
print(f"dot local ready [{MODEL}] ({len(tools)} tools). exit to quit.")
|
|
79
|
+
while True:
|
|
80
|
+
try: q = input("\n> ").strip()
|
|
81
|
+
except (EOFError, KeyboardInterrupt): break
|
|
82
|
+
if q in ("exit", "quit"): break
|
|
83
|
+
if not q: continue
|
|
84
|
+
msgs.append({"role": "user", "content": q})
|
|
85
|
+
for _ in range(10):
|
|
86
|
+
r = client.chat.completions.create(model=MODEL, messages=msgs, tools=tools or None)
|
|
87
|
+
m = r.choices[0].message
|
|
88
|
+
if not m.tool_calls:
|
|
89
|
+
print(m.content); msgs.append({"role": "assistant", "content": m.content or ""}); break
|
|
90
|
+
msgs.append({"role": "assistant", "content": m.content or "", "tool_calls": [
|
|
91
|
+
{"id": c.id, "type": "function",
|
|
92
|
+
"function": {"name": c.function.name, "arguments": c.function.arguments}}
|
|
93
|
+
for c in m.tool_calls]})
|
|
94
|
+
for c in m.tool_calls:
|
|
95
|
+
name = c.function.name
|
|
96
|
+
try: args = json.loads(c.function.arguments or "{}")
|
|
97
|
+
except Exception: args = {}
|
|
98
|
+
try:
|
|
99
|
+
res = await s.call_tool(name, args)
|
|
100
|
+
out = "\n".join(
|
|
101
|
+
getattr(b, "text", str(b)) for b in (res.content or []))[:6000]
|
|
102
|
+
except Exception as e:
|
|
103
|
+
out = f"ERROR: {e}"
|
|
104
|
+
print(f"[{name}] {out[:500]}")
|
|
105
|
+
msgs.append({"role": "tool", "tool_call_id": c.id, "content": out})
|
|
106
|
+
|
|
107
|
+
asyncio.run(main())
|