petfishframework 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.
- petfishframework/__init__.py +33 -0
- petfishframework/config.py +142 -0
- petfishframework/core/__init__.py +91 -0
- petfishframework/core/agent.py +207 -0
- petfishframework/core/compiled.py +89 -0
- petfishframework/core/contracts.py +190 -0
- petfishframework/core/conversation.py +51 -0
- petfishframework/core/environment.py +260 -0
- petfishframework/core/events.py +79 -0
- petfishframework/core/session.py +182 -0
- petfishframework/core/structured.py +202 -0
- petfishframework/core/types.py +192 -0
- petfishframework/mcp/__init__.py +19 -0
- petfishframework/mcp/client.py +96 -0
- petfishframework/mcp/server.py +14 -0
- petfishframework/mcp/stdio_transport.py +218 -0
- petfishframework/mcp/wrapper.py +43 -0
- petfishframework/models/__init__.py +14 -0
- petfishframework/models/anthropic.py +194 -0
- petfishframework/models/fake.py +202 -0
- petfishframework/models/openai.py +178 -0
- petfishframework/observability/__init__.py +10 -0
- petfishframework/observability/sinks.py +23 -0
- petfishframework/permissions/__init__.py +32 -0
- petfishframework/permissions/model.py +110 -0
- petfishframework/reasoning/__init__.py +13 -0
- petfishframework/reasoning/lats.py +222 -0
- petfishframework/reasoning/llm_plus_p.py +202 -0
- petfishframework/reasoning/react.py +176 -0
- petfishframework/reliability/__init__.py +78 -0
- petfishframework/reliability/cost.py +50 -0
- petfishframework/reliability/cost_report.py +101 -0
- petfishframework/reliability/pass_at_k.py +232 -0
- petfishframework/reliability/replay.py +190 -0
- petfishframework/reliability/retry.py +224 -0
- petfishframework/reliability/timeout.py +55 -0
- petfishframework/retrieval/__init__.py +12 -0
- petfishframework/retrieval/adaptive.py +137 -0
- petfishframework/retrieval/crag.py +135 -0
- petfishframework/retrieval/memory_store.py +72 -0
- petfishframework/tools/__init__.py +12 -0
- petfishframework/tools/agent_tool.py +47 -0
- petfishframework/tools/base.py +75 -0
- petfishframework/tools/calculator.py +84 -0
- petfishframework/tools/path_planner.py +90 -0
- petfishframework/tools/registry.py +158 -0
- petfishframework/tools/word_sorter.py +41 -0
- petfishframework-0.1.0.dist-info/METADATA +151 -0
- petfishframework-0.1.0.dist-info/RECORD +51 -0
- petfishframework-0.1.0.dist-info/WHEEL +4 -0
- petfishframework-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""petfishFramework — a general AI Agent framework.
|
|
2
|
+
|
|
3
|
+
Model-agnostic agent framework with pluggable reasoning strategies,
|
|
4
|
+
MCP-first tool contracts, and structural reliability.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from .config import FrameworkConfig
|
|
9
|
+
from .core.agent import Agent
|
|
10
|
+
from .core.contracts import Tool
|
|
11
|
+
from .core.types import Budget, BudgetExceeded, Result, Task
|
|
12
|
+
from .permissions.model import DecisionEffect
|
|
13
|
+
from .reasoning import LATS, LLMPlusP, ReAct
|
|
14
|
+
from .reliability.replay import ReplayMode
|
|
15
|
+
from .tools.base import BaseTool
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Agent",
|
|
21
|
+
"BaseTool",
|
|
22
|
+
"Budget",
|
|
23
|
+
"BudgetExceeded",
|
|
24
|
+
"DecisionEffect",
|
|
25
|
+
"FrameworkConfig",
|
|
26
|
+
"LATS",
|
|
27
|
+
"LLMPlusP",
|
|
28
|
+
"ReAct",
|
|
29
|
+
"ReplayMode",
|
|
30
|
+
"Result",
|
|
31
|
+
"Task",
|
|
32
|
+
"Tool",
|
|
33
|
+
]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Framework-wide configuration (M2 gap).
|
|
2
|
+
|
|
3
|
+
Provides a typed, environment-aware config object with safe defaults and
|
|
4
|
+
factories for env-var / dict (YAML/JSON) loading.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from petfishframework.core.types import Budget
|
|
13
|
+
from petfishframework.reliability.retry import RetryPolicy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _env_str(key: str, default: str) -> str:
|
|
17
|
+
return os.environ.get(key, default)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _env_float(key: str, default: float) -> float:
|
|
21
|
+
value = os.environ.get(key)
|
|
22
|
+
if value is None:
|
|
23
|
+
return default
|
|
24
|
+
return float(value)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _env_float_or_none(key: str) -> float | None:
|
|
28
|
+
value = os.environ.get(key)
|
|
29
|
+
if value is None:
|
|
30
|
+
return None
|
|
31
|
+
return float(value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _env_int_or_none(key: str) -> int | None:
|
|
35
|
+
value = os.environ.get(key)
|
|
36
|
+
if value is None:
|
|
37
|
+
return None
|
|
38
|
+
return int(value)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _env_bool(key: str, default: bool) -> bool:
|
|
42
|
+
value = os.environ.get(key)
|
|
43
|
+
if value is None:
|
|
44
|
+
return default
|
|
45
|
+
return value.lower() not in {"0", "false", "no", "off", ""}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class FrameworkConfig:
|
|
50
|
+
"""Top-level configuration for petfishFramework.
|
|
51
|
+
|
|
52
|
+
Defaults are deliberately conservative: deterministic agents via
|
|
53
|
+
temperature=0.0, no token cap, no budget limits, and a 30s global
|
|
54
|
+
operation timeout.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
default_model: str = "gpt-4o"
|
|
58
|
+
default_temperature: float = 0.0
|
|
59
|
+
default_max_tokens: int | None = None
|
|
60
|
+
default_budget: Budget = field(default_factory=Budget)
|
|
61
|
+
openai_api_key: str | None = None
|
|
62
|
+
anthropic_api_key: str | None = None
|
|
63
|
+
retry_policy: RetryPolicy | None = None
|
|
64
|
+
timeout_s: float = 30.0
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_env(cls) -> FrameworkConfig:
|
|
68
|
+
"""Build a config from environment variables.
|
|
69
|
+
|
|
70
|
+
Supported variables:
|
|
71
|
+
- PETFISH_DEFAULT_MODEL
|
|
72
|
+
- PETFISH_DEFAULT_TEMPERATURE
|
|
73
|
+
- PETFISH_DEFAULT_MAX_TOKENS
|
|
74
|
+
- PETFISH_MAX_TOKENS / PETFISH_MAX_COST_USD / PETFISH_MAX_STEPS /
|
|
75
|
+
PETFISH_MAX_TOOL_CALLS (populate default_budget)
|
|
76
|
+
- OPENAI_API_KEY / ANTHROPIC_API_KEY
|
|
77
|
+
- PETFISH_TIMEOUT_S
|
|
78
|
+
- PETFISH_RETRY_* (max_retries, initial_delay, backoff_factor,
|
|
79
|
+
max_delay, jitter) populate retry_policy when any is present.
|
|
80
|
+
"""
|
|
81
|
+
default_budget = Budget(
|
|
82
|
+
max_tokens=_env_int_or_none("PETFISH_MAX_TOKENS"),
|
|
83
|
+
max_cost_usd=_env_float_or_none("PETFISH_MAX_COST_USD"),
|
|
84
|
+
max_steps=_env_int_or_none("PETFISH_MAX_STEPS"),
|
|
85
|
+
max_tool_calls=_env_int_or_none("PETFISH_MAX_TOOL_CALLS"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
retry_policy = None
|
|
89
|
+
if any(
|
|
90
|
+
os.environ.get(f"PETFISH_RETRY_{name}") is not None
|
|
91
|
+
for name in (
|
|
92
|
+
"MAX_RETRIES",
|
|
93
|
+
"INITIAL_DELAY",
|
|
94
|
+
"BACKOFF_FACTOR",
|
|
95
|
+
"MAX_DELAY",
|
|
96
|
+
"JITTER",
|
|
97
|
+
)
|
|
98
|
+
):
|
|
99
|
+
retry_policy = RetryPolicy(
|
|
100
|
+
max_retries=_env_int_or_none("PETFISH_RETRY_MAX_RETRIES") or 3,
|
|
101
|
+
initial_delay=_env_float("PETFISH_RETRY_INITIAL_DELAY", 1.0),
|
|
102
|
+
backoff_factor=_env_float("PETFISH_RETRY_BACKOFF_FACTOR", 2.0),
|
|
103
|
+
max_delay=_env_float("PETFISH_RETRY_MAX_DELAY", 60.0),
|
|
104
|
+
jitter=_env_bool("PETFISH_RETRY_JITTER", True),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return cls(
|
|
108
|
+
default_model=_env_str("PETFISH_DEFAULT_MODEL", "gpt-4o"),
|
|
109
|
+
default_temperature=_env_float("PETFISH_DEFAULT_TEMPERATURE", 0.0),
|
|
110
|
+
default_max_tokens=_env_int_or_none("PETFISH_DEFAULT_MAX_TOKENS"),
|
|
111
|
+
default_budget=default_budget,
|
|
112
|
+
openai_api_key=os.environ.get("OPENAI_API_KEY"),
|
|
113
|
+
anthropic_api_key=os.environ.get("ANTHROPIC_API_KEY"),
|
|
114
|
+
retry_policy=retry_policy,
|
|
115
|
+
timeout_s=_env_float("PETFISH_TIMEOUT_S", 30.0),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_dict(cls, data: dict[str, Any]) -> FrameworkConfig:
|
|
120
|
+
"""Build a config from a plain dict, e.g. loaded from YAML or JSON."""
|
|
121
|
+
budget_data = data.get("default_budget")
|
|
122
|
+
if isinstance(budget_data, dict):
|
|
123
|
+
default_budget = Budget(**budget_data)
|
|
124
|
+
else:
|
|
125
|
+
default_budget = Budget()
|
|
126
|
+
|
|
127
|
+
retry_data = data.get("retry_policy")
|
|
128
|
+
if isinstance(retry_data, dict):
|
|
129
|
+
retry_policy = RetryPolicy(**retry_data)
|
|
130
|
+
else:
|
|
131
|
+
retry_policy = None
|
|
132
|
+
|
|
133
|
+
return cls(
|
|
134
|
+
default_model=data.get("default_model", "gpt-4o"),
|
|
135
|
+
default_temperature=data.get("default_temperature", 0.0),
|
|
136
|
+
default_max_tokens=data.get("default_max_tokens", None),
|
|
137
|
+
default_budget=default_budget,
|
|
138
|
+
openai_api_key=data.get("openai_api_key"),
|
|
139
|
+
anthropic_api_key=data.get("anthropic_api_key"),
|
|
140
|
+
retry_policy=retry_policy,
|
|
141
|
+
timeout_s=data.get("timeout_s", 30.0),
|
|
142
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Core package — thin contracts + driving loop + event stream (decision 5).
|
|
2
|
+
|
|
3
|
+
Everything here depends only on stdlib + core/types. No concrete
|
|
4
|
+
strategy/adapter/tool implementations. Users can vendor core/ + one
|
|
5
|
+
model adapter + ReAct + one tool and get a working agent.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .compiled import (
|
|
10
|
+
CompiledContext,
|
|
11
|
+
EvidenceBundle,
|
|
12
|
+
MemorySlice,
|
|
13
|
+
OutputContract,
|
|
14
|
+
TaskSpec,
|
|
15
|
+
)
|
|
16
|
+
from .contracts import (
|
|
17
|
+
Clearance,
|
|
18
|
+
Environment,
|
|
19
|
+
MemoryView,
|
|
20
|
+
ModelAdapter,
|
|
21
|
+
ReasoningStrategy,
|
|
22
|
+
Retriever,
|
|
23
|
+
RiskLevel,
|
|
24
|
+
RunContext,
|
|
25
|
+
Tool,
|
|
26
|
+
)
|
|
27
|
+
from .conversation import ConversationStore, InMemoryConversationStore
|
|
28
|
+
from .events import Event, EventEmitter
|
|
29
|
+
from .structured import StructuredResult, parse_json, parse_structured
|
|
30
|
+
from .types import (
|
|
31
|
+
Budget,
|
|
32
|
+
BudgetExceeded,
|
|
33
|
+
Message,
|
|
34
|
+
ModelRequest,
|
|
35
|
+
ModelResponse,
|
|
36
|
+
Result,
|
|
37
|
+
Role,
|
|
38
|
+
Snippet,
|
|
39
|
+
Step,
|
|
40
|
+
Task,
|
|
41
|
+
ToolCall,
|
|
42
|
+
ToolRef,
|
|
43
|
+
ToolResult,
|
|
44
|
+
Trajectory,
|
|
45
|
+
Usage,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
# types
|
|
50
|
+
"Budget",
|
|
51
|
+
"BudgetExceeded",
|
|
52
|
+
"Message",
|
|
53
|
+
"ModelRequest",
|
|
54
|
+
"ModelResponse",
|
|
55
|
+
"Result",
|
|
56
|
+
"Role",
|
|
57
|
+
"Step",
|
|
58
|
+
"Snippet",
|
|
59
|
+
"Task",
|
|
60
|
+
"ToolCall",
|
|
61
|
+
"ToolRef",
|
|
62
|
+
"ToolResult",
|
|
63
|
+
"Trajectory",
|
|
64
|
+
"Usage",
|
|
65
|
+
# contracts
|
|
66
|
+
"Clearance",
|
|
67
|
+
"Environment",
|
|
68
|
+
"MemoryView",
|
|
69
|
+
"ModelAdapter",
|
|
70
|
+
"ReasoningStrategy",
|
|
71
|
+
"Retriever",
|
|
72
|
+
"RiskLevel",
|
|
73
|
+
"RunContext",
|
|
74
|
+
"Tool",
|
|
75
|
+
# conversation
|
|
76
|
+
"ConversationStore",
|
|
77
|
+
"InMemoryConversationStore",
|
|
78
|
+
# events
|
|
79
|
+
"Event",
|
|
80
|
+
"EventEmitter",
|
|
81
|
+
# structured
|
|
82
|
+
"StructuredResult",
|
|
83
|
+
"parse_json",
|
|
84
|
+
"parse_structured",
|
|
85
|
+
# compiled
|
|
86
|
+
"CompiledContext",
|
|
87
|
+
"EvidenceBundle",
|
|
88
|
+
"MemorySlice",
|
|
89
|
+
"OutputContract",
|
|
90
|
+
"TaskSpec",
|
|
91
|
+
]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Agent — an immutable recipe for creating event-sourced Sessions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable, Iterator
|
|
5
|
+
from dataclasses import dataclass, field, fields
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
|
+
|
|
8
|
+
from petfishframework.core.contracts import ModelAdapter, ReasoningStrategy, Retriever, Tool
|
|
9
|
+
from petfishframework.core.conversation import ConversationStore
|
|
10
|
+
from petfishframework.core.events import EventEmitter
|
|
11
|
+
from petfishframework.core.structured import DataclassInstance, StructuredResult, parse_structured
|
|
12
|
+
from petfishframework.core.types import Budget, Message, ModelRequest, Result, Role, Task
|
|
13
|
+
from petfishframework.permissions.model import DefaultAllowPolicy, PermissionPolicy
|
|
14
|
+
|
|
15
|
+
from .session import Session
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T", bound=DataclassInstance)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Agent:
|
|
22
|
+
"""Declarative agent configuration.
|
|
23
|
+
|
|
24
|
+
Agent is the recipe; Session is the execution. Users can call `run()` for
|
|
25
|
+
the simple path, or call `session()` to obtain a replayable/auditable
|
|
26
|
+
process (decision 1).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
model: ModelAdapter
|
|
30
|
+
reasoning: ReasoningStrategy = field(default_factory=lambda: _default_reasoning())
|
|
31
|
+
tools: tuple[Tool, ...] = ()
|
|
32
|
+
retriever: Retriever | None = None
|
|
33
|
+
permission_policy: PermissionPolicy = field(
|
|
34
|
+
default_factory=lambda: _default_policy()
|
|
35
|
+
)
|
|
36
|
+
tool_registry: Any = None # ToolRegistry | None — lazy typed to avoid import cycle
|
|
37
|
+
|
|
38
|
+
def run(
|
|
39
|
+
self,
|
|
40
|
+
task: str | Task,
|
|
41
|
+
budget: Budget | None = None,
|
|
42
|
+
conversation_id: str | None = None,
|
|
43
|
+
conversation_store: ConversationStore | None = None,
|
|
44
|
+
) -> Result:
|
|
45
|
+
"""Create a Session and run it, returning the Result."""
|
|
46
|
+
session = self.session(
|
|
47
|
+
task,
|
|
48
|
+
budget=budget,
|
|
49
|
+
conversation_id=conversation_id,
|
|
50
|
+
conversation_store=conversation_store,
|
|
51
|
+
)
|
|
52
|
+
return session.run()
|
|
53
|
+
|
|
54
|
+
async def run_async(
|
|
55
|
+
self,
|
|
56
|
+
task: str | Task,
|
|
57
|
+
budget: Budget | None = None,
|
|
58
|
+
conversation_id: str | None = None,
|
|
59
|
+
conversation_store: ConversationStore | None = None,
|
|
60
|
+
) -> Result:
|
|
61
|
+
"""Create an async Session and run it, returning the Result."""
|
|
62
|
+
session = await self.session_async(
|
|
63
|
+
task,
|
|
64
|
+
budget=budget,
|
|
65
|
+
conversation_id=conversation_id,
|
|
66
|
+
conversation_store=conversation_store,
|
|
67
|
+
)
|
|
68
|
+
return await session.run_async()
|
|
69
|
+
|
|
70
|
+
def run_structured(
|
|
71
|
+
self,
|
|
72
|
+
task: str | Task,
|
|
73
|
+
output_type: type[T],
|
|
74
|
+
budget: Budget | None = None,
|
|
75
|
+
) -> StructuredResult[T]:
|
|
76
|
+
"""Run the agent and parse the output as structured data (dataclass).
|
|
77
|
+
|
|
78
|
+
The task prompt is augmented with instructions to return JSON matching
|
|
79
|
+
the output_type's fields.
|
|
80
|
+
"""
|
|
81
|
+
if not isinstance(output_type, type) or not hasattr(output_type, "__dataclass_fields__"):
|
|
82
|
+
raise ValueError(f"output_type must be a dataclass, got {output_type!r}")
|
|
83
|
+
|
|
84
|
+
task_obj = task if isinstance(task, Task) else Task(prompt=task)
|
|
85
|
+
field_names = [f.name for f in fields(output_type)]
|
|
86
|
+
augmented_prompt = (
|
|
87
|
+
f"{task_obj.prompt}\n\nReturn your answer as JSON with these fields: {field_names}"
|
|
88
|
+
)
|
|
89
|
+
augmented_task = Task(prompt=augmented_prompt, metadata=task_obj.metadata)
|
|
90
|
+
|
|
91
|
+
result = self.run(augmented_task, budget=budget)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
parsed = parse_structured(result.answer, output_type)
|
|
95
|
+
return StructuredResult(
|
|
96
|
+
answer=result.answer,
|
|
97
|
+
data=parsed,
|
|
98
|
+
parse_error=None,
|
|
99
|
+
session_id=result.session_id,
|
|
100
|
+
)
|
|
101
|
+
except ValueError as exc:
|
|
102
|
+
return StructuredResult(
|
|
103
|
+
answer=result.answer,
|
|
104
|
+
data=None,
|
|
105
|
+
parse_error=str(exc),
|
|
106
|
+
session_id=result.session_id,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def run_stream(
|
|
110
|
+
self,
|
|
111
|
+
task: str | Task,
|
|
112
|
+
budget: Budget | None = None,
|
|
113
|
+
) -> Iterator[str]:
|
|
114
|
+
"""Stream the agent's response as text chunks.
|
|
115
|
+
|
|
116
|
+
Yields text chunks as they arrive from the model. The final chunk
|
|
117
|
+
completes the response. Falls back to single-chunk if model doesn't support streaming.
|
|
118
|
+
"""
|
|
119
|
+
task_obj = task if isinstance(task, Task) else Task(prompt=task)
|
|
120
|
+
|
|
121
|
+
if hasattr(self.model, "query_stream"):
|
|
122
|
+
request = ModelRequest(
|
|
123
|
+
messages=(Message(role=Role.USER, content=task_obj.prompt),),
|
|
124
|
+
max_tokens=budget.max_tokens if budget is not None else None,
|
|
125
|
+
)
|
|
126
|
+
# Avoid direct attribute access on the typed ModelAdapter interface.
|
|
127
|
+
stream_attr = "query_stream"
|
|
128
|
+
stream_method: Callable[[ModelRequest], Iterator[str]] = getattr(
|
|
129
|
+
self.model, stream_attr
|
|
130
|
+
)
|
|
131
|
+
yield from stream_method(request)
|
|
132
|
+
else:
|
|
133
|
+
result = self.run(task_obj, budget=budget)
|
|
134
|
+
yield result.answer
|
|
135
|
+
|
|
136
|
+
def session(
|
|
137
|
+
self,
|
|
138
|
+
task: str | Task,
|
|
139
|
+
budget: Budget | None = None,
|
|
140
|
+
conversation_id: str | None = None,
|
|
141
|
+
conversation_store: ConversationStore | None = None,
|
|
142
|
+
) -> Session:
|
|
143
|
+
"""Create a new Session from this agent's configuration.
|
|
144
|
+
|
|
145
|
+
If tool_registry is set, IntentRouter auto-selects tools based on
|
|
146
|
+
task intent (Council #1: automatic tool selection). Explicit tools
|
|
147
|
+
are always included; auto-selected tools supplement them.
|
|
148
|
+
"""
|
|
149
|
+
if isinstance(task, str):
|
|
150
|
+
task = Task(prompt=task)
|
|
151
|
+
|
|
152
|
+
# Resolve tools: explicit + auto-selected from registry
|
|
153
|
+
resolved_tools = self.tools
|
|
154
|
+
if self.tool_registry is not None:
|
|
155
|
+
from petfishframework.tools.registry import IntentRouter
|
|
156
|
+
|
|
157
|
+
router = IntentRouter()
|
|
158
|
+
auto_tools = router.route(task, self.tool_registry)
|
|
159
|
+
# Merge: explicit tools + auto-selected (deduplicate by name)
|
|
160
|
+
explicit_names = {t.name for t in resolved_tools}
|
|
161
|
+
for t in auto_tools:
|
|
162
|
+
if t.name not in explicit_names:
|
|
163
|
+
resolved_tools = resolved_tools + (t,)
|
|
164
|
+
|
|
165
|
+
events = EventEmitter()
|
|
166
|
+
return Session(
|
|
167
|
+
model=self.model,
|
|
168
|
+
reasoning=self.reasoning,
|
|
169
|
+
tools=resolved_tools,
|
|
170
|
+
retriever=self.retriever,
|
|
171
|
+
policy=self.permission_policy,
|
|
172
|
+
task=task,
|
|
173
|
+
budget=budget,
|
|
174
|
+
events=events,
|
|
175
|
+
conversation_id=conversation_id,
|
|
176
|
+
conversation_store=conversation_store,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def session_async(
|
|
180
|
+
self,
|
|
181
|
+
task: str | Task,
|
|
182
|
+
budget: Budget | None = None,
|
|
183
|
+
conversation_id: str | None = None,
|
|
184
|
+
conversation_store: ConversationStore | None = None,
|
|
185
|
+
) -> Session:
|
|
186
|
+
"""Create a new Session flagged for async execution.
|
|
187
|
+
|
|
188
|
+
The returned Session is identical to the sync session(); the async flag
|
|
189
|
+
is implicit in calling run_async() on it.
|
|
190
|
+
"""
|
|
191
|
+
return self.session(
|
|
192
|
+
task,
|
|
193
|
+
budget=budget,
|
|
194
|
+
conversation_id=conversation_id,
|
|
195
|
+
conversation_store=conversation_store,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _default_reasoning() -> ReasoningStrategy:
|
|
200
|
+
# Import lazily to avoid reasoning -> core cycle at import time.
|
|
201
|
+
from petfishframework.reasoning.react import ReAct
|
|
202
|
+
|
|
203
|
+
return ReAct()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _default_policy() -> PermissionPolicy:
|
|
207
|
+
return DefaultAllowPolicy()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Contract compilation types (v0.2 — absorbed from contract-driven-harness-study).
|
|
2
|
+
|
|
3
|
+
The Environment compiles these BEFORE the model runs. The model operates
|
|
4
|
+
within these bounds — it is a component inside a contract system, not the
|
|
5
|
+
sole source of control. This externalizes reliability obligations into
|
|
6
|
+
explicit, inspectable contracts.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class TaskSpec:
|
|
16
|
+
"""Compiled task specification.
|
|
17
|
+
|
|
18
|
+
Boundaries, success/failure conditions, and forbidden actions.
|
|
19
|
+
Produced by the Environment's intent-routing + compilation step.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
task_type: str = "generic"
|
|
23
|
+
success_criteria: str = ""
|
|
24
|
+
forbidden_actions: tuple[str, ...] = ()
|
|
25
|
+
requires_sources: bool = False
|
|
26
|
+
max_autonomy: str = "full" # full | bounded | supervised
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class MemorySlice:
|
|
31
|
+
"""Bounded memory slice delivered to the strategy.
|
|
32
|
+
|
|
33
|
+
Topic-filtered + TTL-applied + conflict-resolved. This is what the
|
|
34
|
+
strategy sees, not the raw memory store.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
entries: tuple[dict[str, Any], ...] = ()
|
|
38
|
+
topic: str = ""
|
|
39
|
+
ttl_s: float | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class SourceRef:
|
|
44
|
+
"""A reference to an evidence source."""
|
|
45
|
+
|
|
46
|
+
source_id: str
|
|
47
|
+
source_type: str = "" # doc, url, tool_output, etc.
|
|
48
|
+
trust_tier: str = "unverified"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class EvidenceBundle:
|
|
53
|
+
"""Evidence with source provenance and trust tiers.
|
|
54
|
+
|
|
55
|
+
Produced by the Retriever (Environment.retrieve). The strategy receives
|
|
56
|
+
this compiled bundle, not raw retrieval access.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
snippets: tuple[Any, ...] = () # Snippet objects from retrieval
|
|
60
|
+
sources: tuple[SourceRef, ...] = ()
|
|
61
|
+
requires_citation: bool = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class OutputContract:
|
|
66
|
+
"""Output requirements the result must satisfy.
|
|
67
|
+
|
|
68
|
+
Required sections, format, and validation rules. The ValidatorGate
|
|
69
|
+
(reliability/) checks results against this contract.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
required_sections: tuple[str, ...] = ()
|
|
73
|
+
format: str = "text" # text | json | markdown
|
|
74
|
+
max_length: int | None = None
|
|
75
|
+
validation_rules: tuple[str, ...] = ()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class CompiledContext:
|
|
80
|
+
"""All four contract objects, compiled before strategy.run().
|
|
81
|
+
|
|
82
|
+
This is the v0.2 contract compilation layer (decision 3 enrichment).
|
|
83
|
+
Passed to RunContext.compiled so strategies can inspect their bounds.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
task_spec: TaskSpec = field(default_factory=TaskSpec)
|
|
87
|
+
memory_slice: MemorySlice = field(default_factory=MemorySlice)
|
|
88
|
+
evidence_bundle: EvidenceBundle = field(default_factory=EvidenceBundle)
|
|
89
|
+
output_contract: OutputContract = field(default_factory=OutputContract)
|