moiryx 0.1.0a1__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.
- moiryx/__init__.py +6 -0
- moiryx/agent.py +200 -0
- moiryx/agent_spec.py +154 -0
- moiryx/config.py +354 -0
- moiryx/errors.py +210 -0
- moiryx/generation.py +105 -0
- moiryx/messages.py +75 -0
- moiryx/model_registry.py +62 -0
- moiryx/models.py +108 -0
- moiryx/observability.py +110 -0
- moiryx/output/__init__.py +16 -0
- moiryx/output/final_tool.py +35 -0
- moiryx/output/loader.py +37 -0
- moiryx/output/repair.py +77 -0
- moiryx/providers/__init__.py +54 -0
- moiryx/providers/azure_foundry.py +62 -0
- moiryx/providers/azure_openai.py +74 -0
- moiryx/providers/fake.py +80 -0
- moiryx/providers/openai_compatible.py +391 -0
- moiryx/providers/openrouter.py +52 -0
- moiryx/providers/registry.py +114 -0
- moiryx/providers/vertex_ai.py +378 -0
- moiryx/py.typed +1 -0
- moiryx/redaction.py +108 -0
- moiryx/retry.py +181 -0
- moiryx/runtime.py +724 -0
- moiryx/tools/__init__.py +51 -0
- moiryx/tools/builtin/__init__.py +38 -0
- moiryx/tools/builtin/filesystem.py +432 -0
- moiryx/tools/builtin/shell.py +153 -0
- moiryx/tools/builtin/workspace.py +63 -0
- moiryx/tools/decorator.py +21 -0
- moiryx/tools/definition.py +89 -0
- moiryx/tools/executor.py +108 -0
- moiryx/tools/preflight.py +465 -0
- moiryx/tools/registry.py +85 -0
- moiryx/tools/serialization.py +88 -0
- moiryx-0.1.0a1.dist-info/METADATA +245 -0
- moiryx-0.1.0a1.dist-info/RECORD +42 -0
- moiryx-0.1.0a1.dist-info/WHEEL +5 -0
- moiryx-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- moiryx-0.1.0a1.dist-info/top_level.txt +1 -0
moiryx/__init__.py
ADDED
moiryx/agent.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Eagerly validated public Agent facade."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from importlib import import_module
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from moiryx.agent_spec import AgentSpec, load_agent_spec
|
|
12
|
+
from moiryx.config import CapabilityOverrides, RuntimeConfig, load_config
|
|
13
|
+
from moiryx.errors import ConfigurationError, ProviderCapabilityError
|
|
14
|
+
from moiryx.generation import merge_generation_options
|
|
15
|
+
from moiryx.model_registry import ModelRegistry, ResolvedModel
|
|
16
|
+
from moiryx.models import GenerationOptions, ProviderCapabilities, ToolDefinition
|
|
17
|
+
from moiryx.observability import RunObserver
|
|
18
|
+
from moiryx.providers import ProviderAdapter, ProviderRegistry
|
|
19
|
+
from moiryx.redaction import collect_secret_values
|
|
20
|
+
from moiryx.runtime import execute_structured_agent, execute_text_agent
|
|
21
|
+
from moiryx.tools import GLOBAL_TOOL_REGISTRY, build_builtin_tool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class _PreparedAgent:
|
|
26
|
+
spec: AgentSpec
|
|
27
|
+
model: ResolvedModel
|
|
28
|
+
provider: ProviderAdapter
|
|
29
|
+
tools: tuple[ToolDefinition, ...]
|
|
30
|
+
capabilities: ProviderCapabilities
|
|
31
|
+
generation: GenerationOptions
|
|
32
|
+
runtime: RuntimeConfig
|
|
33
|
+
observer: RunObserver
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _import_tool_modules(module_names: list[str]) -> None:
|
|
37
|
+
for module_name in module_names:
|
|
38
|
+
try:
|
|
39
|
+
import_module(module_name)
|
|
40
|
+
except Exception as error:
|
|
41
|
+
raise ConfigurationError(
|
|
42
|
+
f"Could not import tool module '{module_name}' ({type(error).__name__})"
|
|
43
|
+
) from error
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _effective_capabilities(
|
|
47
|
+
adapter: ProviderCapabilities,
|
|
48
|
+
overrides: CapabilityOverrides,
|
|
49
|
+
) -> ProviderCapabilities:
|
|
50
|
+
return ProviderCapabilities(
|
|
51
|
+
tool_calling=(
|
|
52
|
+
adapter.tool_calling
|
|
53
|
+
if overrides.tool_calling is None
|
|
54
|
+
else overrides.tool_calling
|
|
55
|
+
),
|
|
56
|
+
native_structured_output=(
|
|
57
|
+
adapter.native_structured_output
|
|
58
|
+
if overrides.native_structured_output is None
|
|
59
|
+
else overrides.native_structured_output
|
|
60
|
+
),
|
|
61
|
+
parallel_tool_calls=(
|
|
62
|
+
adapter.parallel_tool_calls
|
|
63
|
+
if overrides.parallel_tool_calls is None
|
|
64
|
+
else overrides.parallel_tool_calls
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _validate_capabilities(
|
|
70
|
+
*,
|
|
71
|
+
spec: AgentSpec,
|
|
72
|
+
model: ResolvedModel,
|
|
73
|
+
capabilities: ProviderCapabilities,
|
|
74
|
+
) -> None:
|
|
75
|
+
if spec.tool_names and not capabilities.tool_calling:
|
|
76
|
+
raise ProviderCapabilityError(
|
|
77
|
+
"Agent tools require provider tool-calling capability",
|
|
78
|
+
agent_name=spec.name,
|
|
79
|
+
model_alias=model.alias,
|
|
80
|
+
provider_name=model.provider,
|
|
81
|
+
model_id=model.model,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if spec.output_model is None:
|
|
85
|
+
return
|
|
86
|
+
supports_output = capabilities.tool_calling or (
|
|
87
|
+
not spec.tool_names and capabilities.native_structured_output
|
|
88
|
+
)
|
|
89
|
+
if not supports_output:
|
|
90
|
+
raise ProviderCapabilityError(
|
|
91
|
+
"Structured output requires tool calling or guaranteed native "
|
|
92
|
+
"structured output without user tools",
|
|
93
|
+
agent_name=spec.name,
|
|
94
|
+
model_alias=model.alias,
|
|
95
|
+
provider_name=model.provider,
|
|
96
|
+
model_id=model.model,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Agent:
|
|
101
|
+
"""An immutable, eagerly resolved agent definition ready for execution."""
|
|
102
|
+
|
|
103
|
+
__slots__ = ("_prepared", "_provider_registry")
|
|
104
|
+
|
|
105
|
+
def __init__(self, agent_file: str | Path) -> None:
|
|
106
|
+
config = load_config()
|
|
107
|
+
_import_tool_modules(config.tool_modules)
|
|
108
|
+
spec = load_agent_spec(
|
|
109
|
+
agent_file,
|
|
110
|
+
default_max_steps=config.runtime.default_max_steps,
|
|
111
|
+
)
|
|
112
|
+
model = ModelRegistry(config).resolve(spec.model_alias)
|
|
113
|
+
tools = tuple(
|
|
114
|
+
build_builtin_tool(name, config.runtime)
|
|
115
|
+
or GLOBAL_TOOL_REGISTRY.resolve(name)
|
|
116
|
+
for name in spec.tool_names
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
provider_registry = ProviderRegistry(config)
|
|
120
|
+
provider = provider_registry.resolve(model.provider)
|
|
121
|
+
try:
|
|
122
|
+
adapter_capabilities = provider.capabilities
|
|
123
|
+
except (AttributeError, TypeError) as error:
|
|
124
|
+
raise ConfigurationError(
|
|
125
|
+
"Provider adapter does not expose valid capabilities",
|
|
126
|
+
provider_name=model.provider,
|
|
127
|
+
) from error
|
|
128
|
+
if not isinstance(adapter_capabilities, ProviderCapabilities):
|
|
129
|
+
raise ConfigurationError(
|
|
130
|
+
"Provider adapter returned invalid capabilities",
|
|
131
|
+
provider_name=model.provider,
|
|
132
|
+
)
|
|
133
|
+
provider_config = config.providers[model.provider]
|
|
134
|
+
capabilities = _effective_capabilities(
|
|
135
|
+
adapter_capabilities,
|
|
136
|
+
provider_config.capabilities,
|
|
137
|
+
)
|
|
138
|
+
_validate_capabilities(
|
|
139
|
+
spec=spec,
|
|
140
|
+
model=model,
|
|
141
|
+
capabilities=capabilities,
|
|
142
|
+
)
|
|
143
|
+
generation = merge_generation_options(
|
|
144
|
+
config.runtime.generation,
|
|
145
|
+
model.generation,
|
|
146
|
+
spec.generation,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
self._provider_registry = provider_registry
|
|
150
|
+
self._prepared = _PreparedAgent(
|
|
151
|
+
spec=spec,
|
|
152
|
+
model=model,
|
|
153
|
+
provider=provider,
|
|
154
|
+
tools=tools,
|
|
155
|
+
capabilities=capabilities,
|
|
156
|
+
generation=generation,
|
|
157
|
+
runtime=config.runtime,
|
|
158
|
+
observer=RunObserver(
|
|
159
|
+
config.logging,
|
|
160
|
+
secrets=collect_secret_values(config),
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
async def __call__(self, prompt: str) -> str | BaseModel:
|
|
165
|
+
"""Execute one isolated run of this eagerly prepared agent."""
|
|
166
|
+
if self._prepared.spec.output_model is not None:
|
|
167
|
+
return await execute_structured_agent(
|
|
168
|
+
provider=self._prepared.provider,
|
|
169
|
+
spec=self._prepared.spec,
|
|
170
|
+
model=self._prepared.model,
|
|
171
|
+
tools=self._prepared.tools,
|
|
172
|
+
capabilities=self._prepared.capabilities,
|
|
173
|
+
generation=self._prepared.generation,
|
|
174
|
+
runtime=self._prepared.runtime,
|
|
175
|
+
prompt=prompt,
|
|
176
|
+
observer=self._prepared.observer,
|
|
177
|
+
)
|
|
178
|
+
return await execute_text_agent(
|
|
179
|
+
provider=self._prepared.provider,
|
|
180
|
+
spec=self._prepared.spec,
|
|
181
|
+
model=self._prepared.model,
|
|
182
|
+
tools=self._prepared.tools,
|
|
183
|
+
generation=self._prepared.generation,
|
|
184
|
+
runtime=self._prepared.runtime,
|
|
185
|
+
prompt=prompt,
|
|
186
|
+
observer=self._prepared.observer,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
async def aclose(self) -> None:
|
|
190
|
+
"""Release the provider client when this agent is no longer needed."""
|
|
191
|
+
await self._provider_registry.close()
|
|
192
|
+
|
|
193
|
+
async def __aenter__(self) -> Agent:
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
async def __aexit__(self, *exc_info: object) -> None:
|
|
197
|
+
await self.aclose()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
__all__ = ["Agent"]
|
moiryx/agent_spec.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Parsing and validation of Markdown agent definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
12
|
+
|
|
13
|
+
from moiryx.errors import AgentDefinitionError, ConfigurationError
|
|
14
|
+
from moiryx.generation import merge_generation_options
|
|
15
|
+
from moiryx.models import GenerationOptions
|
|
16
|
+
from moiryx.output import load_output_model
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _AgentFrontmatter(BaseModel):
|
|
20
|
+
model_config = ConfigDict(extra="forbid", frozen=True, strict=True)
|
|
21
|
+
|
|
22
|
+
model: Annotated[str, Field(min_length=1)]
|
|
23
|
+
name: Annotated[str, Field(min_length=1)] | None = None
|
|
24
|
+
tools: list[str] = Field(default_factory=list)
|
|
25
|
+
output: Annotated[str, Field(min_length=1)] | None = None
|
|
26
|
+
max_steps: Annotated[int, Field(gt=0)] | None = None
|
|
27
|
+
generation: dict[str, object] = Field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class AgentSpec:
|
|
32
|
+
"""Validated, provider-independent definition of an agent."""
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
model_alias: str
|
|
36
|
+
instructions: str
|
|
37
|
+
tool_names: tuple[str, ...]
|
|
38
|
+
output_model: type[BaseModel] | None
|
|
39
|
+
max_steps: int
|
|
40
|
+
generation: GenerationOptions
|
|
41
|
+
source_path: Path = field(repr=False, compare=False)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _split_frontmatter(text: str, *, agent_path: Path) -> tuple[object, str]:
|
|
45
|
+
lines = text.splitlines(keepends=True)
|
|
46
|
+
if not lines or lines[0].strip() != "---":
|
|
47
|
+
return {}, text
|
|
48
|
+
|
|
49
|
+
closing_index = next(
|
|
50
|
+
(
|
|
51
|
+
index
|
|
52
|
+
for index, line in enumerate(lines[1:], start=1)
|
|
53
|
+
if line.strip() == "---"
|
|
54
|
+
),
|
|
55
|
+
None,
|
|
56
|
+
)
|
|
57
|
+
if closing_index is None:
|
|
58
|
+
raise AgentDefinitionError(
|
|
59
|
+
f"Agent definition '{agent_path}' has unclosed YAML frontmatter"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
frontmatter_text = "".join(lines[1:closing_index])
|
|
63
|
+
body = "".join(lines[closing_index + 1 :])
|
|
64
|
+
try:
|
|
65
|
+
frontmatter: object = yaml.safe_load(frontmatter_text)
|
|
66
|
+
except yaml.YAMLError as error:
|
|
67
|
+
mark = getattr(error, "problem_mark", None)
|
|
68
|
+
location = (
|
|
69
|
+
f" at line {mark.line + 2}, column {mark.column + 1}"
|
|
70
|
+
if mark is not None
|
|
71
|
+
else ""
|
|
72
|
+
)
|
|
73
|
+
raise AgentDefinitionError(
|
|
74
|
+
f"Invalid YAML frontmatter in agent '{agent_path}'{location}"
|
|
75
|
+
) from error
|
|
76
|
+
|
|
77
|
+
return ({} if frontmatter is None else frontmatter), body
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _validation_summary(error: ValidationError) -> str:
|
|
81
|
+
issues: list[str] = []
|
|
82
|
+
for item in error.errors(
|
|
83
|
+
include_url=False,
|
|
84
|
+
include_context=False,
|
|
85
|
+
include_input=False,
|
|
86
|
+
):
|
|
87
|
+
location = ".".join(str(part) for part in item["loc"])
|
|
88
|
+
prefix = f"{location}: " if location else ""
|
|
89
|
+
issues.append(f"{prefix}{item['msg']}")
|
|
90
|
+
return "; ".join(issues)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_agent_spec(
|
|
94
|
+
agent_file: str | Path,
|
|
95
|
+
*,
|
|
96
|
+
default_max_steps: int = 20,
|
|
97
|
+
) -> AgentSpec:
|
|
98
|
+
"""Read and fully validate one Markdown agent definition."""
|
|
99
|
+
agent_path = Path(agent_file)
|
|
100
|
+
if (
|
|
101
|
+
isinstance(default_max_steps, bool)
|
|
102
|
+
or not isinstance(default_max_steps, int)
|
|
103
|
+
or default_max_steps <= 0
|
|
104
|
+
):
|
|
105
|
+
raise AgentDefinitionError(
|
|
106
|
+
"default_max_steps must be a positive integer",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
with agent_path.open("r", encoding="utf-8", newline="") as source:
|
|
111
|
+
text = source.read()
|
|
112
|
+
except (OSError, UnicodeError) as error:
|
|
113
|
+
raise AgentDefinitionError(
|
|
114
|
+
f"Could not read agent definition '{agent_path}': {type(error).__name__}"
|
|
115
|
+
) from error
|
|
116
|
+
|
|
117
|
+
raw_frontmatter, instructions = _split_frontmatter(text, agent_path=agent_path)
|
|
118
|
+
if not isinstance(raw_frontmatter, Mapping):
|
|
119
|
+
raise AgentDefinitionError(
|
|
120
|
+
f"YAML frontmatter in agent '{agent_path}' must be a mapping"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
frontmatter = _AgentFrontmatter.model_validate(raw_frontmatter)
|
|
125
|
+
except ValidationError as error:
|
|
126
|
+
raise AgentDefinitionError(
|
|
127
|
+
f"Invalid agent definition '{agent_path}': {_validation_summary(error)}"
|
|
128
|
+
) from error
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
generation = merge_generation_options(agent=frontmatter.generation)
|
|
132
|
+
except ConfigurationError as error:
|
|
133
|
+
raise AgentDefinitionError(
|
|
134
|
+
f"Invalid agent definition '{agent_path}': {error.message}"
|
|
135
|
+
) from error
|
|
136
|
+
|
|
137
|
+
output_model = (
|
|
138
|
+
load_output_model(frontmatter.output, agent_path=agent_path)
|
|
139
|
+
if frontmatter.output is not None
|
|
140
|
+
else None
|
|
141
|
+
)
|
|
142
|
+
return AgentSpec(
|
|
143
|
+
name=frontmatter.name or agent_path.stem,
|
|
144
|
+
model_alias=frontmatter.model,
|
|
145
|
+
instructions=instructions,
|
|
146
|
+
tool_names=tuple(frontmatter.tools),
|
|
147
|
+
output_model=output_model,
|
|
148
|
+
max_steps=frontmatter.max_steps or default_max_steps,
|
|
149
|
+
generation=generation,
|
|
150
|
+
source_path=agent_path,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
__all__ = ["AgentSpec", "load_agent_spec"]
|