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.
Files changed (51) hide show
  1. petfishframework/__init__.py +33 -0
  2. petfishframework/config.py +142 -0
  3. petfishframework/core/__init__.py +91 -0
  4. petfishframework/core/agent.py +207 -0
  5. petfishframework/core/compiled.py +89 -0
  6. petfishframework/core/contracts.py +190 -0
  7. petfishframework/core/conversation.py +51 -0
  8. petfishframework/core/environment.py +260 -0
  9. petfishframework/core/events.py +79 -0
  10. petfishframework/core/session.py +182 -0
  11. petfishframework/core/structured.py +202 -0
  12. petfishframework/core/types.py +192 -0
  13. petfishframework/mcp/__init__.py +19 -0
  14. petfishframework/mcp/client.py +96 -0
  15. petfishframework/mcp/server.py +14 -0
  16. petfishframework/mcp/stdio_transport.py +218 -0
  17. petfishframework/mcp/wrapper.py +43 -0
  18. petfishframework/models/__init__.py +14 -0
  19. petfishframework/models/anthropic.py +194 -0
  20. petfishframework/models/fake.py +202 -0
  21. petfishframework/models/openai.py +178 -0
  22. petfishframework/observability/__init__.py +10 -0
  23. petfishframework/observability/sinks.py +23 -0
  24. petfishframework/permissions/__init__.py +32 -0
  25. petfishframework/permissions/model.py +110 -0
  26. petfishframework/reasoning/__init__.py +13 -0
  27. petfishframework/reasoning/lats.py +222 -0
  28. petfishframework/reasoning/llm_plus_p.py +202 -0
  29. petfishframework/reasoning/react.py +176 -0
  30. petfishframework/reliability/__init__.py +78 -0
  31. petfishframework/reliability/cost.py +50 -0
  32. petfishframework/reliability/cost_report.py +101 -0
  33. petfishframework/reliability/pass_at_k.py +232 -0
  34. petfishframework/reliability/replay.py +190 -0
  35. petfishframework/reliability/retry.py +224 -0
  36. petfishframework/reliability/timeout.py +55 -0
  37. petfishframework/retrieval/__init__.py +12 -0
  38. petfishframework/retrieval/adaptive.py +137 -0
  39. petfishframework/retrieval/crag.py +135 -0
  40. petfishframework/retrieval/memory_store.py +72 -0
  41. petfishframework/tools/__init__.py +12 -0
  42. petfishframework/tools/agent_tool.py +47 -0
  43. petfishframework/tools/base.py +75 -0
  44. petfishframework/tools/calculator.py +84 -0
  45. petfishframework/tools/path_planner.py +90 -0
  46. petfishframework/tools/registry.py +158 -0
  47. petfishframework/tools/word_sorter.py +41 -0
  48. petfishframework-0.1.0.dist-info/METADATA +151 -0
  49. petfishframework-0.1.0.dist-info/RECORD +51 -0
  50. petfishframework-0.1.0.dist-info/WHEEL +4 -0
  51. 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)