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.
- xah/__init__.py +4 -0
- xah/caps/__init__.py +0 -0
- xah/caps/retry.py +99 -0
- xah/caps/traffic.py +398 -0
- xah/class_collections.py +12 -0
- xah/constants.py +10 -0
- xah/entrypoint.py +15 -0
- xah/examples/__init__.py +0 -0
- xah/examples/m01/__init__.py +0 -0
- xah/examples/m01/e01_hello.py +10 -0
- xah/examples/m02/__init__.py +0 -0
- xah/examples/m02/e01_tool.py +21 -0
- xah/examples/m02/e02_toolset.py +11 -0
- xah/examples/m02/e03_harness.py +15 -0
- xah/examples/m02/e04_subclassing.py +13 -0
- xah/examples/m02/e05_adding.py +15 -0
- xah/harness/__init__.py +0 -0
- xah/harness/base.py +505 -0
- xah/harness/deps.py +122 -0
- xah/harness/manager.py +85 -0
- xah/instructions/__init__.py +0 -0
- xah/instructions/agent.py +17 -0
- xah/instructions/base.py +15 -0
- xah/instructions/paths.py +20 -0
- xah/instructions/static.py +6 -0
- xah/models.py +86 -0
- xah/obs.py +17 -0
- xah/options/__init__.py +0 -0
- xah/options/base.py +92 -0
- xah/options/path.py +62 -0
- xah/options/provider.py +24 -0
- xah/paths.py +21 -0
- xah/pyproject.package.toml +66 -0
- xah/server/__init__.py +1 -0
- xah/server/acp/__init__.py +0 -0
- xah/server/acp/agent.py +45 -0
- xah/server/acp/proxy.py +31 -0
- xah/server/acp/server.py +214 -0
- xah/server/acp/thinking.py +217 -0
- xah/server/acp/usage.py +33 -0
- xah/settings.py +84 -0
- xah/toolsets/__init__.py +0 -0
- xah/toolsets/base/__init__.py +0 -0
- xah/toolsets/base/approval.py +32 -0
- xah/toolsets/base/tool.py +227 -0
- xah/toolsets/base/toolset.py +278 -0
- xah/toolsets/context/__init__.py +0 -0
- xah/toolsets/context/compact.py +19 -0
- xah/toolsets/context/toolset.py +13 -0
- xah/toolsets/fs/__init__.py +0 -0
- xah/toolsets/fs/edit.py +80 -0
- xah/toolsets/fs/mkdir.py +15 -0
- xah/toolsets/fs/move.py +43 -0
- xah/toolsets/fs/path.py +13 -0
- xah/toolsets/fs/permissions.py +12 -0
- xah/toolsets/fs/read.py +125 -0
- xah/toolsets/fs/remove.py +20 -0
- xah/toolsets/fs/search.py +111 -0
- xah/toolsets/fs/similarity.py +26 -0
- xah/toolsets/fs/toolset.py +23 -0
- xah/toolsets/fs/write.py +45 -0
- xah/toolsets/kb/__init__.py +0 -0
- xah/toolsets/kb/db.py +48 -0
- xah/toolsets/kb/get.py +30 -0
- xah/toolsets/kb/history.py +28 -0
- xah/toolsets/kb/list.py +15 -0
- xah/toolsets/kb/query.py +60 -0
- xah/toolsets/kb/toolset.py +39 -0
- xah/toolsets/kb/validators.py +13 -0
- xah/toolsets/management/__init__.py +0 -0
- xah/toolsets/management/activate.py +87 -0
- xah/toolsets/management/list.py +9 -0
- xah/toolsets/management/toolset.py +21 -0
- xah/toolsets/python/__init__.py +0 -0
- xah/toolsets/python/pip.py +16 -0
- xah/toolsets/python/run.py +45 -0
- xah/toolsets/python/run_code.py +34 -0
- xah/toolsets/python/test.py +33 -0
- xah/toolsets/python/toolset.py +20 -0
- xah/toolsets/shell/__init__.py +0 -0
- xah/toolsets/shell/run.py +181 -0
- xah/toolsets/shell/toolset.py +17 -0
- xah/toolsets/skills/__init__.py +0 -0
- xah/toolsets/skills/list.py +31 -0
- xah/toolsets/skills/load.py +62 -0
- xah/toolsets/skills/skill.py +22 -0
- xah/toolsets/skills/toolset.py +44 -0
- xah/toolsets/subagent/__init__.py +0 -0
- xah/toolsets/subagent/parent.py +25 -0
- xah/toolsets/subagent/task.py +44 -0
- xah/toolsets/subagent/toolset.py +26 -0
- xah/toolsets/terminal/__init__.py +0 -0
- xah/toolsets/terminal/base.py +118 -0
- xah/toolsets/terminal/kill.py +9 -0
- xah/toolsets/terminal/output.py +21 -0
- xah/toolsets/terminal/status.py +11 -0
- xah/toolsets/terminal/terminal.py +167 -0
- xah/toolsets/terminal/toolset.py +31 -0
- xah/toolsets/todo/__init__.py +0 -0
- xah/toolsets/todo/constants.py +8 -0
- xah/toolsets/todo/item.py +27 -0
- xah/toolsets/todo/read.py +9 -0
- xah/toolsets/todo/toolset.py +87 -0
- xah/toolsets/todo/update_status.py +30 -0
- xah/toolsets/todo/write.py +17 -0
- xah/toolsets/utils/__init__.py +0 -0
- xah/toolsets/utils/sleep.py +20 -0
- xah/toolsets/utils/toolset.py +13 -0
- xah/toolsets/web/__init__.py +0 -0
- xah/toolsets/web/base.py +106 -0
- xah/toolsets/web/fetch.py +13 -0
- xah/toolsets/web/request.py +168 -0
- xah/toolsets/web/research.py +285 -0
- xah/toolsets/web/search.py +23 -0
- xah/toolsets/web/toolset.py +23 -0
- xah-0.0.1.dist-info/METADATA +97 -0
- xah-0.0.1.dist-info/RECORD +120 -0
- xah-0.0.1.dist-info/WHEEL +5 -0
- xah-0.0.1.dist-info/entry_points.txt +2 -0
- xah-0.0.1.dist-info/top_level.txt +1 -0
xah/__init__.py
ADDED
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})
|
xah/class_collections.py
ADDED
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
xah/examples/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
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,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()
|
xah/harness/__init__.py
ADDED
|
File without changes
|