dmt-agent-api 0.3.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.
dumate/resources.py ADDED
@@ -0,0 +1,206 @@
1
+ """Legacy API resources. Responses retain the original JSON field names."""
2
+ from ._common import segment, fields
3
+ from .errors import PaginationError, ProtocolError
4
+
5
+ class Resource:
6
+ def __init__(self, client, path):
7
+ self._client = client
8
+ self._path = path
9
+
10
+ def iter(self, **params):
11
+ """Iterate cursor pages; raise rather than silently return partial data."""
12
+ cursor = params.pop("cursor", None)
13
+ seen = set()
14
+ while True:
15
+ page = self.list(cursor=cursor, **params)
16
+ if not isinstance(page, dict) or not isinstance(page.get("data"), list):
17
+ raise PaginationError("Invalid list response")
18
+ yield from page["data"]
19
+ if page.get("hasMore") is False:
20
+ return
21
+ if page.get("hasMore") is not True:
22
+ raise PaginationError("Missing explicit hasMore")
23
+ nxt = page.get("nextCursor")
24
+ if not isinstance(nxt, (str, int)) or isinstance(nxt, bool) or not str(nxt) or str(nxt) == str(cursor) or str(nxt) in seen:
25
+ raise PaginationError("Cursor did not advance")
26
+ seen.add(str(nxt))
27
+ cursor = str(nxt)
28
+
29
+ class Agents(Resource):
30
+ def list(self, *, keyword=None, cursor=None, limit=None):
31
+ return self._client._request("GET", self._path, params=fields(keyword=keyword, cursor=cursor, limit=limit))
32
+
33
+ def create(self, *, name, description=None, system=None, skills=None, files=None, mcp_servers=None, tools=None):
34
+ return self._client._request("POST", self._path, json=fields(name=name, description=description, system=system, skills=skills, files=files, mcpServers=mcp_servers, tools=tools))
35
+
36
+ def get(self, agent_id, *, version=None):
37
+ return self._client._request("GET", f"{self._path}/{segment(agent_id)}", params=fields(version=version))
38
+
39
+ def update(self, agent_id, *, version, system=None, skills=None, files=None, mcp_servers=None, tools=None):
40
+ return self._client._request("POST", f"{self._path}/{segment(agent_id)}", json=fields(version=version, system=system, skills=skills, files=files, mcpServers=mcp_servers, tools=tools))
41
+
42
+ def delete(self, agent_id):
43
+ """Destructive: the platform also deletes associated sessions."""
44
+ return self._client._request("DELETE", f"{self._path}/{segment(agent_id)}")
45
+
46
+ class Skills(Resource):
47
+ def list(self, *, page=1, page_size=20, skill_id=None, keyword=None, order=None):
48
+ return self._client._request("GET", self._path, params=fields(page=page, pageSize=page_size, skillId=skill_id, keyword=keyword, order=order))
49
+
50
+ def iter(self, *, page_size=20, **params):
51
+ page_number, count, total = 1, 0, None
52
+ seen = set()
53
+ while True:
54
+ page = self.list(page=page_number, page_size=page_size, **params)
55
+ if not isinstance(page, dict) or not isinstance(page.get("data"), list) or not isinstance(page.get("total"), int):
56
+ raise PaginationError("Invalid Skill page")
57
+ if total is not None and page["total"] != total:
58
+ raise PaginationError("Skill total changed while paginating")
59
+ total = page["total"]
60
+ for row in page["data"]:
61
+ identity = (row.get("id"), row.get("releaseId"))
62
+ if not identity[0] or identity in seen:
63
+ raise PaginationError("Repeated or unidentified Skill")
64
+ seen.add(identity)
65
+ count += 1
66
+ yield row
67
+ if count == total:
68
+ return
69
+ if count > total or not page["data"]:
70
+ raise PaginationError("Skill page count inconsistent with total")
71
+ page_number += 1
72
+
73
+ def ensure(self, *, name, file):
74
+ """Reuse an exact name; upload only when absent. Does not update existing content."""
75
+ matches = [row for row in self.iter(keyword=name) if row.get("name") == name]
76
+ if len(matches) > 1:
77
+ raise ProtocolError("Multiple matching Skills; select an explicit ID")
78
+ return matches[0] if matches else self.upload(file)
79
+
80
+ def upload(self, file):
81
+ return self._client._upload("POST", f"{self._path}/upload", file)
82
+
83
+ def create(self, *, url):
84
+ return self._client._request("POST", self._path, json={"url": url})
85
+
86
+ def get(self, skill_id, *, release_id=None):
87
+ return self._client._request("GET", f"{self._path}/{segment(skill_id)}", params=fields(releaseId=release_id))
88
+
89
+ def update(self, skill_id, *, url):
90
+ return self._client._request("POST", f"{self._path}/{segment(skill_id)}", json={"url": url})
91
+
92
+ def update_zip(self, skill_id, file):
93
+ return self._client._upload("POST", f"{self._path}/{segment(skill_id)}/update", file)
94
+
95
+ def delete(self, skill_id):
96
+ return self._client._request("DELETE", f"{self._path}/{segment(skill_id)}")
97
+
98
+ def mount(self, skill_id, *, release_id=None, type="custom"):
99
+ """Read releaseId, never confuse it with the display version."""
100
+ skill = self.get(skill_id, release_id=release_id)
101
+ if not skill.get("releaseId"):
102
+ raise ProtocolError("Skill releaseId is not available yet")
103
+ if type not in ("custom", "official"):
104
+ raise ValueError("type must be custom or official")
105
+ return {"skillID": skill_id, "releaseID": skill["releaseId"], "type": type}
106
+
107
+ class Vaults(Resource):
108
+ def list(self, *, cursor=None, limit=None):
109
+ return self._client._request("GET", self._path, params=fields(cursor=cursor, limit=limit))
110
+
111
+ def create(self, *, name):
112
+ return self._client._request("POST", self._path, json={"name": name})
113
+
114
+ def get(self, vault_id):
115
+ return self._client._request("GET", f"{self._path}/{segment(vault_id)}")
116
+
117
+ def delete(self, vault_id):
118
+ return self._client._request("DELETE", f"{self._path}/{segment(vault_id)}")
119
+
120
+ def credentials(self, vault_id):
121
+ return Credentials(self._client, f"{self._path}/{segment(vault_id)}/credentials")
122
+
123
+ class Credentials(Resource):
124
+ def list(self, *, cursor=None, limit=None, auth_type=None):
125
+ return self._client._request("GET", self._path, params=fields(cursor=cursor, limit=limit, authType=auth_type))
126
+
127
+ def create(self, *, name, auth):
128
+ return self._client._request("POST", self._path, json={"name": name, "auth": auth})
129
+
130
+ def get(self, credential_id):
131
+ return self._client._request("GET", f"{self._path}/{segment(credential_id)}")
132
+
133
+ def delete(self, credential_id):
134
+ return self._client._request("DELETE", f"{self._path}/{segment(credential_id)}")
135
+
136
+ class Sessions(Resource):
137
+ def list(self, *, cursor=None, limit=None, status=None):
138
+ return self._client._request("GET", self._path, params=fields(cursor=cursor, limit=limit, status=status))
139
+
140
+ def create(self, *, agent_id, metadata=None, vault_ids=None, skills=None, mcp_servers=None):
141
+ return self._client._request("POST", self._path, json=fields(agent={"id": agent_id}, metadata=metadata, vaultIds=vault_ids, skills=skills, mcpServers=mcp_servers))
142
+
143
+ def get(self, session_id):
144
+ return self._client._request("GET", f"{self._path}/{segment(session_id)}")
145
+
146
+ def update(self, session_id, *, metadata=None, vault_ids=None, skills=None, mcp_servers=None):
147
+ return self._client._request("POST", f"{self._path}/{segment(session_id)}", json=fields(metadata=metadata, vaultIds=vault_ids, skills=skills, mcpServers=mcp_servers))
148
+
149
+ def delete(self, session_id):
150
+ return self._client._request("DELETE", f"{self._path}/{segment(session_id)}")
151
+
152
+ def history(self, session_id, *, limit=None, paginated=False):
153
+ return self._client._request("GET", f"{self._path}/{segment(session_id)}/events", params=fields(limit=limit, paginated=str(paginated).lower()))
154
+
155
+ def send(self, session_id, *, message, file_ids=None):
156
+ content = [{"type": "text", "text": message}]
157
+ content.extend({"type": "file", "file_id": fid} for fid in (file_ids or []))
158
+ return self._client._send(session_id, {"type": "user.message", "content": content})
159
+
160
+ def send_event(self, session_id, event):
161
+ if not isinstance(event, dict) or not isinstance(event.get("type"), str):
162
+ raise ValueError("event must be a single typed object")
163
+ return self._client._request("POST", f"{self._path}/{segment(session_id)}/events", json={"events": [event]})
164
+
165
+ def interrupt(self, session_id):
166
+ return self.send_event(session_id, {"type": "user.interrupt"})
167
+
168
+ def submit_tool_result(self, session_id, *, custom_tool_use_id, content, is_error=False):
169
+ return self.send_event(session_id, {"type": "user.custom_tool_result", "content": [{"customToolUseId": custom_tool_use_id, "content": content, "isError": is_error}]})
170
+
171
+ def resource_points(self, session_id, *, start_at=None, end_at=None):
172
+ if start_at is not None and start_at < 0 or end_at is not None and end_at < 0:
173
+ raise ValueError("timestamps must be non-negative")
174
+ if start_at is not None and end_at is not None and start_at >= end_at:
175
+ raise ValueError("start_at must be less than end_at")
176
+ return self._client._request("GET", f"{self._path}/{segment(session_id)}/resource-points", params=fields(startAt=start_at, endAt=end_at))
177
+
178
+ def stream(self, session_id, *, message=None, file_ids=None):
179
+ """Context manager; connect before optional send, never automatically resend."""
180
+ return self._client._stream(session_id, message=message, file_ids=file_ids)
181
+
182
+ class Files(Resource):
183
+ def list(self, *, cursor=None, limit=None, order=None, file_ids=None):
184
+ return self._client._request("GET", self._path, params=fields(cursor=cursor, limit=limit, order=order, fileIds=file_ids))
185
+
186
+ def upload(self, file, *, session_id=None, mime_type=None):
187
+ return self._client._upload("POST", self._path, file, data=fields(sessionId=session_id, mimeType=mime_type))
188
+
189
+ def update(self, file_id, file):
190
+ return self._client._upload("PUT", f"{self._path}/{segment(file_id)}", file)
191
+
192
+ def get(self, file_id):
193
+ return self._client._request("GET", f"{self._path}/{segment(file_id)}")
194
+
195
+ def delete(self, file_id):
196
+ return self._client._request("DELETE", f"{self._path}/{segment(file_id)}")
197
+
198
+ def download(self, file_id, destination, *, max_bytes=100 * 1024 * 1024, overwrite=False):
199
+ return self._client._download_api(f"{self._path}/{segment(file_id)}/content", destination, max_bytes=max_bytes, overwrite=overwrite)
200
+
201
+ class Artifacts(Resource):
202
+ def path_map(self, session_id, *, paths=None):
203
+ return self._client._request("GET", f"sessions/{segment(session_id)}/artifact/path-map", params=fields(path=paths))
204
+
205
+ def download(self, session_id, artifact_id, destination, *, max_bytes=100 * 1024 * 1024, overwrite=False):
206
+ return self._client._download_artifact(session_id, artifact_id, destination, max_bytes=max_bytes, overwrite=overwrite)
dumate/runs.py ADDED
@@ -0,0 +1,61 @@
1
+ """Client-side convenience calls over sessions/events, not a server /runs API."""
2
+ import time
3
+ from ._common import history_messages
4
+ from .errors import ProtocolError, RunTimeoutError, RunFailedError
5
+ from .types import RunResult
6
+
7
+
8
+ def correlated_result(session_id, history, baseline_ids, message, request_id):
9
+ rows = history_messages(history)
10
+ users = []
11
+ for row in rows:
12
+ info = row["info"]
13
+ if info.get("sessionID") != session_id or info.get("role") != "user" or info.get("id") in baseline_ids:
14
+ continue
15
+ text = "\n".join(p.get("text", "") for p in row["parts"] if p.get("type") == "text")
16
+ if text == message or (request_id and info.get("requestID") == request_id):
17
+ users.append(info["id"])
18
+ if len(users) > 1:
19
+ raise ProtocolError("Multiple matching user messages; serialize sends within a session")
20
+ if not users:
21
+ return None
22
+ user_id = users[0]
23
+ assistants = [r for r in rows if r["info"].get("sessionID") == session_id
24
+ and r["info"].get("role") == "assistant" and r["info"].get("parentID") == user_id]
25
+ for row in reversed(assistants):
26
+ info = row["info"]
27
+ if info.get("error"):
28
+ raise RunFailedError(session_id)
29
+ if info.get("finish") != "stop" or not info.get("time", {}).get("completed"):
30
+ continue
31
+ texts = [p for p in row["parts"] if p.get("type") == "text" and p.get("phase") in (None, "final_answer")]
32
+ final = [p for p in texts if p.get("phase") == "final_answer"]
33
+ output = "\n".join(p.get("text", "") for p in (final or texts)).strip()
34
+ if output:
35
+ return RunResult(session_id, user_id, request_id, output, assistants)
36
+ return None
37
+
38
+ class Runs:
39
+ def __init__(self, client):
40
+ self._client = client
41
+
42
+ def run_and_wait(self, *, session_id, message, file_ids=None, timeout=None, poll_interval=2.0):
43
+ """Poll authoritative history. No event replay, persistence or automatic resubmit.
44
+
45
+ One caller at a time per session. Timeout stops waiting, not remote execution.
46
+ """
47
+ if poll_interval <= 0 or (timeout is not None and timeout <= 0):
48
+ raise ValueError("poll_interval and timeout must be positive")
49
+ baseline = history_messages(self._client.sessions.history(session_id))
50
+ baseline_ids = {x["info"].get("id") for x in baseline}
51
+ receipt = self._client.sessions.send(session_id, message=message, file_ids=file_ids)
52
+ deadline = None if timeout is None else time.monotonic() + timeout
53
+ while True:
54
+ rows = self._client.sessions.history(session_id)
55
+ result = correlated_result(session_id, rows, baseline_ids, message, receipt.request_id)
56
+ if result is not None:
57
+ return result
58
+ if deadline is not None and time.monotonic() >= deadline:
59
+ raise RunTimeoutError(session_id)
60
+ wait = poll_interval if deadline is None else min(poll_interval, max(0, deadline-time.monotonic()))
61
+ time.sleep(wait)
dumate/types.py ADDED
@@ -0,0 +1,51 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, TypedDict, Literal
3
+
4
+ class SkillMount(TypedDict):
5
+ skillID: str
6
+ releaseID: str
7
+ type: Literal["custom", "official"]
8
+
9
+ class MCPServer(TypedDict):
10
+ type: Literal["url"]
11
+ name: str
12
+ url: str
13
+
14
+ class FileMount(TypedDict):
15
+ fileID: str
16
+
17
+ @dataclass(frozen=True)
18
+ class Event:
19
+ type: str
20
+ properties: dict[str, Any] = field(repr=False)
21
+ raw: dict[str, Any] = field(repr=False)
22
+ id: str | None = None
23
+ event: str | None = None
24
+
25
+ @property
26
+ def session_id(self) -> str | None:
27
+ p = self.properties
28
+ if isinstance(p.get("sessionID"), str):
29
+ return p["sessionID"]
30
+ for key in ("part", "info"):
31
+ obj = p.get(key)
32
+ if isinstance(obj, dict) and isinstance(obj.get("sessionID"), str):
33
+ return obj["sessionID"]
34
+ if self.type == "session.updated" and isinstance(p.get("info"), dict):
35
+ return p["info"].get("id")
36
+ return None
37
+
38
+ @dataclass(frozen=True)
39
+ class SendReceipt:
40
+ session_id: str
41
+ request_id: str | None
42
+ accepted: bool = True
43
+
44
+ @dataclass(frozen=True)
45
+ class RunResult:
46
+ session_id: str
47
+ user_message_id: str
48
+ request_id: str | None
49
+ output_text: str = field(repr=False)
50
+ messages: list[dict[str, Any]] = field(repr=False)
51
+ status: str = "completed"