xah 0.0.1__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.
Files changed (120) hide show
  1. xah/__init__.py +4 -0
  2. xah/caps/__init__.py +0 -0
  3. xah/caps/retry.py +99 -0
  4. xah/caps/traffic.py +398 -0
  5. xah/class_collections.py +12 -0
  6. xah/constants.py +10 -0
  7. xah/entrypoint.py +15 -0
  8. xah/examples/__init__.py +0 -0
  9. xah/examples/m01/__init__.py +0 -0
  10. xah/examples/m01/e01_hello.py +10 -0
  11. xah/examples/m02/__init__.py +0 -0
  12. xah/examples/m02/e01_tool.py +21 -0
  13. xah/examples/m02/e02_toolset.py +11 -0
  14. xah/examples/m02/e03_harness.py +15 -0
  15. xah/examples/m02/e04_subclassing.py +13 -0
  16. xah/examples/m02/e05_adding.py +15 -0
  17. xah/harness/__init__.py +0 -0
  18. xah/harness/base.py +505 -0
  19. xah/harness/deps.py +122 -0
  20. xah/harness/manager.py +85 -0
  21. xah/instructions/__init__.py +0 -0
  22. xah/instructions/agent.py +17 -0
  23. xah/instructions/base.py +15 -0
  24. xah/instructions/paths.py +20 -0
  25. xah/instructions/static.py +6 -0
  26. xah/models.py +86 -0
  27. xah/obs.py +17 -0
  28. xah/options/__init__.py +0 -0
  29. xah/options/base.py +92 -0
  30. xah/options/path.py +62 -0
  31. xah/options/provider.py +24 -0
  32. xah/paths.py +21 -0
  33. xah/pyproject.package.toml +66 -0
  34. xah/server/__init__.py +1 -0
  35. xah/server/acp/__init__.py +0 -0
  36. xah/server/acp/agent.py +45 -0
  37. xah/server/acp/proxy.py +31 -0
  38. xah/server/acp/server.py +214 -0
  39. xah/server/acp/thinking.py +217 -0
  40. xah/server/acp/usage.py +33 -0
  41. xah/settings.py +84 -0
  42. xah/toolsets/__init__.py +0 -0
  43. xah/toolsets/base/__init__.py +0 -0
  44. xah/toolsets/base/approval.py +32 -0
  45. xah/toolsets/base/tool.py +227 -0
  46. xah/toolsets/base/toolset.py +278 -0
  47. xah/toolsets/context/__init__.py +0 -0
  48. xah/toolsets/context/compact.py +19 -0
  49. xah/toolsets/context/toolset.py +13 -0
  50. xah/toolsets/fs/__init__.py +0 -0
  51. xah/toolsets/fs/edit.py +80 -0
  52. xah/toolsets/fs/mkdir.py +15 -0
  53. xah/toolsets/fs/move.py +43 -0
  54. xah/toolsets/fs/path.py +13 -0
  55. xah/toolsets/fs/permissions.py +12 -0
  56. xah/toolsets/fs/read.py +125 -0
  57. xah/toolsets/fs/remove.py +20 -0
  58. xah/toolsets/fs/search.py +111 -0
  59. xah/toolsets/fs/similarity.py +26 -0
  60. xah/toolsets/fs/toolset.py +23 -0
  61. xah/toolsets/fs/write.py +45 -0
  62. xah/toolsets/kb/__init__.py +0 -0
  63. xah/toolsets/kb/db.py +48 -0
  64. xah/toolsets/kb/get.py +30 -0
  65. xah/toolsets/kb/history.py +28 -0
  66. xah/toolsets/kb/list.py +15 -0
  67. xah/toolsets/kb/query.py +60 -0
  68. xah/toolsets/kb/toolset.py +39 -0
  69. xah/toolsets/kb/validators.py +13 -0
  70. xah/toolsets/management/__init__.py +0 -0
  71. xah/toolsets/management/activate.py +87 -0
  72. xah/toolsets/management/list.py +9 -0
  73. xah/toolsets/management/toolset.py +21 -0
  74. xah/toolsets/python/__init__.py +0 -0
  75. xah/toolsets/python/pip.py +16 -0
  76. xah/toolsets/python/run.py +45 -0
  77. xah/toolsets/python/run_code.py +34 -0
  78. xah/toolsets/python/test.py +33 -0
  79. xah/toolsets/python/toolset.py +20 -0
  80. xah/toolsets/shell/__init__.py +0 -0
  81. xah/toolsets/shell/run.py +181 -0
  82. xah/toolsets/shell/toolset.py +17 -0
  83. xah/toolsets/skills/__init__.py +0 -0
  84. xah/toolsets/skills/list.py +31 -0
  85. xah/toolsets/skills/load.py +62 -0
  86. xah/toolsets/skills/skill.py +22 -0
  87. xah/toolsets/skills/toolset.py +44 -0
  88. xah/toolsets/subagent/__init__.py +0 -0
  89. xah/toolsets/subagent/parent.py +25 -0
  90. xah/toolsets/subagent/task.py +44 -0
  91. xah/toolsets/subagent/toolset.py +26 -0
  92. xah/toolsets/terminal/__init__.py +0 -0
  93. xah/toolsets/terminal/base.py +118 -0
  94. xah/toolsets/terminal/kill.py +9 -0
  95. xah/toolsets/terminal/output.py +21 -0
  96. xah/toolsets/terminal/status.py +11 -0
  97. xah/toolsets/terminal/terminal.py +167 -0
  98. xah/toolsets/terminal/toolset.py +31 -0
  99. xah/toolsets/todo/__init__.py +0 -0
  100. xah/toolsets/todo/constants.py +8 -0
  101. xah/toolsets/todo/item.py +27 -0
  102. xah/toolsets/todo/read.py +9 -0
  103. xah/toolsets/todo/toolset.py +87 -0
  104. xah/toolsets/todo/update_status.py +30 -0
  105. xah/toolsets/todo/write.py +17 -0
  106. xah/toolsets/utils/__init__.py +0 -0
  107. xah/toolsets/utils/sleep.py +20 -0
  108. xah/toolsets/utils/toolset.py +13 -0
  109. xah/toolsets/web/__init__.py +0 -0
  110. xah/toolsets/web/base.py +106 -0
  111. xah/toolsets/web/fetch.py +13 -0
  112. xah/toolsets/web/request.py +168 -0
  113. xah/toolsets/web/research.py +285 -0
  114. xah/toolsets/web/search.py +23 -0
  115. xah/toolsets/web/toolset.py +23 -0
  116. xah-0.0.1.dist-info/METADATA +97 -0
  117. xah-0.0.1.dist-info/RECORD +120 -0
  118. xah-0.0.1.dist-info/WHEEL +5 -0
  119. xah-0.0.1.dist-info/entry_points.txt +2 -0
  120. xah-0.0.1.dist-info/top_level.txt +1 -0
xah/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from xah.harness.base import Harness
2
+ from xah.toolsets.base.toolset import Toolset
3
+ from xah.toolsets.base.tool import Tool
4
+ from xah.class_collections import classes
xah/caps/__init__.py ADDED
File without changes
xah/caps/retry.py ADDED
@@ -0,0 +1,99 @@
1
+ from dataclasses import dataclass
2
+ from pydantic_ai import RunContext
3
+
4
+ from pydantic_ai.capabilities import AbstractCapability
5
+ from pydantic_ai.messages import SystemPromptPart
6
+ from pydantic_deep import DeepAgentDeps
7
+
8
+ from corio import logs, strings, yml
9
+ from xah.constants import HARNESS_FEEDBACK
10
+ from corio.jsn import from_json, to_json
11
+ from xah.obs import logger
12
+
13
+ @dataclass
14
+ class ToolCallArgsDeserialzer(AbstractCapability[DeepAgentDeps]):
15
+ """Log schema-validation failures before Pydantic AI retries the model."""
16
+
17
+ label: str
18
+
19
+ def _enqueue(self, ctx, *messages: str):
20
+ content = strings.trim(HARNESS_FEEDBACK, *messages)
21
+ ctx.enqueue(SystemPromptPart(content=content))
22
+
23
+ def _log(self, ctx: RunContext[DeepAgentDeps], *, kind: str, error, tool_name: str | None = None):
24
+ payload = {
25
+ "agent": self.label,
26
+ "part_kind": "model-retry",
27
+ "retry_kind": kind,
28
+ "retry": ctx.retry,
29
+ "max_retries": ctx.max_retries,
30
+ "error": str(error),
31
+ }
32
+ if tool_name is not None:
33
+ payload["tool_name"] = tool_name
34
+ logger.info(logs.sanitize(yml.to_yaml(payload)))
35
+
36
+ async def wrap_tool_validate(self, ctx, *, call, tool_def, args, handler):
37
+ try:
38
+ return await handler(args)
39
+ except Exception as validation_error:
40
+ if isinstance(args, dict):
41
+ properties = tool_def.parameters_json_schema.get("properties", {})
42
+ recovered_args = dict(args)
43
+ recovered = False
44
+
45
+ for name, value in args.items():
46
+ expected_type = properties.get(name, {}).get("type")
47
+ if not isinstance(value, str) or expected_type not in {"array", "object"}:
48
+ continue
49
+
50
+ try:
51
+ decoded_value = from_json(value)
52
+ except (TypeError, ValueError):
53
+ continue
54
+
55
+ expected_python_type = list if expected_type == "array" else dict
56
+ if isinstance(decoded_value, expected_python_type):
57
+ recovered_args[name] = decoded_value
58
+ recovered = True
59
+
60
+ if not recovered:
61
+ raise validation_error
62
+
63
+ self._enqueue(
64
+ ctx,
65
+ "In future, pass nested array and object arguments as JSON values, not escaped JSON strings.",
66
+ )
67
+ return await handler(recovered_args)
68
+
69
+ if not isinstance(args, str):
70
+ raise
71
+
72
+ try:
73
+ decoded_args = from_json(args)
74
+ except (TypeError, ValueError):
75
+ raise validation_error
76
+
77
+ # Regular JSON objects have already had their fair validation attempt.
78
+ # Only retry when the outer JSON value was itself an encoded JSON string.
79
+ if not isinstance(decoded_args, str):
80
+ raise validation_error
81
+
82
+ self._enqueue(
83
+ ctx,
84
+ "In future, pass tool arguments as regular JSON, not as a nested/escaped JSON string.",
85
+ )
86
+ return await handler(decoded_args)
87
+
88
+ async def on_tool_validate_error(self, ctx, *, call, tool_def, args, error):
89
+ self._log(ctx, kind="tool-schema", tool_name=call.tool_name, error=error)
90
+ self._enqueue(
91
+ ctx,
92
+ "Remember the tool definition and make sure the arguments match its schema:",
93
+ to_json(tool_def.parameters_json_schema),
94
+ )
95
+ raise error
96
+
97
+ async def on_output_validate_error(self, ctx, *, output_context, output, error):
98
+ self._log(ctx, kind="output-schema", error=error)
99
+ raise error
xah/caps/traffic.py ADDED
@@ -0,0 +1,398 @@
1
+ import json
2
+ import re
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, ClassVar
5
+
6
+ from pydantic_ai import ModelMessagesTypeAdapter, RunContext
7
+ from pydantic_ai.capabilities import AbstractCapability
8
+ from pydantic_ai.messages import (
9
+ FunctionToolResultEvent,
10
+ InstructionPart,
11
+ ModelMessage,
12
+ ModelRequest,
13
+ ModelRequestPart,
14
+ ModelResponse,
15
+ )
16
+ from pydantic_ai.models import ModelRequestContext
17
+ from pydantic_deep import DeepAgentDeps
18
+
19
+ from corio import logs, yml
20
+ from corio.jsn import to_json
21
+ from xah.constants import TODO_STATE_END, TODO_STATE_START
22
+ from xah.obs import logger
23
+
24
+
25
+ TODO_STATE_PATTERN = re.compile(
26
+ f"{re.escape(TODO_STATE_START)}(.*?){re.escape(TODO_STATE_END)}",
27
+ re.DOTALL,
28
+ )
29
+ UNREADABLE_CHARACTER_PATTERN = re.compile(r"[\x00-\x08\x0b-\x1f\x7f-\x9f\ufeff]")
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class StructuredDataDeserializer:
34
+ value: Any
35
+
36
+ @property
37
+ def data(self) -> Any:
38
+ if self.is_binary:
39
+ return self.binary_data
40
+ if isinstance(self.value, dict):
41
+ return {key: type(self)(value).data for key, value in self.value.items()}
42
+ if isinstance(self.value, list | tuple):
43
+ return [type(self)(value).data for value in self.value]
44
+ if isinstance(self.value, str):
45
+ return self.decoded_string
46
+ return self.value
47
+
48
+ @property
49
+ def is_binary(self) -> bool:
50
+ return isinstance(self.value, dict) and self.value.get("kind") == "binary"
51
+
52
+ @property
53
+ def binary_data(self) -> dict[str, Any]:
54
+ encoded = self.value["data"]
55
+ byte_count = (len(encoded) * 3) // 4 - encoded.count("=")
56
+ return {
57
+ "kind": "binary",
58
+ "media_type": self.value["media_type"],
59
+ "bytes": byte_count,
60
+ "identifier": self.value["identifier"],
61
+ }
62
+
63
+ @property
64
+ def decoded_string(self) -> Any:
65
+ value = self.value
66
+ while isinstance(value, str):
67
+ try:
68
+ value = json.loads(value)
69
+ except json.JSONDecodeError:
70
+ return value
71
+
72
+ if isinstance(value, dict | list):
73
+ return type(self)(value).data
74
+ return self.value
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class PartDeserializer:
79
+ source: dict[str, Any]
80
+
81
+ fields: ClassVar[tuple[str, ...]] = ("content",)
82
+ ignored_fields: ClassVar[frozenset[str]] = frozenset(
83
+ {
84
+ "id",
85
+ "provider_details",
86
+ "provider_name",
87
+ "signature",
88
+ "timestamp",
89
+ }
90
+ )
91
+
92
+ @classmethod
93
+ def from_source(cls, source: dict[str, Any]) -> "PartDeserializer":
94
+ deserializer = cls.deserializers.get(source["part_kind"], GenericPartDeserializer)
95
+ return deserializer(source)
96
+
97
+ deserializers: ClassVar[dict[str, type["PartDeserializer"]]]
98
+
99
+ @property
100
+ def kind(self) -> str:
101
+ return self.source["part_kind"]
102
+
103
+ @property
104
+ def data(self) -> dict[str, Any]:
105
+ data = {"kind": self.kind}
106
+ data.update(self.selected_data)
107
+ return data
108
+
109
+ @property
110
+ def selected_data(self) -> dict[str, Any]:
111
+ return {
112
+ name: StructuredDataDeserializer(self.source[name]).data
113
+ for name in self.fields
114
+ if name in self.source
115
+ }
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class ToolPartDeserializer(PartDeserializer):
120
+ fields: ClassVar[tuple[str, ...]] = ("tool_name", "tool_call_id")
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class ToolCallDeserializer(ToolPartDeserializer):
125
+ fields: ClassVar[tuple[str, ...]] = (*ToolPartDeserializer.fields, "args")
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class ToolResultDeserializer(ToolPartDeserializer):
130
+ fields: ClassVar[tuple[str, ...]] = (*ToolPartDeserializer.fields, "outcome", "content")
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class GenericPartDeserializer(PartDeserializer):
135
+ @property
136
+ def selected_data(self) -> dict[str, Any]:
137
+ return {
138
+ name: StructuredDataDeserializer(value).data
139
+ for name, value in self.source.items()
140
+ if name != "part_kind" and name not in self.ignored_fields
141
+ }
142
+
143
+
144
+ PartDeserializer.deserializers = {
145
+ "builtin-tool-call": ToolCallDeserializer,
146
+ "builtin-tool-return": ToolResultDeserializer,
147
+ "retry-prompt": ToolResultDeserializer,
148
+ "tool-call": ToolCallDeserializer,
149
+ "tool-return": ToolResultDeserializer,
150
+ }
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class MessageDeserializer:
155
+ messages: list[ModelMessage]
156
+
157
+ @property
158
+ def serialized_messages(self) -> list[dict[str, Any]]:
159
+ return ModelMessagesTypeAdapter.dump_python(self.messages, mode="json", exclude_none=True)
160
+
161
+ @property
162
+ def current_request_messages(self) -> list[dict[str, Any]]:
163
+ current: list[dict[str, Any]] = []
164
+ for message in reversed(self.serialized_messages):
165
+ if message["kind"] == "response":
166
+ break
167
+ current.append(message)
168
+ return list(reversed(current))
169
+
170
+ @property
171
+ def current_request_parts(self) -> list[dict[str, Any]]:
172
+ return self.parts(self.current_request_messages)
173
+
174
+ @classmethod
175
+ def parts(cls, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
176
+ return [
177
+ PartDeserializer.from_source(part).data
178
+ for message in messages
179
+ for part in message["parts"]
180
+ ]
181
+
182
+
183
+ @dataclass(frozen=True)
184
+ class ResponseDeserializer(MessageDeserializer):
185
+ @property
186
+ def response_parts(self) -> list[dict[str, Any]]:
187
+ return self.parts(self.serialized_messages)
188
+
189
+
190
+ @dataclass(frozen=True)
191
+ class EventPartDeserializer:
192
+ part: ModelRequestPart
193
+
194
+ @property
195
+ def data(self) -> dict[str, Any]:
196
+ message = ModelRequest(parts=[self.part])
197
+ source = ModelMessagesTypeAdapter.dump_python([message], mode="json", exclude_none=True)[0]["parts"][0]
198
+ return PartDeserializer.from_source(source).data
199
+
200
+
201
+ @dataclass(frozen=True)
202
+ class SetupSnapshot:
203
+ instructions: str
204
+ tools: tuple[str, ...]
205
+ todos: str
206
+
207
+ @classmethod
208
+ def from_context(cls, request_context: ModelRequestContext) -> "SetupSnapshot":
209
+ parameters = request_context.model_request_parameters
210
+ parts = parameters.instruction_parts or []
211
+ instructions = InstructionDeserializer(parts)
212
+ return cls(
213
+ instructions=instructions.instructions,
214
+ tools=tuple(sorted(parameters.tool_defs)),
215
+ todos=instructions.todos,
216
+ )
217
+
218
+ def changes(self, previous: "SetupSnapshot | None") -> dict[str, Any]:
219
+ if previous is None:
220
+ return self.initial_data
221
+ changes: dict[str, Any] = {}
222
+ if self.instructions != previous.instructions:
223
+ changes["instructions"] = self.instructions
224
+ if self.tools != previous.tools:
225
+ changes["tools"] = list(self.tools)
226
+ if self.todos != previous.todos:
227
+ changes["todos"] = self.todos
228
+ return changes
229
+
230
+ @property
231
+ def initial_data(self) -> dict[str, Any]:
232
+ data: dict[str, Any] = {}
233
+ if self.instructions:
234
+ data["instructions"] = self.instructions
235
+ if self.tools:
236
+ data["tools"] = list(self.tools)
237
+ if self.todos:
238
+ data["todos"] = self.todos
239
+ return data
240
+
241
+
242
+ @dataclass(frozen=True)
243
+ class InstructionDeserializer:
244
+ parts: list[InstructionPart]
245
+
246
+ @property
247
+ def instruction_texts(self) -> tuple[str, ...]:
248
+ texts = [self.instruction_content(part) for part in self.parts]
249
+ return tuple(text for text in texts if text)
250
+
251
+ @property
252
+ def instructions(self) -> str:
253
+ texts = tuple(dict.fromkeys(self.instruction_texts))
254
+ texts = tuple(
255
+ text
256
+ for text in texts
257
+ if not any(text != other and text in other for other in texts)
258
+ )
259
+ return "\n\n".join(texts)
260
+
261
+ @property
262
+ def todos(self) -> str:
263
+ blocks = [
264
+ match.group(1).strip()
265
+ for part in self.parts
266
+ for match in TODO_STATE_PATTERN.finditer(part.content)
267
+ if match.group(1).strip()
268
+ ]
269
+ return "\n\n".join(blocks)
270
+
271
+ @staticmethod
272
+ def instruction_content(part: InstructionPart) -> str:
273
+ content = TODO_STATE_PATTERN.sub("", part.content)
274
+ return re.sub(r"\n{3,}", "\n\n", content).strip()
275
+
276
+ @dataclass
277
+ class SetupTracker:
278
+ snapshot: SetupSnapshot | None = None
279
+
280
+
281
+ @dataclass
282
+ class ModelTrafficLogger(AbstractCapability[DeepAgentDeps]):
283
+ label: str
284
+ setup_tracker: SetupTracker = field(default_factory=SetupTracker)
285
+ request_signature: str | None = None
286
+ pending_event_parts: set[str] = field(default_factory=set)
287
+
288
+ async def for_run(self, ctx: RunContext[DeepAgentDeps]) -> "ModelTrafficLogger":
289
+ return type(self)(label=self.label, setup_tracker=self.setup_tracker)
290
+
291
+ @staticmethod
292
+ def readable_payload(value: Any) -> Any:
293
+ if isinstance(value, dict):
294
+ return {key: ModelTrafficLogger.readable_payload(item) for key, item in value.items()}
295
+ if isinstance(value, list):
296
+ return [ModelTrafficLogger.readable_payload(item) for item in value]
297
+ if not isinstance(value, str):
298
+ return value
299
+
300
+ if "\n" not in value and "\\n" in value:
301
+ value = value.replace("\\r\\n", "\n").replace("\\n", "\n")
302
+
303
+ value = value.replace("\r\n", "\n").replace("\r", "\n").replace("\t", " ")
304
+ value = "\n".join(line.rstrip() for line in value.split("\n"))
305
+ return UNREADABLE_CHARACTER_PATTERN.sub("�", value)
306
+
307
+ def log_payload(self, payload: dict[str, Any]) -> None:
308
+ payload = self.readable_payload(payload)
309
+ message = yml.to_yaml({"agent": self.label, **payload})
310
+ logger.info(logs.sanitize(message))
311
+
312
+ def log_setup(self, request_context: ModelRequestContext) -> None:
313
+ snapshot = SetupSnapshot.from_context(request_context)
314
+ changes = snapshot.changes(self.setup_tracker.snapshot)
315
+ self.setup_tracker.snapshot = snapshot
316
+ if changes:
317
+ self.log_payload({"event": "setup", **changes})
318
+
319
+ def log_request(self, request_context: ModelRequestContext) -> None:
320
+ message = MessageDeserializer(request_context.messages)
321
+ parts = self.unlogged_request_parts(message)
322
+ signature = to_json(parts)
323
+ if self.request_signature == signature:
324
+ return
325
+ self.request_signature = signature
326
+ if parts:
327
+ self.log_payload(
328
+ {
329
+ "event": "model-request",
330
+ "model": request_context.model.model_name,
331
+ "messages": parts,
332
+ }
333
+ )
334
+
335
+ def unlogged_request_parts(self, message: MessageDeserializer) -> list[dict[str, Any]]:
336
+ parts: list[dict[str, Any]] = []
337
+ for part in message.current_request_parts:
338
+ signature = to_json(part)
339
+ if signature in self.pending_event_parts:
340
+ self.pending_event_parts.remove(signature)
341
+ continue
342
+ parts.append(part)
343
+ return parts
344
+
345
+ def log_response(self, response: ModelResponse) -> None:
346
+ message = ResponseDeserializer([response])
347
+ self.log_payload(
348
+ {
349
+ "event": "model-response",
350
+ "model": response.model_name,
351
+ "messages": message.response_parts,
352
+ }
353
+ )
354
+
355
+ def log_compaction(
356
+ self,
357
+ messages: list[ModelMessage],
358
+ summarized: bool,
359
+ summary: str | None,
360
+ ) -> None:
361
+ if not summarized:
362
+ return
363
+
364
+ self.log_payload(
365
+ {
366
+ "event": "context-compaction",
367
+ "summary": summary,
368
+ "messages": MessageDeserializer(messages).serialized_messages,
369
+ }
370
+ )
371
+
372
+ async def wrap_model_request(
373
+ self,
374
+ ctx: RunContext[DeepAgentDeps],
375
+ *,
376
+ request_context: ModelRequestContext,
377
+ handler,
378
+ ) -> ModelResponse:
379
+ self.log_setup(request_context)
380
+ self.log_request(request_context)
381
+ response = await handler(request_context)
382
+ self.log_response(response)
383
+ self.request_signature = None
384
+ return response
385
+
386
+ async def wrap_run_event_stream(self, ctx: RunContext[DeepAgentDeps], *, stream):
387
+ async for event in stream:
388
+ if isinstance(event, FunctionToolResultEvent):
389
+ self.log_event_part(event)
390
+ yield event
391
+
392
+ def log_event_part(self, event: FunctionToolResultEvent) -> None:
393
+ part = EventPartDeserializer(event.part).data
394
+ signature = to_json(part)
395
+ if signature in self.pending_event_parts:
396
+ return
397
+ self.pending_event_parts.add(signature)
398
+ self.log_payload({"event": "tool-result", **part})
@@ -0,0 +1,12 @@
1
+ from collections.abc import Callable
2
+
3
+ from corio.function import ccp
4
+ from corio.iterator import ilist
5
+
6
+
7
+ def classes(function: Callable) -> ccp:
8
+ @ccp
9
+ def declaration(cls):
10
+ return ilist(function(cls))
11
+
12
+ return declaration
xah/constants.py ADDED
@@ -0,0 +1,10 @@
1
+ PROJECTION_READ_TOOL_NAMES = "read_tool_names"
2
+ PROJECTION_WRITE_TOOL_NAMES = "write_tool_names"
3
+ PROJECTION_EDIT_TOOL_NAMES = "edit_tool_names"
4
+ PROJECTION_SEARCH_TOOL_NAMES = "search_tool_names"
5
+ PROJECTION_BASH_TOOL_NAMES = "bash_tool_names"
6
+
7
+ HARNESS_FEEDBACK = "[HARNESS FEEDBACK]"
8
+
9
+ TODO_STATE_START = "<todo-state>"
10
+ TODO_STATE_END = "</todo-state>"
xah/entrypoint.py ADDED
@@ -0,0 +1,15 @@
1
+
2
+
3
+ from xah.settings import Settings
4
+
5
+
6
+ def main():
7
+
8
+ Settings().run()
9
+ from xah.harness.base import Harness
10
+ Harness.launch()
11
+
12
+
13
+
14
+ if __name__ == "__main__":
15
+ main()
File without changes
File without changes
@@ -0,0 +1,10 @@
1
+ from xah import Harness
2
+
3
+
4
+ class HelloWorld(Harness):
5
+ # (1)!
6
+ instructions = 'You are an agent who only says "Hello World"!'
7
+
8
+
9
+ if __name__ == "__main__":
10
+ HelloWorld.launch()
File without changes
@@ -0,0 +1,21 @@
1
+ from random import randint
2
+
3
+ from pydantic import Field
4
+
5
+ from xah import Tool
6
+
7
+
8
+ class AgeTool(Tool):
9
+ """Use this tool to provide a randomly generated age for a named user."""
10
+ # (1)!
11
+
12
+ # (2)!
13
+ user: str = Field(
14
+ ...,
15
+ description="The name of the user whose age you want to know.", # (3)!
16
+ )
17
+
18
+ async def run(self):
19
+ # (4)!
20
+ age = randint(1, 100)
21
+ return f"{self.user} is {age} years old."
@@ -0,0 +1,11 @@
1
+ from xah import Toolset, classes
2
+ from xah.examples.m02.e01_tool import AgeTool
3
+
4
+
5
+ class UserFactsToolset(Toolset):
6
+ """Tools for fetching facts about users."""
7
+
8
+ @classes
9
+ def Tools(cls):
10
+ # (1)!
11
+ return [AgeTool]
@@ -0,0 +1,15 @@
1
+ from xah import Harness, classes
2
+ from xah.examples.m02.e02_toolset import UserFactsToolset
3
+
4
+
5
+ class DemoHarness(Harness):
6
+ instructions = "Use the tools to tell the user facts about themself."
7
+
8
+ @classes
9
+ def Toolsets(cls):
10
+ # (1)!
11
+ return [UserFactsToolset]
12
+
13
+
14
+ if __name__ == "__main__":
15
+ DemoHarness.launch()
@@ -0,0 +1,13 @@
1
+ from random import choice
2
+
3
+ from xah.examples.m02.e01_tool import AgeTool
4
+
5
+
6
+ # (1)!
7
+ class PetTool(AgeTool):
8
+ """Return a randomly chosen pet for a user."""
9
+
10
+ # (2)!
11
+ async def run(self):
12
+ pet = choice(["cat", "otter", "axolotl"])
13
+ return f"{self.user} has a {pet} as a pet."
@@ -0,0 +1,15 @@
1
+ from xah.examples.m02.e01_tool import AgeTool
2
+ from xah.examples.m02.e02_toolset import UserFactsToolset
3
+ from xah.examples.m02.e03_harness import DemoHarness
4
+ from xah.examples.m02.e04_subclassing import PetTool
5
+
6
+
7
+ # (1)!
8
+ UserFactsToolset.Tools.append(PetTool)
9
+
10
+ # (2)!
11
+ UserFactsToolset.Tools.remove(AgeTool)
12
+
13
+
14
+ if __name__ == "__main__":
15
+ DemoHarness.launch()
File without changes