agentbuildkit 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.
- agentbuildkit/__init__.py +7 -0
- agentbuildkit/__main__.py +6 -0
- agentbuildkit/agents/__init__.py +21 -0
- agentbuildkit/agents/boundary.py +350 -0
- agentbuildkit/cli.py +140 -0
- agentbuildkit/config/__init__.py +23 -0
- agentbuildkit/config/settings.py +112 -0
- agentbuildkit/demo/__init__.py +9 -0
- agentbuildkit/demo/models.py +61 -0
- agentbuildkit/demo/present.py +79 -0
- agentbuildkit/demo/scenario.py +554 -0
- agentbuildkit/evaluation/__init__.py +35 -0
- agentbuildkit/evaluation/assertions.py +224 -0
- agentbuildkit/evaluation/models.py +142 -0
- agentbuildkit/evaluation/runner.py +165 -0
- agentbuildkit/evaluation/trace.py +46 -0
- agentbuildkit/external_execution/__init__.py +98 -0
- agentbuildkit/external_execution/binding.py +756 -0
- agentbuildkit/external_execution/models.py +171 -0
- agentbuildkit/external_execution/provider.py +184 -0
- agentbuildkit/external_execution/service.py +787 -0
- agentbuildkit/guardrails/__init__.py +27 -0
- agentbuildkit/guardrails/approval.py +500 -0
- agentbuildkit/integrations/__init__.py +100 -0
- agentbuildkit/integrations/cursor.py +394 -0
- agentbuildkit/integrations/github.py +1165 -0
- agentbuildkit/integrations/http.py +366 -0
- agentbuildkit/mcp/__init__.py +25 -0
- agentbuildkit/mcp/registry.py +282 -0
- agentbuildkit/observability/__init__.py +27 -0
- agentbuildkit/observability/audit.py +121 -0
- agentbuildkit/observability/correlation.py +23 -0
- agentbuildkit/observability/telemetry.py +50 -0
- agentbuildkit/orchestrations/__init__.py +24 -0
- agentbuildkit/providers/__init__.py +38 -0
- agentbuildkit/providers/anthropic.py +60 -0
- agentbuildkit/providers/base.py +59 -0
- agentbuildkit/providers/openai.py +48 -0
- agentbuildkit/providers/registry.py +42 -0
- agentbuildkit/py.typed +0 -0
- agentbuildkit/runtime.py +39 -0
- agentbuildkit/state/__init__.py +91 -0
- agentbuildkit/state/api.py +641 -0
- agentbuildkit/state/github.py +1419 -0
- agentbuildkit/state/models.py +886 -0
- agentbuildkit/state/registry.py +3721 -0
- agentbuildkit/testing/__init__.py +5 -0
- agentbuildkit/testing/scripted_chat.py +107 -0
- agentbuildkit/workflows/__init__.py +107 -0
- agentbuildkit/workflows/nesting.py +175 -0
- agentbuildkit/workflows/patterns.py +229 -0
- agentbuildkit/workflows/persistence.py +1032 -0
- agentbuildkit/workflows/runtime.py +457 -0
- agentbuildkit-0.1.0.dist-info/METADATA +166 -0
- agentbuildkit-0.1.0.dist-info/RECORD +58 -0
- agentbuildkit-0.1.0.dist-info/WHEEL +4 -0
- agentbuildkit-0.1.0.dist-info/entry_points.txt +2 -0
- agentbuildkit-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Reusable agent construction and typed execution."""
|
|
2
|
+
|
|
3
|
+
from agentbuildkit.agents.boundary import (
|
|
4
|
+
AgentApprovalRequired,
|
|
5
|
+
AgentDefinition,
|
|
6
|
+
AgentDefinitionError,
|
|
7
|
+
AgentInputError,
|
|
8
|
+
AgentOutputError,
|
|
9
|
+
PlatformAgent,
|
|
10
|
+
create_agent,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AgentApprovalRequired",
|
|
15
|
+
"AgentDefinition",
|
|
16
|
+
"AgentDefinitionError",
|
|
17
|
+
"AgentInputError",
|
|
18
|
+
"AgentOutputError",
|
|
19
|
+
"PlatformAgent",
|
|
20
|
+
"create_agent",
|
|
21
|
+
]
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Typed agent creation and execution on Microsoft Agent Framework.
|
|
2
|
+
|
|
3
|
+
The caller supplies the definition and a chat client. This module does not
|
|
4
|
+
construct a provider client. ``provider`` and ``model`` on the definition name
|
|
5
|
+
what the agent asked for. The caller decides whether that client comes from
|
|
6
|
+
the provider registry or from a test double.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
import uuid
|
|
11
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from agent_framework import (
|
|
16
|
+
Agent,
|
|
17
|
+
AgentResponse,
|
|
18
|
+
AgentSession,
|
|
19
|
+
ChatOptions,
|
|
20
|
+
Content,
|
|
21
|
+
FunctionInvocationContext,
|
|
22
|
+
FunctionMiddleware,
|
|
23
|
+
FunctionTool,
|
|
24
|
+
SupportsChatGetResponse,
|
|
25
|
+
)
|
|
26
|
+
from pydantic import BaseModel, ValidationError
|
|
27
|
+
|
|
28
|
+
from agentbuildkit.guardrails import (
|
|
29
|
+
PreparedToolGuard,
|
|
30
|
+
ToolEffect,
|
|
31
|
+
ToolPolicyError,
|
|
32
|
+
tool_audit_middleware,
|
|
33
|
+
tool_effect,
|
|
34
|
+
)
|
|
35
|
+
from agentbuildkit.observability import (
|
|
36
|
+
AuditEvent,
|
|
37
|
+
AuditLog,
|
|
38
|
+
argument_hash,
|
|
39
|
+
bind_correlation_id,
|
|
40
|
+
reset_correlation_id,
|
|
41
|
+
)
|
|
42
|
+
from agentbuildkit.observability.audit import ApprovalState, AuditOutcome
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AgentInputError(Exception):
|
|
46
|
+
"""The caller did not supply the agent's input model."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AgentOutputError(Exception):
|
|
50
|
+
"""The agent response did not match the output model."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AgentDefinitionError(Exception):
|
|
54
|
+
"""An agent definition is missing a provider id or a model id."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AgentApprovalRequired(Exception):
|
|
58
|
+
"""A tool call is waiting for approval, so the run has no final output."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, requests: Sequence[Content]) -> None:
|
|
61
|
+
super().__init__("Agent run is waiting for tool approval.")
|
|
62
|
+
self.requests = list(requests)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class AgentDefinition[InputT: BaseModel, OutputT: BaseModel]:
|
|
67
|
+
"""One agent. ``create_agent`` receives the chat client separately.
|
|
68
|
+
|
|
69
|
+
``provider`` and ``model`` belong to this agent. They are not process
|
|
70
|
+
settings, and they are not the agent's name. The caller still injects the
|
|
71
|
+
chat client, so a test can supply a scripted client for the same definition.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
name: str
|
|
75
|
+
purpose: str
|
|
76
|
+
instructions: str
|
|
77
|
+
input_type: type[InputT]
|
|
78
|
+
output_type: type[OutputT]
|
|
79
|
+
provider: str
|
|
80
|
+
model: str
|
|
81
|
+
tools: tuple[FunctionTool, ...] = ()
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
object.__setattr__(self, "provider", _require_definition_id(self.provider, "Provider id"))
|
|
85
|
+
object.__setattr__(self, "model", _require_definition_id(self.model, "Model id"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class PlatformAgent[InputT: BaseModel, OutputT: BaseModel]:
|
|
90
|
+
"""A framework agent plus the typed run helper."""
|
|
91
|
+
|
|
92
|
+
definition: AgentDefinition[InputT, OutputT]
|
|
93
|
+
agent: Agent
|
|
94
|
+
audit_log: AuditLog
|
|
95
|
+
|
|
96
|
+
def create_session(self, *, session_id: str | None = None) -> AgentSession:
|
|
97
|
+
"""Create an independent Agent Framework session."""
|
|
98
|
+
return self.agent.create_session(session_id=session_id)
|
|
99
|
+
|
|
100
|
+
async def run(
|
|
101
|
+
self,
|
|
102
|
+
data: InputT,
|
|
103
|
+
*,
|
|
104
|
+
session: AgentSession | None = None,
|
|
105
|
+
correlation_id: str | None = None,
|
|
106
|
+
) -> OutputT:
|
|
107
|
+
"""Validate input, run the agent, and return the parsed output model.
|
|
108
|
+
|
|
109
|
+
This method does not accept additional tools. Only tools declared on the
|
|
110
|
+
definition are registered.
|
|
111
|
+
"""
|
|
112
|
+
if not isinstance(data, self.definition.input_type):
|
|
113
|
+
raise AgentInputError(f"Expected input type {self.definition.input_type.__name__}.")
|
|
114
|
+
resolved_correlation_id = correlation_id or uuid.uuid4().hex
|
|
115
|
+
correlation_token = bind_correlation_id(resolved_correlation_id)
|
|
116
|
+
try:
|
|
117
|
+
response = await self.agent.run(
|
|
118
|
+
data.model_dump_json(),
|
|
119
|
+
session=session,
|
|
120
|
+
options={"response_format": self.definition.output_type},
|
|
121
|
+
middleware=[
|
|
122
|
+
tool_audit_middleware(
|
|
123
|
+
self.audit_log,
|
|
124
|
+
correlation_id=resolved_correlation_id,
|
|
125
|
+
agent_name=self.definition.name,
|
|
126
|
+
provider=self.definition.provider,
|
|
127
|
+
model=self.definition.model,
|
|
128
|
+
),
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
_record_unassigned_tool_calls(
|
|
132
|
+
response,
|
|
133
|
+
names=frozenset(tool.name for tool in self.definition.tools),
|
|
134
|
+
audit_log=self.audit_log,
|
|
135
|
+
correlation_id=resolved_correlation_id,
|
|
136
|
+
agent_name=self.definition.name,
|
|
137
|
+
provider=self.definition.provider,
|
|
138
|
+
model=self.definition.model,
|
|
139
|
+
)
|
|
140
|
+
if response.user_input_requests:
|
|
141
|
+
_record_run(
|
|
142
|
+
self.audit_log,
|
|
143
|
+
correlation_id=resolved_correlation_id,
|
|
144
|
+
agent_name=self.definition.name,
|
|
145
|
+
outcome="pending_approval",
|
|
146
|
+
approval="pending",
|
|
147
|
+
provider=self.definition.provider,
|
|
148
|
+
model=self.definition.model,
|
|
149
|
+
)
|
|
150
|
+
raise AgentApprovalRequired(
|
|
151
|
+
_requests_without_arguments(response.user_input_requests)
|
|
152
|
+
)
|
|
153
|
+
output = _validated_output(self.definition, response)
|
|
154
|
+
except AgentApprovalRequired:
|
|
155
|
+
raise
|
|
156
|
+
except Exception:
|
|
157
|
+
_record_run(
|
|
158
|
+
self.audit_log,
|
|
159
|
+
correlation_id=resolved_correlation_id,
|
|
160
|
+
agent_name=self.definition.name,
|
|
161
|
+
outcome="failed",
|
|
162
|
+
approval="not_required",
|
|
163
|
+
provider=self.definition.provider,
|
|
164
|
+
model=self.definition.model,
|
|
165
|
+
)
|
|
166
|
+
raise
|
|
167
|
+
else:
|
|
168
|
+
_record_run(
|
|
169
|
+
self.audit_log,
|
|
170
|
+
correlation_id=resolved_correlation_id,
|
|
171
|
+
agent_name=self.definition.name,
|
|
172
|
+
outcome="completed",
|
|
173
|
+
approval="not_required",
|
|
174
|
+
provider=self.definition.provider,
|
|
175
|
+
model=self.definition.model,
|
|
176
|
+
)
|
|
177
|
+
return output
|
|
178
|
+
finally:
|
|
179
|
+
reset_correlation_id(correlation_token)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def create_agent[InputT: BaseModel, OutputT: BaseModel](
|
|
183
|
+
definition: AgentDefinition[InputT, OutputT],
|
|
184
|
+
*,
|
|
185
|
+
client: SupportsChatGetResponse,
|
|
186
|
+
audit_log: AuditLog | None = None,
|
|
187
|
+
) -> PlatformAgent[InputT, OutputT]:
|
|
188
|
+
"""Create a framework agent from a caller-supplied definition and client.
|
|
189
|
+
|
|
190
|
+
The client is not built here. Production code asks the provider registry
|
|
191
|
+
for a client. Tests pass a scripted client for the same definition.
|
|
192
|
+
"""
|
|
193
|
+
for function_tool in definition.tools:
|
|
194
|
+
_require_prepared_tool(function_tool)
|
|
195
|
+
resolved_log = audit_log or AuditLog()
|
|
196
|
+
options: ChatOptions[None] = {"model": definition.model}
|
|
197
|
+
agent = Agent(
|
|
198
|
+
client,
|
|
199
|
+
definition.instructions,
|
|
200
|
+
name=definition.name,
|
|
201
|
+
description=definition.purpose,
|
|
202
|
+
tools=list(definition.tools),
|
|
203
|
+
default_options=options,
|
|
204
|
+
middleware=[
|
|
205
|
+
_AssignedToolGuard(frozenset(function_tool.name for function_tool in definition.tools)),
|
|
206
|
+
PreparedToolGuard(),
|
|
207
|
+
],
|
|
208
|
+
)
|
|
209
|
+
return PlatformAgent(
|
|
210
|
+
definition=definition,
|
|
211
|
+
agent=agent,
|
|
212
|
+
audit_log=resolved_log,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class _AssignedToolGuard(FunctionMiddleware):
|
|
217
|
+
"""Refuse tool calls that were not declared on the agent."""
|
|
218
|
+
|
|
219
|
+
def __init__(self, names: frozenset[str]) -> None:
|
|
220
|
+
self._names = names
|
|
221
|
+
|
|
222
|
+
async def process(
|
|
223
|
+
self,
|
|
224
|
+
context: FunctionInvocationContext,
|
|
225
|
+
call_next: Callable[[], Awaitable[None]],
|
|
226
|
+
) -> None:
|
|
227
|
+
if context.function.name not in self._names:
|
|
228
|
+
context.metadata["approval_state"] = "rejected"
|
|
229
|
+
context.result = "Tool is not registered for this agent."
|
|
230
|
+
return
|
|
231
|
+
await call_next()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _record_unassigned_tool_calls(
|
|
235
|
+
response: AgentResponse[Any],
|
|
236
|
+
*,
|
|
237
|
+
names: frozenset[str],
|
|
238
|
+
audit_log: AuditLog,
|
|
239
|
+
correlation_id: str,
|
|
240
|
+
agent_name: str,
|
|
241
|
+
provider: str,
|
|
242
|
+
model: str,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Audit tool names the model requested that this agent is not allowed to call.
|
|
245
|
+
|
|
246
|
+
The framework returns an error result for a name that is not registered.
|
|
247
|
+
This records that rejection from the run transcript. It does not invent one.
|
|
248
|
+
"""
|
|
249
|
+
for message in response.messages:
|
|
250
|
+
for content in message.contents or []:
|
|
251
|
+
name = content.name
|
|
252
|
+
if content.type != "function_call" or not isinstance(name, str) or name in names:
|
|
253
|
+
continue
|
|
254
|
+
audit_log.record(
|
|
255
|
+
AuditEvent(
|
|
256
|
+
correlation_id=correlation_id,
|
|
257
|
+
agent_name=agent_name,
|
|
258
|
+
tool_name=name,
|
|
259
|
+
approval="rejected",
|
|
260
|
+
outcome="failed",
|
|
261
|
+
argument_hash=argument_hash(_call_arguments(content)),
|
|
262
|
+
provider=provider,
|
|
263
|
+
model=model,
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _call_arguments(content: Content) -> dict[str, Any]:
|
|
269
|
+
if content.type != "function_call":
|
|
270
|
+
return {}
|
|
271
|
+
parsed = content.parse_arguments()
|
|
272
|
+
if not isinstance(parsed, Mapping):
|
|
273
|
+
return {}
|
|
274
|
+
arguments: dict[str, Any] = {}
|
|
275
|
+
for key, value in parsed.items():
|
|
276
|
+
arguments[str(key)] = value
|
|
277
|
+
return arguments
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _requests_without_arguments(requests: Sequence[Content]) -> list[Content]:
|
|
281
|
+
"""Keep approval requests without the argument values the model supplied."""
|
|
282
|
+
safe: list[Content] = []
|
|
283
|
+
for request in requests:
|
|
284
|
+
copied = copy.copy(request)
|
|
285
|
+
copied.arguments = None
|
|
286
|
+
function_call = copied.function_call
|
|
287
|
+
if function_call is not None:
|
|
288
|
+
call = copy.copy(function_call)
|
|
289
|
+
call.arguments = None
|
|
290
|
+
copied.function_call = call
|
|
291
|
+
safe.append(copied)
|
|
292
|
+
return safe
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _validated_output[InputT: BaseModel, OutputT: BaseModel](
|
|
296
|
+
definition: AgentDefinition[InputT, OutputT],
|
|
297
|
+
response: AgentResponse[Any],
|
|
298
|
+
) -> OutputT:
|
|
299
|
+
try:
|
|
300
|
+
value = response.value
|
|
301
|
+
except (ValidationError, ValueError):
|
|
302
|
+
raise AgentOutputError(
|
|
303
|
+
f"Agent '{definition.name}' returned output that does not match "
|
|
304
|
+
f"{definition.output_type.__name__}."
|
|
305
|
+
) from None
|
|
306
|
+
if not isinstance(value, definition.output_type):
|
|
307
|
+
raise AgentOutputError(
|
|
308
|
+
f"Agent '{definition.name}' returned output that does not match "
|
|
309
|
+
f"{definition.output_type.__name__}."
|
|
310
|
+
)
|
|
311
|
+
return value
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _record_run(
|
|
315
|
+
audit_log: AuditLog,
|
|
316
|
+
*,
|
|
317
|
+
correlation_id: str,
|
|
318
|
+
agent_name: str,
|
|
319
|
+
outcome: AuditOutcome,
|
|
320
|
+
approval: ApprovalState,
|
|
321
|
+
provider: str,
|
|
322
|
+
model: str,
|
|
323
|
+
) -> None:
|
|
324
|
+
audit_log.record(
|
|
325
|
+
AuditEvent(
|
|
326
|
+
correlation_id=correlation_id,
|
|
327
|
+
agent_name=agent_name,
|
|
328
|
+
tool_name=agent_name,
|
|
329
|
+
approval=approval,
|
|
330
|
+
outcome=outcome,
|
|
331
|
+
argument_hash="agent-run",
|
|
332
|
+
provider=provider,
|
|
333
|
+
model=model,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _require_definition_id(value: object, label: str) -> str:
|
|
339
|
+
if not isinstance(value, str) or value.strip() == "":
|
|
340
|
+
raise AgentDefinitionError(f"{label} must not be empty.")
|
|
341
|
+
return value.strip()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _require_prepared_tool(function_tool: FunctionTool) -> None:
|
|
345
|
+
effect = tool_effect(function_tool)
|
|
346
|
+
expected = "always_require" if effect is ToolEffect.SIDE_EFFECT else "never_require"
|
|
347
|
+
if function_tool.approval_mode != expected:
|
|
348
|
+
raise ToolPolicyError(
|
|
349
|
+
f"Tool '{function_tool.name}' effect {effect.value} does not match its approval mode."
|
|
350
|
+
)
|
agentbuildkit/cli.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""AgentBuildKit command line.
|
|
2
|
+
|
|
3
|
+
``demo``, ``doctor``, and ``version`` are the current commands. Add later
|
|
4
|
+
commands as new subparsers without changing the ones that already exist.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
import sqlite3
|
|
11
|
+
import sys
|
|
12
|
+
import traceback
|
|
13
|
+
from collections.abc import Callable, Sequence
|
|
14
|
+
|
|
15
|
+
import agentbuildkit
|
|
16
|
+
from agentbuildkit.demo import DemoError, run_demo
|
|
17
|
+
|
|
18
|
+
_OPENAI_KEY = "OPENAI_API_KEY"
|
|
19
|
+
_ANTHROPIC_KEY = "ANTHROPIC_API_KEY"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
"""Console-script entry point."""
|
|
24
|
+
raise SystemExit(run(sys.argv[1:]))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run(argv: Sequence[str]) -> int:
|
|
28
|
+
"""Run one AgentBuildKit command and return its exit status."""
|
|
29
|
+
parser = _parser()
|
|
30
|
+
try:
|
|
31
|
+
args = parser.parse_args(list(argv))
|
|
32
|
+
except SystemExit as exc:
|
|
33
|
+
if isinstance(exc.code, int):
|
|
34
|
+
return exc.code
|
|
35
|
+
return 0
|
|
36
|
+
command = args.command
|
|
37
|
+
if command == "version":
|
|
38
|
+
print(f"AgentBuildKit {agentbuildkit.__version__}")
|
|
39
|
+
return 0
|
|
40
|
+
if command == "doctor":
|
|
41
|
+
return doctor()
|
|
42
|
+
if command == "demo":
|
|
43
|
+
return _demo(approve=args.approve, reject=args.reject, verbose=args.verbose)
|
|
44
|
+
print("Unknown command.", file=sys.stderr)
|
|
45
|
+
return 2
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def doctor(*, writer: Callable[[str], None] = print) -> int:
|
|
49
|
+
"""Report local installation checks. Missing provider keys are not a failure."""
|
|
50
|
+
python = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
51
|
+
package = agentbuildkit.__version__
|
|
52
|
+
sqlite_ok, sqlite_version = _sqlite()
|
|
53
|
+
writer("AgentBuildKit Doctor")
|
|
54
|
+
writer("")
|
|
55
|
+
writer(_status("Python", python, python != ""))
|
|
56
|
+
writer(_status("AgentBuildKit", package, package != ""))
|
|
57
|
+
writer(_status("SQLite", sqlite_version, sqlite_ok))
|
|
58
|
+
writer("")
|
|
59
|
+
writer("Providers")
|
|
60
|
+
writer(f"{'OpenAI':<18} {_credential(_OPENAI_KEY)}")
|
|
61
|
+
writer(f"{'Anthropic':<18} {_credential(_ANTHROPIC_KEY)}")
|
|
62
|
+
writer("")
|
|
63
|
+
healthy = python != "" and package != "" and sqlite_ok
|
|
64
|
+
if healthy:
|
|
65
|
+
writer("Status: ready for offline demo")
|
|
66
|
+
return 0
|
|
67
|
+
writer("Status: installation check failed")
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _parser() -> argparse.ArgumentParser:
|
|
72
|
+
parser = argparse.ArgumentParser(
|
|
73
|
+
prog="agentkit",
|
|
74
|
+
description="AgentBuildKit is a harness for reliable AI agents.",
|
|
75
|
+
)
|
|
76
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
77
|
+
commands.add_parser("version", help="Print the AgentBuildKit version.")
|
|
78
|
+
commands.add_parser("doctor", help="Check the local installation.")
|
|
79
|
+
demo = commands.add_parser("demo", help="Run the offline harness demonstration.")
|
|
80
|
+
choice = demo.add_mutually_exclusive_group()
|
|
81
|
+
choice.add_argument(
|
|
82
|
+
"--approve",
|
|
83
|
+
action="store_true",
|
|
84
|
+
help="Approve the side effect without prompting.",
|
|
85
|
+
)
|
|
86
|
+
choice.add_argument(
|
|
87
|
+
"--reject",
|
|
88
|
+
action="store_true",
|
|
89
|
+
help="Reject the side effect without prompting.",
|
|
90
|
+
)
|
|
91
|
+
demo.add_argument("--verbose", action="store_true", help="Show an error traceback.")
|
|
92
|
+
return parser
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _demo(*, approve: bool, reject: bool, verbose: bool) -> int:
|
|
96
|
+
if approve:
|
|
97
|
+
decision: bool | None = True
|
|
98
|
+
elif reject:
|
|
99
|
+
decision = False
|
|
100
|
+
else:
|
|
101
|
+
decision = None
|
|
102
|
+
try:
|
|
103
|
+
result = asyncio.run(run_demo(decision=decision, writer=print))
|
|
104
|
+
except DemoError as exc:
|
|
105
|
+
print(exc)
|
|
106
|
+
return 1
|
|
107
|
+
except Exception:
|
|
108
|
+
print("The demo could not finish.")
|
|
109
|
+
if verbose:
|
|
110
|
+
traceback.print_exc()
|
|
111
|
+
return 1
|
|
112
|
+
return 0 if result.ok else 1
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _status(label: str, detail: str, ok: bool) -> str:
|
|
116
|
+
mark = "✓" if ok else "✗"
|
|
117
|
+
return f"{label:<18} {detail:<16} {mark}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _credential(name: str) -> str:
|
|
121
|
+
value = os.environ.get(name)
|
|
122
|
+
if isinstance(value, str) and value.strip() != "":
|
|
123
|
+
return "configured"
|
|
124
|
+
return "not configured"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _sqlite() -> tuple[bool, str]:
|
|
128
|
+
try:
|
|
129
|
+
connection = sqlite3.connect(":memory:")
|
|
130
|
+
except sqlite3.Error:
|
|
131
|
+
return False, ""
|
|
132
|
+
try:
|
|
133
|
+
row = connection.execute("select sqlite_version()").fetchone()
|
|
134
|
+
except sqlite3.Error:
|
|
135
|
+
return False, ""
|
|
136
|
+
finally:
|
|
137
|
+
connection.close()
|
|
138
|
+
if row is None or not isinstance(row[0], str) or row[0] == "":
|
|
139
|
+
return False, ""
|
|
140
|
+
return True, row[0]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Application settings and secret loading."""
|
|
2
|
+
|
|
3
|
+
from agentbuildkit.config.settings import (
|
|
4
|
+
AppSettings,
|
|
5
|
+
CursorSettings,
|
|
6
|
+
GithubSettings,
|
|
7
|
+
load_app_settings,
|
|
8
|
+
load_cursor_settings,
|
|
9
|
+
load_github_settings,
|
|
10
|
+
load_openai_settings,
|
|
11
|
+
redacted_settings,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AppSettings",
|
|
16
|
+
"CursorSettings",
|
|
17
|
+
"GithubSettings",
|
|
18
|
+
"load_app_settings",
|
|
19
|
+
"load_cursor_settings",
|
|
20
|
+
"load_github_settings",
|
|
21
|
+
"load_openai_settings",
|
|
22
|
+
"redacted_settings",
|
|
23
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Load application and OpenAI settings without exposing secret values."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any, TypedDict
|
|
5
|
+
|
|
6
|
+
from agent_framework import SecretString, load_settings
|
|
7
|
+
from agent_framework.openai import OpenAISettings
|
|
8
|
+
|
|
9
|
+
_REDACTED = "**********"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CursorSettings(TypedDict, total=False):
|
|
13
|
+
"""Cursor Cloud Agent settings loaded from the ``CURSOR_`` prefix.
|
|
14
|
+
|
|
15
|
+
``api_key`` stays a ``SecretString``. Callers unwrap it only when building
|
|
16
|
+
the authorization header.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
api_key: SecretString | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GithubSettings(TypedDict, total=False):
|
|
23
|
+
"""GitHub App settings loaded from the ``GITHUB_`` prefix.
|
|
24
|
+
|
|
25
|
+
``private_key`` stays a ``SecretString``. Callers unwrap it only when signing
|
|
26
|
+
an installation token.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
app_id: str | None
|
|
30
|
+
installation_id: str | None
|
|
31
|
+
private_key: SecretString | None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AppSettings(TypedDict, total=False):
|
|
35
|
+
"""Process settings loaded from the ``APP_`` prefix.
|
|
36
|
+
|
|
37
|
+
These values are not secrets. Provider credentials are not loaded here.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
service_name: str | None
|
|
41
|
+
environment: str | None
|
|
42
|
+
enable_console_exporters: bool | None
|
|
43
|
+
otlp_endpoint: str | None
|
|
44
|
+
state_directory: str | None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_app_settings(
|
|
48
|
+
*,
|
|
49
|
+
env_file_path: str | None = None,
|
|
50
|
+
**overrides: Any,
|
|
51
|
+
) -> AppSettings:
|
|
52
|
+
"""Load application settings from overrides, an env file, then the process environment."""
|
|
53
|
+
return load_settings(
|
|
54
|
+
AppSettings,
|
|
55
|
+
env_prefix="APP_",
|
|
56
|
+
env_file_path=env_file_path,
|
|
57
|
+
**overrides,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_cursor_settings(
|
|
62
|
+
*,
|
|
63
|
+
env_file_path: str | None = None,
|
|
64
|
+
**overrides: Any,
|
|
65
|
+
) -> CursorSettings:
|
|
66
|
+
"""Load Cursor API settings. The API key remains a ``SecretString``."""
|
|
67
|
+
return load_settings(
|
|
68
|
+
CursorSettings,
|
|
69
|
+
env_prefix="CURSOR_",
|
|
70
|
+
env_file_path=env_file_path,
|
|
71
|
+
**overrides,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def load_github_settings(
|
|
76
|
+
*,
|
|
77
|
+
env_file_path: str | None = None,
|
|
78
|
+
**overrides: Any,
|
|
79
|
+
) -> GithubSettings:
|
|
80
|
+
"""Load GitHub App settings. The private key remains a ``SecretString``."""
|
|
81
|
+
return load_settings(
|
|
82
|
+
GithubSettings,
|
|
83
|
+
env_prefix="GITHUB_",
|
|
84
|
+
env_file_path=env_file_path,
|
|
85
|
+
**overrides,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def load_openai_settings(
|
|
90
|
+
*,
|
|
91
|
+
env_file_path: str | None = None,
|
|
92
|
+
**overrides: Any,
|
|
93
|
+
) -> OpenAISettings:
|
|
94
|
+
"""Load OpenAI provider settings.
|
|
95
|
+
|
|
96
|
+
A missing API key stays unset so offline tests can run. These settings
|
|
97
|
+
belong to the OpenAI provider. The rest of the harness does not require them.
|
|
98
|
+
"""
|
|
99
|
+
return load_settings(
|
|
100
|
+
OpenAISettings,
|
|
101
|
+
env_prefix="OPENAI_",
|
|
102
|
+
env_file_path=env_file_path,
|
|
103
|
+
**overrides,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def redacted_settings(settings: Mapping[str, Any]) -> dict[str, Any]:
|
|
108
|
+
"""Return a copy safe to log. Secret values are replaced with a mask."""
|
|
109
|
+
redacted: dict[str, Any] = {}
|
|
110
|
+
for key, value in settings.items():
|
|
111
|
+
redacted[str(key)] = _REDACTED if isinstance(value, SecretString) else value
|
|
112
|
+
return redacted
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Offline harness demonstration shipped with the AgentBuildKit package.
|
|
2
|
+
|
|
3
|
+
This sample uses the harness directly. It is not the customer-service example,
|
|
4
|
+
and it does not call a model provider.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from agentbuildkit.demo.scenario import DemoError, DemoResult, run_demo
|
|
8
|
+
|
|
9
|
+
__all__ = ["DemoError", "DemoResult", "run_demo"]
|