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 ADDED
@@ -0,0 +1,6 @@
1
+ """Public API for Moiryx."""
2
+
3
+ from moiryx.agent import Agent
4
+ from moiryx.tools import tool
5
+
6
+ __all__ = ["Agent", "tool"]
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"]