dagent-ai 0.2.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.
- dagent/__init__.py +90 -0
- dagent/agent.py +89 -0
- dagent/capabilities/__init__.py +42 -0
- dagent/capabilities/bootstrap.py +28 -0
- dagent/capabilities/boundaries.py +32 -0
- dagent/capabilities/catalog.py +131 -0
- dagent/capabilities/decorator.py +207 -0
- dagent/capabilities/mcp/__init__.py +103 -0
- dagent/capabilities/mcp/config.py +53 -0
- dagent/capabilities/mcp/errors.py +27 -0
- dagent/capabilities/mcp/handlers.py +100 -0
- dagent/capabilities/mcp/manager.py +115 -0
- dagent/capabilities/mcp/schema.py +86 -0
- dagent/capabilities/mcp/server_task.py +96 -0
- dagent/capabilities/providers.py +431 -0
- dagent/capabilities/skills.py +700 -0
- dagent/capabilities/tools/__init__.py +6 -0
- dagent/capabilities/tools/boundary.py +147 -0
- dagent/capabilities/tools/command_tools.py +74 -0
- dagent/capabilities/tools/file_tools.py +110 -0
- dagent/capabilities/tools/registry.py +65 -0
- dagent/capabilities/toolsets.py +233 -0
- dagent/capabilities/workspace.py +27 -0
- dagent/config.py +82 -0
- dagent/dag_builder.py +375 -0
- dagent/harness_runtime/__init__.py +60 -0
- dagent/harness_runtime/artifacts.py +187 -0
- dagent/harness_runtime/capability_executor.py +82 -0
- dagent/harness_runtime/capability_scope.py +16 -0
- dagent/harness_runtime/dag_agent.py +1257 -0
- dagent/harness_runtime/dag_builder.py +544 -0
- dagent/harness_runtime/dag_executor.py +615 -0
- dagent/harness_runtime/feedback_learner.py +39 -0
- dagent/harness_runtime/profiled_agent.py +74 -0
- dagent/harness_runtime/runtime.py +585 -0
- dagent/harness_runtime/runtime_events.py +100 -0
- dagent/harness_runtime/runtime_session.py +123 -0
- dagent/harness_runtime/task_record.py +131 -0
- dagent/harness_runtime/tool_agent.py +699 -0
- dagent/harness_runtime/validator_agent.py +95 -0
- dagent/profiles.py +90 -0
- dagent/providers/__init__.py +15 -0
- dagent/providers/base.py +43 -0
- dagent/providers/mock.py +37 -0
- dagent/providers/openai_compatible.py +161 -0
- dagent/resources/__init__.py +1 -0
- dagent/resources/profiles/__init__.py +1 -0
- dagent/resources/profiles/conversation.md +23 -0
- dagent/resources/profiles/dag_agent.md +107 -0
- dagent/resources/profiles/feedback_learner.md +10 -0
- dagent/resources/profiles/validator_agent.md +43 -0
- dagent/result.py +353 -0
- dagent/review.py +91 -0
- dagent/runner.py +1161 -0
- dagent/schemas/__init__.py +71 -0
- dagent/schemas/artifact.py +30 -0
- dagent/schemas/capability.py +106 -0
- dagent/schemas/common.py +38 -0
- dagent/schemas/dag.py +88 -0
- dagent/schemas/edge.py +12 -0
- dagent/schemas/feedback.py +22 -0
- dagent/schemas/node.py +50 -0
- dagent/schemas/results.py +62 -0
- dagent/schemas/run_trace.py +176 -0
- dagent/schemas/value.py +105 -0
- dagent/state/__init__.py +6 -0
- dagent/state/prompt_builder.py +64 -0
- dagent_ai-0.2.1.dist-info/METADATA +421 -0
- dagent_ai-0.2.1.dist-info/RECORD +72 -0
- dagent_ai-0.2.1.dist-info/WHEEL +5 -0
- dagent_ai-0.2.1.dist-info/licenses/LICENSE +201 -0
- dagent_ai-0.2.1.dist-info/top_level.txt +1 -0
dagent/__init__.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Reviewable DAG agent runtime SDK."""
|
|
2
|
+
|
|
3
|
+
from dagent.agent import AutoAgent, DagAgent, ToolAgent
|
|
4
|
+
from dagent.capabilities import (
|
|
5
|
+
SkillAmbiguousError,
|
|
6
|
+
SkillEntry,
|
|
7
|
+
SkillNotFoundError,
|
|
8
|
+
SkillPermissionError,
|
|
9
|
+
SkillStore,
|
|
10
|
+
SkillStoreError,
|
|
11
|
+
SkillView,
|
|
12
|
+
default_managed_skill_root,
|
|
13
|
+
default_skill_roots,
|
|
14
|
+
)
|
|
15
|
+
from dagent.capabilities.decorator import CapabilityBinding, tool
|
|
16
|
+
from dagent.dag_builder import ArtifactRef, ArtifactValueRef, Dag, FormatRef, InputRef, Node, NodeOutputRef
|
|
17
|
+
from dagent.harness_runtime import ArtifactUpload, CapabilityScope, validate_dag_spec
|
|
18
|
+
from dagent.profiles import AgentProfile, ProfileStore, list_builtin_profiles, load_builtin_profile
|
|
19
|
+
from dagent.providers import Provider
|
|
20
|
+
from dagent.result import RunResult, RunStreamChunk, RunStreamEvent
|
|
21
|
+
from dagent.review import ReviewDecision, ReviewHandle, ReviewLevel
|
|
22
|
+
from dagent.runner import Runner
|
|
23
|
+
from dagent.schemas import (
|
|
24
|
+
Boundary,
|
|
25
|
+
CapabilityDefinition,
|
|
26
|
+
CapabilityInvocation,
|
|
27
|
+
CapabilityPolicy,
|
|
28
|
+
CapabilityResult,
|
|
29
|
+
DAG,
|
|
30
|
+
DAGRun,
|
|
31
|
+
DAGSpec,
|
|
32
|
+
PendingReview,
|
|
33
|
+
RiskLevel,
|
|
34
|
+
RunTrace,
|
|
35
|
+
RuntimeResponse,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__version__ = "0.2.1"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"__version__",
|
|
42
|
+
"AgentProfile",
|
|
43
|
+
"ArtifactRef",
|
|
44
|
+
"ArtifactUpload",
|
|
45
|
+
"ArtifactValueRef",
|
|
46
|
+
"AutoAgent",
|
|
47
|
+
"Boundary",
|
|
48
|
+
"CapabilityBinding",
|
|
49
|
+
"CapabilityDefinition",
|
|
50
|
+
"CapabilityInvocation",
|
|
51
|
+
"CapabilityPolicy",
|
|
52
|
+
"CapabilityResult",
|
|
53
|
+
"CapabilityScope",
|
|
54
|
+
"DAG",
|
|
55
|
+
"DAGRun",
|
|
56
|
+
"DAGSpec",
|
|
57
|
+
"DagAgent",
|
|
58
|
+
"Dag",
|
|
59
|
+
"FormatRef",
|
|
60
|
+
"InputRef",
|
|
61
|
+
"list_builtin_profiles",
|
|
62
|
+
"load_builtin_profile",
|
|
63
|
+
"Node",
|
|
64
|
+
"NodeOutputRef",
|
|
65
|
+
"PendingReview",
|
|
66
|
+
"ProfileStore",
|
|
67
|
+
"Provider",
|
|
68
|
+
"ReviewLevel",
|
|
69
|
+
"ReviewDecision",
|
|
70
|
+
"ReviewHandle",
|
|
71
|
+
"RiskLevel",
|
|
72
|
+
"RunResult",
|
|
73
|
+
"RunStreamChunk",
|
|
74
|
+
"RunStreamEvent",
|
|
75
|
+
"RunTrace",
|
|
76
|
+
"RuntimeResponse",
|
|
77
|
+
"Runner",
|
|
78
|
+
"SkillAmbiguousError",
|
|
79
|
+
"SkillEntry",
|
|
80
|
+
"SkillNotFoundError",
|
|
81
|
+
"SkillPermissionError",
|
|
82
|
+
"SkillStore",
|
|
83
|
+
"SkillStoreError",
|
|
84
|
+
"SkillView",
|
|
85
|
+
"ToolAgent",
|
|
86
|
+
"default_managed_skill_root",
|
|
87
|
+
"default_skill_roots",
|
|
88
|
+
"tool",
|
|
89
|
+
"validate_dag_spec",
|
|
90
|
+
]
|
dagent/agent.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Declarative public agent SDK configurations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
from dagent.capabilities.decorator import CapabilityBinding
|
|
10
|
+
from dagent.profiles import AgentProfile
|
|
11
|
+
from dagent.review import ReviewLevel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CapabilityRef = CapabilityBinding | str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class AutoAgent:
|
|
19
|
+
"""Agent configuration that lets the runtime choose tool-loop or DAG execution."""
|
|
20
|
+
|
|
21
|
+
profile: str | AgentProfile = "conversation"
|
|
22
|
+
planner_profile: str | AgentProfile = "dag_agent"
|
|
23
|
+
name: str | None = None
|
|
24
|
+
max_steps: int = 8
|
|
25
|
+
max_cycles: int = 6
|
|
26
|
+
capabilities: Iterable[CapabilityRef] | None = None
|
|
27
|
+
skills: Iterable[str] | None = None
|
|
28
|
+
review: ReviewLevel = "fast"
|
|
29
|
+
|
|
30
|
+
def __post_init__(self) -> None:
|
|
31
|
+
object.__setattr__(self, "name", self.name or _default_profile_name(self.profile))
|
|
32
|
+
if self.max_steps < 1:
|
|
33
|
+
raise ValueError("max_steps must be at least 1.")
|
|
34
|
+
if self.max_cycles < 1:
|
|
35
|
+
raise ValueError("max_cycles must be at least 1.")
|
|
36
|
+
if self.capabilities is not None:
|
|
37
|
+
object.__setattr__(self, "capabilities", tuple(self.capabilities))
|
|
38
|
+
if self.skills is not None:
|
|
39
|
+
object.__setattr__(self, "skills", tuple(str(skill) for skill in self.skills))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ToolAgent:
|
|
44
|
+
"""Profile-backed tool-loop agent configuration."""
|
|
45
|
+
|
|
46
|
+
profile: str | AgentProfile
|
|
47
|
+
name: str | None = None
|
|
48
|
+
max_steps: int = 8
|
|
49
|
+
capabilities: Iterable[CapabilityRef] | None = None
|
|
50
|
+
skills: Iterable[str] | None = None
|
|
51
|
+
review: ReviewLevel = "fast"
|
|
52
|
+
description: str = ""
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
object.__setattr__(self, "name", self.name or _default_profile_name(self.profile))
|
|
56
|
+
if self.max_steps < 1:
|
|
57
|
+
raise ValueError("max_steps must be at least 1.")
|
|
58
|
+
if self.capabilities is not None:
|
|
59
|
+
object.__setattr__(self, "capabilities", tuple(self.capabilities))
|
|
60
|
+
if self.skills is not None:
|
|
61
|
+
object.__setattr__(self, "skills", tuple(str(skill) for skill in self.skills))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class DagAgent:
|
|
66
|
+
"""Dynamic DAG planner configuration."""
|
|
67
|
+
|
|
68
|
+
planner_profile: str | AgentProfile = "dag_agent"
|
|
69
|
+
name: str | None = None
|
|
70
|
+
max_cycles: int = 6
|
|
71
|
+
capabilities: Iterable[CapabilityRef] | None = None
|
|
72
|
+
skills: Iterable[str] | None = None
|
|
73
|
+
review: ReviewLevel = "fast"
|
|
74
|
+
|
|
75
|
+
def __post_init__(self) -> None:
|
|
76
|
+
object.__setattr__(self, "name", self.name or _default_profile_name(self.planner_profile))
|
|
77
|
+
if self.max_cycles < 1:
|
|
78
|
+
raise ValueError("max_cycles must be at least 1.")
|
|
79
|
+
if self.capabilities is not None:
|
|
80
|
+
object.__setattr__(self, "capabilities", tuple(self.capabilities))
|
|
81
|
+
if self.skills is not None:
|
|
82
|
+
object.__setattr__(self, "skills", tuple(str(skill) for skill in self.skills))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _default_profile_name(profile: str | AgentProfile) -> str:
|
|
86
|
+
if isinstance(profile, AgentProfile):
|
|
87
|
+
return profile.name
|
|
88
|
+
path = Path(profile)
|
|
89
|
+
return path.stem if path.suffix == ".md" else path.name
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Capability registration and providers."""
|
|
2
|
+
|
|
3
|
+
from dagent.capabilities.bootstrap import create_default_capability_catalog
|
|
4
|
+
from dagent.capabilities.catalog import CapabilityCatalog
|
|
5
|
+
from dagent.capabilities.decorator import CapabilityBinding, tool
|
|
6
|
+
from dagent.capabilities.mcp import MCPCapabilityProvider
|
|
7
|
+
from dagent.capabilities.providers import AgentCapabilityProvider, AgentNodeSessionStore
|
|
8
|
+
from dagent.capabilities.skills import (
|
|
9
|
+
SkillAmbiguousError,
|
|
10
|
+
SkillEntry,
|
|
11
|
+
SkillNotFoundError,
|
|
12
|
+
SkillPermissionError,
|
|
13
|
+
SkillStore,
|
|
14
|
+
SkillStoreError,
|
|
15
|
+
SkillView,
|
|
16
|
+
SkillsCapabilityProvider,
|
|
17
|
+
default_managed_skill_root,
|
|
18
|
+
default_skill_roots,
|
|
19
|
+
)
|
|
20
|
+
from dagent.capabilities.toolsets import CapabilityToolAdapter, CapabilityToolset
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AgentCapabilityProvider",
|
|
24
|
+
"AgentNodeSessionStore",
|
|
25
|
+
"CapabilityBinding",
|
|
26
|
+
"CapabilityCatalog",
|
|
27
|
+
"CapabilityToolAdapter",
|
|
28
|
+
"CapabilityToolset",
|
|
29
|
+
"MCPCapabilityProvider",
|
|
30
|
+
"SkillAmbiguousError",
|
|
31
|
+
"SkillEntry",
|
|
32
|
+
"SkillNotFoundError",
|
|
33
|
+
"SkillPermissionError",
|
|
34
|
+
"SkillStore",
|
|
35
|
+
"SkillStoreError",
|
|
36
|
+
"SkillView",
|
|
37
|
+
"SkillsCapabilityProvider",
|
|
38
|
+
"create_default_capability_catalog",
|
|
39
|
+
"default_managed_skill_root",
|
|
40
|
+
"default_skill_roots",
|
|
41
|
+
"tool",
|
|
42
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Default capability catalog assembly."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from dagent.capabilities.catalog import CapabilityCatalog
|
|
8
|
+
from dagent.capabilities.providers import (
|
|
9
|
+
MemoryCapabilityProvider,
|
|
10
|
+
ToolCapabilityProvider,
|
|
11
|
+
)
|
|
12
|
+
from dagent.capabilities.skills import SkillsCapabilityProvider
|
|
13
|
+
from dagent.capabilities.tools.file_tools import create_file_tool_registry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_default_capability_catalog(
|
|
17
|
+
*,
|
|
18
|
+
workspace_root: str | Path = ".",
|
|
19
|
+
skill_roots: list[str | Path] | None = None,
|
|
20
|
+
skills_provider: SkillsCapabilityProvider | None = None,
|
|
21
|
+
) -> CapabilityCatalog:
|
|
22
|
+
if skills_provider is not None and skill_roots is not None:
|
|
23
|
+
raise ValueError("Pass either skill_roots or skills_provider, not both.")
|
|
24
|
+
catalog = CapabilityCatalog(workspace_root=workspace_root)
|
|
25
|
+
ToolCapabilityProvider(create_file_tool_registry()).register_into(catalog)
|
|
26
|
+
MemoryCapabilityProvider().register_into(catalog)
|
|
27
|
+
(skills_provider or SkillsCapabilityProvider(skill_roots)).register_into(catalog)
|
|
28
|
+
return catalog
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Boundary inference for capability invocations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from dagent.schemas import Boundary, CapabilityDefinition
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def infer_capability_boundary(
|
|
11
|
+
definition: CapabilityDefinition | None,
|
|
12
|
+
args: dict[str, Any],
|
|
13
|
+
) -> Boundary:
|
|
14
|
+
if definition is None:
|
|
15
|
+
return Boundary(mode="read_only")
|
|
16
|
+
config = definition.config
|
|
17
|
+
checked_args = {**(config.get("default_args") or {}), **args}
|
|
18
|
+
path_args = tuple(config.get("path_args") or ())
|
|
19
|
+
paths = [_boundary_path_value(checked_args.get(path_arg)) for path_arg in path_args] or ["."]
|
|
20
|
+
action = str(config.get("action") or "read")
|
|
21
|
+
|
|
22
|
+
if action in {"write", "command"}:
|
|
23
|
+
return Boundary(mode="write_limited", allowed_paths=paths)
|
|
24
|
+
return Boundary(mode="read_only", allowed_paths=paths)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _boundary_path_value(value: Any) -> Any:
|
|
28
|
+
if value is None or value == "":
|
|
29
|
+
return "."
|
|
30
|
+
if isinstance(value, dict):
|
|
31
|
+
return value
|
|
32
|
+
return str(value)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Catalog for executable capabilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from dagent.schemas import (
|
|
10
|
+
CapabilityDefinition,
|
|
11
|
+
CapabilityInvocation,
|
|
12
|
+
CapabilityKind,
|
|
13
|
+
CapabilityResult,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
CapabilityHandlerResult = CapabilityResult | Awaitable[CapabilityResult]
|
|
18
|
+
CapabilityHandler = Callable[..., CapabilityHandlerResult]
|
|
19
|
+
ShutdownHook = Callable[[], None]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class CapabilityEntry:
|
|
24
|
+
definition: CapabilityDefinition
|
|
25
|
+
handler: CapabilityHandler
|
|
26
|
+
supports_context: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CapabilityCatalog:
|
|
30
|
+
"""Owns capability definitions and their executable handlers."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, *, workspace_root: str | Path = ".") -> None:
|
|
33
|
+
self.workspace_root = Path(workspace_root).resolve()
|
|
34
|
+
self._entries: dict[str, CapabilityEntry] = {}
|
|
35
|
+
self._shutdown_hooks: list[ShutdownHook] = []
|
|
36
|
+
self._shutdown_complete = False
|
|
37
|
+
|
|
38
|
+
def register(
|
|
39
|
+
self,
|
|
40
|
+
definition: CapabilityDefinition,
|
|
41
|
+
handler: CapabilityHandler,
|
|
42
|
+
*,
|
|
43
|
+
supports_context: bool = False,
|
|
44
|
+
) -> None:
|
|
45
|
+
if definition.id in self._entries:
|
|
46
|
+
raise ValueError(f"Capability '{definition.id}' is already registered.")
|
|
47
|
+
self._entries[definition.id] = CapabilityEntry(
|
|
48
|
+
definition=definition.model_copy(deep=True),
|
|
49
|
+
handler=handler,
|
|
50
|
+
supports_context=supports_context,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def replace(
|
|
54
|
+
self,
|
|
55
|
+
definition: CapabilityDefinition,
|
|
56
|
+
handler: CapabilityHandler,
|
|
57
|
+
*,
|
|
58
|
+
supports_context: bool = False,
|
|
59
|
+
) -> None:
|
|
60
|
+
if definition.id not in self._entries:
|
|
61
|
+
raise KeyError(f"Capability '{definition.id}' is not registered.")
|
|
62
|
+
self._entries[definition.id] = CapabilityEntry(
|
|
63
|
+
definition=definition.model_copy(deep=True),
|
|
64
|
+
handler=handler,
|
|
65
|
+
supports_context=supports_context,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def set_enabled(self, capability_id: str, enabled: bool) -> CapabilityDefinition:
|
|
69
|
+
entry = self._entries.get(capability_id)
|
|
70
|
+
if entry is None:
|
|
71
|
+
raise KeyError(f"Capability '{capability_id}' is not registered.")
|
|
72
|
+
updated = entry.definition.model_copy(update={"enabled": enabled}, deep=True)
|
|
73
|
+
self._entries[capability_id] = CapabilityEntry(
|
|
74
|
+
definition=updated,
|
|
75
|
+
handler=entry.handler,
|
|
76
|
+
supports_context=entry.supports_context,
|
|
77
|
+
)
|
|
78
|
+
return updated
|
|
79
|
+
|
|
80
|
+
def delete(self, capability_id: str) -> None:
|
|
81
|
+
self._entries.pop(capability_id, None)
|
|
82
|
+
|
|
83
|
+
def get(self, capability_id: str) -> CapabilityDefinition | None:
|
|
84
|
+
entry = self._entries.get(capability_id)
|
|
85
|
+
return entry.definition.model_copy(deep=True) if entry is not None else None
|
|
86
|
+
|
|
87
|
+
def get_entry(self, capability_id: str) -> CapabilityEntry | None:
|
|
88
|
+
return self._entries.get(capability_id)
|
|
89
|
+
|
|
90
|
+
def get_by_name(self, name: str, *, kind: CapabilityKind | None = None) -> CapabilityDefinition | None:
|
|
91
|
+
for entry in self._entries.values():
|
|
92
|
+
definition = entry.definition
|
|
93
|
+
if definition.name == name and (kind is None or definition.kind == kind):
|
|
94
|
+
return definition.model_copy(deep=True)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def list(self, *, kind: CapabilityKind | None = None, enabled_only: bool = False) -> list[CapabilityDefinition]:
|
|
98
|
+
definitions = [entry.definition for entry in self._entries.values()]
|
|
99
|
+
if kind is not None:
|
|
100
|
+
definitions = [definition for definition in definitions if definition.kind == kind]
|
|
101
|
+
if enabled_only:
|
|
102
|
+
definitions = [definition for definition in definitions if definition.enabled]
|
|
103
|
+
return sorted(
|
|
104
|
+
[definition.model_copy(deep=True) for definition in definitions],
|
|
105
|
+
key=lambda definition: definition.id,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def names(self, *, kind: CapabilityKind | None = None) -> set[str]:
|
|
109
|
+
return {definition.name for definition in self.list(kind=kind)}
|
|
110
|
+
|
|
111
|
+
def ids(self) -> set[str]:
|
|
112
|
+
return set(self._entries)
|
|
113
|
+
|
|
114
|
+
def add_shutdown_hook(self, hook: ShutdownHook) -> None:
|
|
115
|
+
if hook not in self._shutdown_hooks:
|
|
116
|
+
self._shutdown_hooks.append(hook)
|
|
117
|
+
|
|
118
|
+
def remove_shutdown_hook(self, hook: ShutdownHook) -> None:
|
|
119
|
+
try:
|
|
120
|
+
self._shutdown_hooks.remove(hook)
|
|
121
|
+
except ValueError:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
def shutdown(self) -> None:
|
|
125
|
+
if self._shutdown_complete:
|
|
126
|
+
return
|
|
127
|
+
self._shutdown_complete = True
|
|
128
|
+
hooks = list(self._shutdown_hooks)
|
|
129
|
+
self._shutdown_hooks.clear()
|
|
130
|
+
for hook in hooks:
|
|
131
|
+
hook()
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Public capability decorator for the dagent SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, get_type_hints
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from dagent.capabilities.catalog import CapabilityHandler
|
|
14
|
+
from dagent.schemas import (
|
|
15
|
+
CapabilityDefinition,
|
|
16
|
+
CapabilityInvocation,
|
|
17
|
+
CapabilityPolicy,
|
|
18
|
+
CapabilityResult,
|
|
19
|
+
RiskLevel,
|
|
20
|
+
)
|
|
21
|
+
from dagent.schemas.common import json_schema_for_type
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class CapabilityBinding:
|
|
26
|
+
"""A public SDK binding of a capability definition and executable handler."""
|
|
27
|
+
|
|
28
|
+
definition: CapabilityDefinition
|
|
29
|
+
handler: CapabilityHandler
|
|
30
|
+
supports_context: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def tool(
|
|
34
|
+
fn: Callable[..., Any] | None = None,
|
|
35
|
+
*,
|
|
36
|
+
id: str | None = None,
|
|
37
|
+
name: str | None = None,
|
|
38
|
+
description: str = "",
|
|
39
|
+
risk: RiskLevel = "low",
|
|
40
|
+
requires_review: bool = False,
|
|
41
|
+
sandbox_required: bool = False,
|
|
42
|
+
network: bool = False,
|
|
43
|
+
secrets: list[str] | None = None,
|
|
44
|
+
parameters: dict[str, Any] | None = None,
|
|
45
|
+
config: dict[str, Any] | None = None,
|
|
46
|
+
enabled: bool = True,
|
|
47
|
+
supports_context: bool = False,
|
|
48
|
+
) -> CapabilityBinding | Callable[[Callable[..., Any]], CapabilityBinding]:
|
|
49
|
+
"""Decorate a Python function as a dagent tool capability.
|
|
50
|
+
|
|
51
|
+
This is the public way to expose a Python function as an LLM-callable
|
|
52
|
+
tool. MCP and skill capabilities are registered through
|
|
53
|
+
``Runner.add_mcp_server`` and ``Runner.add_skill_root``.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def decorate(func: Callable[..., Any]) -> CapabilityBinding:
|
|
57
|
+
type_hints = _type_hints_for(func)
|
|
58
|
+
capability_name = name or func.__name__
|
|
59
|
+
capability_id = id or f"tool.{capability_name}"
|
|
60
|
+
definition = CapabilityDefinition(
|
|
61
|
+
id=capability_id,
|
|
62
|
+
name=capability_name,
|
|
63
|
+
kind="tool",
|
|
64
|
+
description=description or inspect.getdoc(func) or "",
|
|
65
|
+
parameters=parameters or _schema_from_signature(func, type_hints),
|
|
66
|
+
output_schema=_output_schema_from_signature(func, type_hints),
|
|
67
|
+
policy=CapabilityPolicy(
|
|
68
|
+
risk=risk,
|
|
69
|
+
requires_review=requires_review,
|
|
70
|
+
sandbox_required=sandbox_required,
|
|
71
|
+
network=network,
|
|
72
|
+
secrets=secrets or [],
|
|
73
|
+
),
|
|
74
|
+
config=config or {},
|
|
75
|
+
enabled=enabled,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def handler(
|
|
79
|
+
invocation: CapabilityInvocation,
|
|
80
|
+
*,
|
|
81
|
+
context: Any = None,
|
|
82
|
+
callbacks: Any = None,
|
|
83
|
+
) -> CapabilityResult:
|
|
84
|
+
try:
|
|
85
|
+
result = _invoke_function(
|
|
86
|
+
func,
|
|
87
|
+
invocation.arguments,
|
|
88
|
+
context=context,
|
|
89
|
+
callbacks=callbacks,
|
|
90
|
+
supports_context=supports_context,
|
|
91
|
+
)
|
|
92
|
+
if inspect.isawaitable(result):
|
|
93
|
+
result = await result
|
|
94
|
+
if isinstance(result, CapabilityResult):
|
|
95
|
+
return _normalize_capability_result(result)
|
|
96
|
+
content, value = _content_and_value_from_result(result)
|
|
97
|
+
return CapabilityResult(
|
|
98
|
+
invocation_id=invocation.invocation_id,
|
|
99
|
+
capability_id=invocation.capability_id,
|
|
100
|
+
kind=invocation.kind,
|
|
101
|
+
status="completed",
|
|
102
|
+
content=content,
|
|
103
|
+
value=value,
|
|
104
|
+
)
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
return CapabilityResult(
|
|
107
|
+
invocation_id=invocation.invocation_id,
|
|
108
|
+
capability_id=invocation.capability_id,
|
|
109
|
+
kind=invocation.kind,
|
|
110
|
+
status="failed",
|
|
111
|
+
error=str(exc),
|
|
112
|
+
stop_reason=type(exc).__name__,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return CapabilityBinding(
|
|
116
|
+
definition=definition,
|
|
117
|
+
handler=handler,
|
|
118
|
+
supports_context=supports_context,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if fn is not None:
|
|
122
|
+
return decorate(fn)
|
|
123
|
+
return decorate
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _invoke_function(
|
|
127
|
+
func: Callable[..., Any],
|
|
128
|
+
arguments: dict[str, Any],
|
|
129
|
+
*,
|
|
130
|
+
context: Any,
|
|
131
|
+
callbacks: Any,
|
|
132
|
+
supports_context: bool,
|
|
133
|
+
) -> Any:
|
|
134
|
+
if supports_context:
|
|
135
|
+
return func(**arguments, context=context, callbacks=callbacks)
|
|
136
|
+
return func(**arguments)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _content_and_value_from_result(result: Any) -> tuple[str, Any]:
|
|
140
|
+
if result is None:
|
|
141
|
+
return "", None
|
|
142
|
+
if isinstance(result, BaseModel):
|
|
143
|
+
value = result.model_dump(mode="json")
|
|
144
|
+
return result.model_dump_json(), value
|
|
145
|
+
if isinstance(result, str):
|
|
146
|
+
return result, result
|
|
147
|
+
if isinstance(result, bytes):
|
|
148
|
+
value = result.decode("utf-8", errors="replace")
|
|
149
|
+
return value, value
|
|
150
|
+
if isinstance(result, tuple):
|
|
151
|
+
value = list(result)
|
|
152
|
+
return json.dumps(value, ensure_ascii=False), value
|
|
153
|
+
if isinstance(result, (dict, list, bool, int, float)):
|
|
154
|
+
return json.dumps(result, ensure_ascii=False), result
|
|
155
|
+
value = str(result)
|
|
156
|
+
return value, value
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _normalize_capability_result(result: CapabilityResult) -> CapabilityResult:
|
|
160
|
+
if result.status == "completed" and result.value is None:
|
|
161
|
+
return result.model_copy(update={"value": result.content})
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _schema_from_signature(func: Callable[..., Any], type_hints: dict[str, Any]) -> dict[str, Any]:
|
|
166
|
+
signature = inspect.signature(func)
|
|
167
|
+
properties: dict[str, Any] = {}
|
|
168
|
+
required: list[str] = []
|
|
169
|
+
for parameter in signature.parameters.values():
|
|
170
|
+
if parameter.kind in {
|
|
171
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
172
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
173
|
+
}:
|
|
174
|
+
raise ValueError(f"Cannot infer schema for variadic parameter '{parameter.name}'.")
|
|
175
|
+
if parameter.name in {"context", "callbacks"}:
|
|
176
|
+
continue
|
|
177
|
+
schema = _schema_for_annotation(type_hints.get(parameter.name, parameter.annotation))
|
|
178
|
+
if parameter.default is inspect.Parameter.empty:
|
|
179
|
+
required.append(parameter.name)
|
|
180
|
+
else:
|
|
181
|
+
schema["default"] = parameter.default
|
|
182
|
+
properties[parameter.name] = schema
|
|
183
|
+
output: dict[str, Any] = {
|
|
184
|
+
"type": "object",
|
|
185
|
+
"properties": properties,
|
|
186
|
+
}
|
|
187
|
+
if required:
|
|
188
|
+
output["required"] = required
|
|
189
|
+
return output
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _output_schema_from_signature(func: Callable[..., Any], type_hints: dict[str, Any]) -> dict[str, Any]:
|
|
193
|
+
annotation = type_hints.get("return", inspect.signature(func).return_annotation)
|
|
194
|
+
return json_schema_for_type(annotation)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _type_hints_for(func: Callable[..., Any]) -> dict[str, Any]:
|
|
198
|
+
try:
|
|
199
|
+
return get_type_hints(func, include_extras=True)
|
|
200
|
+
except (NameError, TypeError, AttributeError):
|
|
201
|
+
return {}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _schema_for_annotation(annotation: Any) -> dict[str, Any]:
|
|
205
|
+
if annotation is inspect.Parameter.empty:
|
|
206
|
+
return {"type": "string"}
|
|
207
|
+
return json_schema_for_type(annotation) or {"type": "string"}
|