pyagentic-core 2.14.1.dev1__py3-none-any.whl → 2.15.1.dev1__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.
- pyagentic/__init__.py +7 -1
- pyagentic/_base/_agent/_agent.py +11 -5
- pyagentic/_base/_agent/_agent_linking.py +2 -2
- pyagentic/_base/_agent/_agent_state.py +35 -3
- pyagentic/_base/_depends.py +1 -1
- pyagentic/_base/_exceptions.py +22 -3
- pyagentic/_base/_mcp.py +1 -1
- pyagentic/_base/_metaclasses.py +23 -9
- pyagentic/_base/_prompts.py +243 -0
- pyagentic/_base/_ref.py +1 -1
- pyagentic/_base/_spec.py +1 -1
- pyagentic/_base/_state.py +1 -1
- pyagentic/_base/_tool.py +1 -1
- pyagentic/models/response.py +9 -0
- pyagentic/policies/_policy.py +1 -1
- pyagentic/policies/messages.py +1 -1
- {pyagentic_core-2.14.1.dev1.dist-info → pyagentic_core-2.15.1.dev1.dist-info}/METADATA +1 -1
- {pyagentic_core-2.14.1.dev1.dist-info → pyagentic_core-2.15.1.dev1.dist-info}/RECORD +23 -22
- {pyagentic_core-2.14.1.dev1.dist-info → pyagentic_core-2.15.1.dev1.dist-info}/scm_file_list.json +3 -0
- pyagentic_core-2.15.1.dev1.dist-info/scm_version.json +8 -0
- pyagentic_core-2.14.1.dev1.dist-info/scm_version.json +0 -8
- {pyagentic_core-2.14.1.dev1.dist-info → pyagentic_core-2.15.1.dev1.dist-info}/WHEEL +0 -0
- {pyagentic_core-2.14.1.dev1.dist-info → pyagentic_core-2.15.1.dev1.dist-info}/licenses/LICENSE +0 -0
- {pyagentic_core-2.14.1.dev1.dist-info → pyagentic_core-2.15.1.dev1.dist-info}/top_level.txt +0 -0
pyagentic/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ Quick Start:
|
|
|
13
13
|
from pyagentic import BaseAgent, tool
|
|
14
14
|
|
|
15
15
|
class MyAgent(BaseAgent):
|
|
16
|
-
|
|
16
|
+
__instructions__ = "You are a helpful assistant"
|
|
17
17
|
|
|
18
18
|
@tool("Calculate the sum of two numbers")
|
|
19
19
|
def add(self, a: int, b: int) -> str:
|
|
@@ -34,6 +34,7 @@ Main Components:
|
|
|
34
34
|
- spec: Configuration factory for state, params, and agent links
|
|
35
35
|
- ref: Dynamic state reference system for tool parameters
|
|
36
36
|
- AgentExtension: Base class for creating reusable agent mixins
|
|
37
|
+
- PromptEngine / LocalPromptEngine: Managed sources for agent instructions
|
|
37
38
|
"""
|
|
38
39
|
|
|
39
40
|
from pyagentic._base._spec import spec
|
|
@@ -45,6 +46,7 @@ from pyagentic._base._tool import tool
|
|
|
45
46
|
from pyagentic._base._state import State
|
|
46
47
|
from pyagentic._base._depends import Depends
|
|
47
48
|
from pyagentic._base._ref import ref
|
|
49
|
+
from pyagentic._base._prompts import PromptEngine, LocalPromptEngine, PromptRef, PromptSource
|
|
48
50
|
|
|
49
51
|
__all__ = [
|
|
50
52
|
"BaseAgent",
|
|
@@ -56,4 +58,8 @@ __all__ = [
|
|
|
56
58
|
"Depends",
|
|
57
59
|
"MCPLink",
|
|
58
60
|
"ref",
|
|
61
|
+
"PromptEngine",
|
|
62
|
+
"LocalPromptEngine",
|
|
63
|
+
"PromptRef",
|
|
64
|
+
"PromptSource",
|
|
59
65
|
]
|
pyagentic/_base/_agent/_agent.py
CHANGED
|
@@ -24,6 +24,7 @@ from pyagentic._base._state import _StateDefinition
|
|
|
24
24
|
from pyagentic._base._metaclasses import AgentMeta
|
|
25
25
|
from pyagentic._base._exceptions import InvalidLLMSetup, InvalidToolDefinition
|
|
26
26
|
from pyagentic._base._info import _SpecInfo
|
|
27
|
+
from pyagentic._base._prompts import PromptRef
|
|
27
28
|
from pyagentic._base._agent._agent_state import _AgentState
|
|
28
29
|
|
|
29
30
|
if TYPE_CHECKING:
|
|
@@ -91,7 +92,7 @@ class AgentExtension:
|
|
|
91
92
|
return f"Logged: {message}"
|
|
92
93
|
|
|
93
94
|
class MyAgent(BaseAgent, LoggingMixin):
|
|
94
|
-
|
|
95
|
+
__instructions__ = "You are a helpful agent with logging"
|
|
95
96
|
```
|
|
96
97
|
"""
|
|
97
98
|
|
|
@@ -127,7 +128,10 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
127
128
|
|
|
128
129
|
Agent definition requires the use of special decorators and class attributes:
|
|
129
130
|
- @tool: Declares a method as a tool callable by the LLM
|
|
130
|
-
-
|
|
131
|
+
- __instructions__: Required class attribute defining the agent's instructions,
|
|
132
|
+
either as a string or a PromptRef from a PromptEngine. Rendered with state
|
|
133
|
+
into the system message sent to the LLM. (__system_message__ is the
|
|
134
|
+
deprecated spelling.)
|
|
131
135
|
- __description__: Optional description used when agent is linked to another agent
|
|
132
136
|
- __input_template__: Optional template for formatting user input
|
|
133
137
|
- __response_format__: Optional Pydantic model for structured output
|
|
@@ -175,7 +179,8 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
175
179
|
__dependencies__: ClassVar[dict[str, type]] # Depends[T] field -> dependency type
|
|
176
180
|
|
|
177
181
|
# User-set Class Attributes (defined in subclass)
|
|
178
|
-
|
|
182
|
+
__instructions__: ClassVar[Union[str, PromptRef]] # Required: the agent's instructions
|
|
183
|
+
__system_message__: ClassVar[str] # Deprecated: old spelling of __instructions__
|
|
179
184
|
__description__: ClassVar[str] # Optional: description for linked agents
|
|
180
185
|
__input_template__: ClassVar[str] = None # Optional: template for user input
|
|
181
186
|
__response_format__: ClassVar[Type[BaseModel]] = None # Optional: structured output format
|
|
@@ -381,7 +386,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
381
386
|
Example:
|
|
382
387
|
```python
|
|
383
388
|
class DatabaseAgent(BaseAgent):
|
|
384
|
-
|
|
389
|
+
__instructions__ = "You query databases"
|
|
385
390
|
connection: Optional[Connection] = None
|
|
386
391
|
|
|
387
392
|
def __post_init__(self):
|
|
@@ -887,6 +892,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
887
892
|
"final_output": final_ai_output,
|
|
888
893
|
"state": self.state,
|
|
889
894
|
"provider_info": self.provider._info,
|
|
895
|
+
"prompt": self.state.prompt_source,
|
|
890
896
|
}
|
|
891
897
|
# Include tool/agent responses if any were called
|
|
892
898
|
if self.__tool_defs__:
|
|
@@ -957,7 +963,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
957
963
|
Example (Custom Implementation):
|
|
958
964
|
```python
|
|
959
965
|
class CoursePlannerAgent(BaseAgent):
|
|
960
|
-
|
|
966
|
+
__instructions__ = "You design course curricula"
|
|
961
967
|
__description__ = "Creates structured course plans"
|
|
962
968
|
|
|
963
969
|
async def __call__(
|
|
@@ -21,11 +21,11 @@ class Link(Generic[T]):
|
|
|
21
21
|
Example:
|
|
22
22
|
```python
|
|
23
23
|
class ResearchAgent(BaseAgent):
|
|
24
|
-
|
|
24
|
+
__instructions__ = "You research topics deeply"
|
|
25
25
|
__description__ = "Research agent for gathering detailed information"
|
|
26
26
|
|
|
27
27
|
class OrchestratorAgent(BaseAgent):
|
|
28
|
-
|
|
28
|
+
__instructions__ = "You coordinate research tasks"
|
|
29
29
|
|
|
30
30
|
# Link research agent as a tool
|
|
31
31
|
researcher: Link[ResearchAgent]
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import threading
|
|
3
3
|
from typing import Any, Type, Self, Optional, ClassVar
|
|
4
|
-
from pydantic import BaseModel, create_model, Field, PrivateAttr
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, create_model, Field, PrivateAttr
|
|
5
5
|
from jinja2 import Template
|
|
6
6
|
from typing import Optional, Literal, Callable
|
|
7
7
|
from transitions import Machine
|
|
8
8
|
|
|
9
9
|
from pyagentic._base._exceptions import InvalidStateRefNotFoundInState
|
|
10
10
|
from pyagentic._base._state import _StateDefinition
|
|
11
|
+
from pyagentic._base._prompts import PromptRef, PromptSource, _inline_source
|
|
11
12
|
from pyagentic.policies._policy import Policy
|
|
12
13
|
from pyagentic.policies._events import Event, EventKind, GetEvent, SetEvent, CompileEvent
|
|
13
14
|
from pyagentic.policies._list import PolicyList
|
|
@@ -30,14 +31,18 @@ class _AgentState(BaseModel):
|
|
|
30
31
|
("background", EventKind.APPEND): "background_append",
|
|
31
32
|
}
|
|
32
33
|
__policies__: ClassVar[dict[str, list[Policy]]] = {}
|
|
34
|
+
__agent_name__: ClassVar[str] = ""
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
37
|
+
|
|
38
|
+
instructions: str | PromptRef
|
|
35
39
|
input_template: Optional[str] = "{{ user_message }}"
|
|
36
40
|
|
|
37
41
|
_machine: Machine = PrivateAttr(default=None)
|
|
38
42
|
_messages: list[Message] = PrivateAttr(default_factory=list)
|
|
39
43
|
_context: list[Message] = PrivateAttr(default_factory=list)
|
|
40
44
|
_last_usage: Optional[UsageInfo] = PrivateAttr(default=None)
|
|
45
|
+
_prompt_source: Optional[PromptSource] = PrivateAttr(default=None)
|
|
41
46
|
_instructions_template: Template = PrivateAttr(default_factory=lambda: Template(source=""))
|
|
42
47
|
_input_template: Template = PrivateAttr(
|
|
43
48
|
default_factory=lambda: Template(source="{{ user_message }}")
|
|
@@ -73,6 +78,18 @@ class _AgentState(BaseModel):
|
|
|
73
78
|
getattr(self._machine, trigger)()
|
|
74
79
|
|
|
75
80
|
def model_post_init(self, state):
|
|
81
|
+
# Instructions declared as a PromptRef resolve here, at instantiation, so
|
|
82
|
+
# every new agent instance (and every fork) picks up the latest prompt.
|
|
83
|
+
# Plain strings get an "inline" PromptSource so every run still carries
|
|
84
|
+
# versioned prompt metadata.
|
|
85
|
+
if isinstance(self.instructions, PromptRef):
|
|
86
|
+
self._prompt_source = self.instructions.resolve()
|
|
87
|
+
self.instructions = self._prompt_source.text
|
|
88
|
+
else:
|
|
89
|
+
self._prompt_source = _inline_source(
|
|
90
|
+
self.instructions, source=self.__agent_name__ or type(self).__name__
|
|
91
|
+
)
|
|
92
|
+
|
|
76
93
|
self._instructions_template = Template(source=self.instructions)
|
|
77
94
|
if self.input_template:
|
|
78
95
|
self._input_template = Template(source=self.input_template)
|
|
@@ -364,7 +381,10 @@ class _AgentState(BaseModel):
|
|
|
364
381
|
raise RuntimeError(f"Unexpected ctx_map entry for {_name!r}: {definition!r}")
|
|
365
382
|
|
|
366
383
|
# now build the dataclass
|
|
367
|
-
|
|
384
|
+
model = create_model(f"AgentState[{name}]", __base__=cls, **pydantic_fields)
|
|
385
|
+
# Used as the PromptSource `source` for inline (plain string) instructions
|
|
386
|
+
model.__agent_name__ = name
|
|
387
|
+
return model
|
|
368
388
|
|
|
369
389
|
@property
|
|
370
390
|
def phase(self) -> str:
|
|
@@ -380,6 +400,18 @@ class _AgentState(BaseModel):
|
|
|
380
400
|
"""
|
|
381
401
|
return self._messages[-1]
|
|
382
402
|
|
|
403
|
+
@property
|
|
404
|
+
def prompt_source(self) -> PromptSource:
|
|
405
|
+
"""
|
|
406
|
+
Returns metadata about the prompt behind the instructions: the engine it
|
|
407
|
+
was loaded from (source_type "local", ...), or an "inline" source named
|
|
408
|
+
after the agent for instructions declared as a plain string.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
PromptSource: The prompt text plus source, version, and load-time metadata.
|
|
412
|
+
"""
|
|
413
|
+
return self._prompt_source
|
|
414
|
+
|
|
383
415
|
@property
|
|
384
416
|
def system_message(self) -> str:
|
|
385
417
|
"""
|
pyagentic/_base/_depends.py
CHANGED
|
@@ -28,7 +28,7 @@ class Depends(Generic[T]):
|
|
|
28
28
|
from pyagentic import BaseAgent, State, Depends
|
|
29
29
|
|
|
30
30
|
class ResearchAgent(BaseAgent):
|
|
31
|
-
|
|
31
|
+
__instructions__ = "You research topics"
|
|
32
32
|
|
|
33
33
|
topic: State[TopicState] # client-provided per session
|
|
34
34
|
db: Depends[Database] # injected by the developer, by type
|
pyagentic/_base/_exceptions.py
CHANGED
|
@@ -15,17 +15,36 @@ class InvalidToolDefinition(Exception):
|
|
|
15
15
|
super().__init__(message)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class
|
|
18
|
+
class InstructionsNotDeclared(Exception):
|
|
19
19
|
"""
|
|
20
|
-
Exception raised when an Agent subclass is created without declaring
|
|
20
|
+
Exception raised when an Agent subclass is created without declaring __instructions__
|
|
21
|
+
(or the deprecated __system_message__).
|
|
21
22
|
"""
|
|
22
23
|
|
|
23
24
|
def __init__(self):
|
|
24
25
|
super().__init__(
|
|
25
|
-
"
|
|
26
|
+
"Instructions not declared on agent. Agent must be declared with `__instructions__`"
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
# Deprecated alias, kept for backwards compatibility with the old dunder name
|
|
31
|
+
SystemMessageNotDeclared = InstructionsNotDeclared
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PromptNotFound(Exception):
|
|
35
|
+
"""
|
|
36
|
+
Exception raised when a PromptEngine cannot find a prompt for the given key.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
key (str): The prompt key that was requested
|
|
40
|
+
source (str): Where the engine looked (e.g. the root directory)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, key, source):
|
|
44
|
+
message = f"Prompt '{key}' not found in {source}"
|
|
45
|
+
super().__init__(message)
|
|
46
|
+
|
|
47
|
+
|
|
29
48
|
class UnexpectedStateItemType(Exception):
|
|
30
49
|
"""
|
|
31
50
|
Exception raised when a state item is initialized with a value of an unexpected type.
|
pyagentic/_base/_mcp.py
CHANGED
pyagentic/_base/_metaclasses.py
CHANGED
|
@@ -11,7 +11,7 @@ from pydantic import BaseModel, Field, create_model
|
|
|
11
11
|
|
|
12
12
|
from pyagentic._base._info import _SpecInfo, ParamInfo, AgentInfo, MCPInfo
|
|
13
13
|
from pyagentic._base._validation import _AgentConstructionValidator
|
|
14
|
-
from pyagentic._base._exceptions import
|
|
14
|
+
from pyagentic._base._exceptions import InstructionsNotDeclared, UnexpectedStateItemType
|
|
15
15
|
from pyagentic._base._agent._agent_state import _AgentState
|
|
16
16
|
from pyagentic._base._tool import _ToolDefinition, tool
|
|
17
17
|
from pyagentic._base._state import State, StateInfo, _StateDefinition
|
|
@@ -35,7 +35,7 @@ class Agent:
|
|
|
35
35
|
class AgentMeta(type):
|
|
36
36
|
"""
|
|
37
37
|
Metaclass that applies only to Agent subclasses:
|
|
38
|
-
- Ensures __system_message__ was declared
|
|
38
|
+
- Ensures __instructions__ (or the deprecated __system_message__) was declared
|
|
39
39
|
- Collects @tool definitions and State field attributes
|
|
40
40
|
- Initializes class __tool_defs__ and __state_defs__
|
|
41
41
|
- Dynamically injects an __init__ signature based on class __annotations__
|
|
@@ -608,9 +608,9 @@ class AgentMeta(type):
|
|
|
608
608
|
else:
|
|
609
609
|
compiled[name] = definition.info.get_default()
|
|
610
610
|
|
|
611
|
-
# Create the state object with
|
|
611
|
+
# Create the state object with instructions and state fields
|
|
612
612
|
self.state = self.__state_class__(
|
|
613
|
-
instructions=self.
|
|
613
|
+
instructions=self.__instructions__,
|
|
614
614
|
input_template=self.__input_template__,
|
|
615
615
|
**compiled,
|
|
616
616
|
)
|
|
@@ -665,7 +665,7 @@ class AgentMeta(type):
|
|
|
665
665
|
|
|
666
666
|
Inheritance is respected in MRO order. Tools, state attributes, and linked agents
|
|
667
667
|
can all be inherited from other agents or mixins.
|
|
668
|
-
|
|
668
|
+
__instructions__ and __input_template__ are *not* inherited.
|
|
669
669
|
|
|
670
670
|
Args:
|
|
671
671
|
name (str): Name of the new class
|
|
@@ -677,7 +677,8 @@ class AgentMeta(type):
|
|
|
677
677
|
type: The newly created Agent subclass
|
|
678
678
|
|
|
679
679
|
Raises:
|
|
680
|
-
|
|
680
|
+
InstructionsNotDeclared: If neither __instructions__ nor the deprecated
|
|
681
|
+
__system_message__ is defined in the class
|
|
681
682
|
"""
|
|
682
683
|
|
|
683
684
|
# Create an inherited namespace by combining all bases in MRO order
|
|
@@ -699,9 +700,22 @@ class AgentMeta(type):
|
|
|
699
700
|
mcs.__BaseAgent__ = cls
|
|
700
701
|
return cls
|
|
701
702
|
cls.__abstract_base__ = False
|
|
702
|
-
# Verify
|
|
703
|
-
|
|
704
|
-
|
|
703
|
+
# Verify instructions are set (not inherited); __system_message__ is
|
|
704
|
+
# the deprecated spelling and normalizes onto __instructions__
|
|
705
|
+
if "__instructions__" in namespace:
|
|
706
|
+
cls.__instructions__ = namespace["__instructions__"]
|
|
707
|
+
elif "__system_message__" in namespace:
|
|
708
|
+
warnings.warn(
|
|
709
|
+
f"`__system_message__` on {name} is deprecated; "
|
|
710
|
+
"declare `__instructions__` instead",
|
|
711
|
+
DeprecationWarning,
|
|
712
|
+
stacklevel=2,
|
|
713
|
+
)
|
|
714
|
+
cls.__instructions__ = namespace["__system_message__"]
|
|
715
|
+
else:
|
|
716
|
+
raise InstructionsNotDeclared()
|
|
717
|
+
# Keep the deprecated attribute readable for backwards compatibility
|
|
718
|
+
cls.__system_message__ = cls.__instructions__
|
|
705
719
|
|
|
706
720
|
# Extract and attach Agent attributes:
|
|
707
721
|
#
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt management for agents.
|
|
3
|
+
|
|
4
|
+
A `PromptEngine` is a source agents can pull managed instructions from (local files,
|
|
5
|
+
a database, a prompt-management service, ...). Engines load prompt text by key and
|
|
6
|
+
return it as a `PromptSource` carrying version metadata. `PromptEngine.ref(key)`
|
|
7
|
+
produces a deferred `PromptRef` that can be assigned to `__instructions__`; the agent
|
|
8
|
+
resolves it at instantiation time, so every new instance (and every fork) picks up
|
|
9
|
+
the latest version of the prompt.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import re
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
|
|
20
|
+
from pyagentic._base._exceptions import PromptNotFound
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PromptSource(BaseModel):
|
|
24
|
+
"""
|
|
25
|
+
A loaded prompt along with metadata about where and when it was loaded.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
text (str): The prompt text itself.
|
|
29
|
+
source (str): Identifier of where the prompt came from (e.g. a file path).
|
|
30
|
+
source_type (str): The kind of engine that loaded it (e.g. "local").
|
|
31
|
+
version (str): Version identifier for the loaded text (e.g. a content hash).
|
|
32
|
+
loaded_at (datetime): When the prompt was loaded.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
text: str
|
|
36
|
+
source: str
|
|
37
|
+
source_type: str
|
|
38
|
+
version: str
|
|
39
|
+
loaded_at: datetime
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _version_hash(text: str) -> str:
|
|
43
|
+
"""Content hash used as the version identifier for prompt text."""
|
|
44
|
+
return hashlib.sha256(text.encode()).hexdigest()[:12]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _inline_source(text: str, source: str) -> PromptSource:
|
|
48
|
+
"""Build a PromptSource for instructions declared inline as a plain string."""
|
|
49
|
+
return PromptSource(
|
|
50
|
+
text=text,
|
|
51
|
+
source=source,
|
|
52
|
+
source_type="inline",
|
|
53
|
+
version=_version_hash(text),
|
|
54
|
+
loaded_at=datetime.now(timezone.utc),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PromptRef:
|
|
59
|
+
"""
|
|
60
|
+
A deferred pointer to a prompt in a `PromptEngine`.
|
|
61
|
+
|
|
62
|
+
Created via `PromptEngine.ref(key)` and assigned to `__instructions__`; resolved
|
|
63
|
+
at agent instantiation, so each new instance (and each fork) reloads the prompt.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, engine: "PromptEngine", key: str, version: str | None = None):
|
|
67
|
+
self.engine = engine
|
|
68
|
+
self.key = key
|
|
69
|
+
self.version = version
|
|
70
|
+
|
|
71
|
+
def resolve(self) -> PromptSource:
|
|
72
|
+
"""
|
|
73
|
+
Load the referenced prompt from its engine.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
PromptSource: The loaded prompt text plus metadata.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
PromptNotFound: If the engine has no prompt for the key.
|
|
80
|
+
"""
|
|
81
|
+
return self.engine.load(self.key, version=self.version)
|
|
82
|
+
|
|
83
|
+
def __repr__(self) -> str:
|
|
84
|
+
return f"PromptRef(engine={self.engine!r}, key={self.key!r}, version={self.version!r})"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class PromptEngine(ABC):
|
|
88
|
+
"""
|
|
89
|
+
Base class for prompt engines, designed to be extended to support multiple
|
|
90
|
+
different source types (local files, databases, managed prompt services, ...).
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def load(self, key: str, version: str | None = None) -> PromptSource:
|
|
95
|
+
"""
|
|
96
|
+
Load a prompt by key.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
key (str): Engine-specific identifier for the prompt.
|
|
100
|
+
version (str | None): Specific version to load; the engine's latest
|
|
101
|
+
when omitted.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
PromptSource: The loaded prompt text plus metadata.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
PromptNotFound: If no prompt exists for the key (or version).
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def ref(self, key: str, version: str | None = None) -> PromptRef:
|
|
111
|
+
"""
|
|
112
|
+
Create a deferred reference to a prompt, resolved at agent instantiation.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
key (str): Engine-specific identifier for the prompt.
|
|
116
|
+
version (str | None): Pin a specific version; the engine's latest
|
|
117
|
+
when omitted.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
PromptRef: A reference that loads the prompt when resolved.
|
|
121
|
+
"""
|
|
122
|
+
return PromptRef(self, key, version=version)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _natural_sort_key(version: str) -> list:
|
|
126
|
+
"""Sort key treating digit runs numerically, so e.g. v10 orders after v2."""
|
|
127
|
+
return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", version)]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class LocalPromptEngine(PromptEngine):
|
|
131
|
+
"""
|
|
132
|
+
Prompt engine backed by the local file system.
|
|
133
|
+
|
|
134
|
+
A `pattern` defines how prompt files are laid out under the root directory
|
|
135
|
+
(default `.prompts/` in the current working directory). The pattern is a
|
|
136
|
+
template with a required `{key}` placeholder and an optional `{version}`
|
|
137
|
+
placeholder:
|
|
138
|
+
|
|
139
|
+
- Without `{version}` (default `"{key}.md"`): the key maps to a single file
|
|
140
|
+
and the prompt is versioned by content hash.
|
|
141
|
+
- With `{version}` (e.g. `"{key}/{version}.md"`): the directory structure
|
|
142
|
+
holds the versions. `load(key)` picks the latest version (natural sort,
|
|
143
|
+
so `v10` > `v2`); `load(key, version="v1")` pins one. The path's version
|
|
144
|
+
segment becomes `PromptSource.version`.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
```python
|
|
148
|
+
# .prompts/researcher/v1.md, .prompts/researcher/v2.md, ...
|
|
149
|
+
prompts = LocalPromptEngine(".prompts", pattern="{key}/{version}.md")
|
|
150
|
+
|
|
151
|
+
class ResearchAgent(BaseAgent):
|
|
152
|
+
__instructions__ = prompts.ref("researcher") # latest version
|
|
153
|
+
```
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(self, path: str | Path | None = None, pattern: str = "{key}.md"):
|
|
157
|
+
"""
|
|
158
|
+
Args:
|
|
159
|
+
path (str | Path | None): Root directory holding prompt files. Defaults
|
|
160
|
+
to `.prompts/` under the current working directory.
|
|
161
|
+
pattern (str): File layout template relative to the root. Must contain
|
|
162
|
+
`{key}`; may contain `{version}` to derive versions from the file
|
|
163
|
+
structure. Defaults to `"{key}.md"`.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
ValueError: If the pattern does not contain a `{key}` placeholder.
|
|
167
|
+
"""
|
|
168
|
+
if "{key}" not in pattern:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"LocalPromptEngine pattern {pattern!r} must contain a '{{key}}' placeholder"
|
|
171
|
+
)
|
|
172
|
+
self.root = Path(path) if path else Path.cwd() / ".prompts"
|
|
173
|
+
self.pattern = pattern
|
|
174
|
+
self.versioned = "{version}" in pattern
|
|
175
|
+
|
|
176
|
+
def _resolve_versioned(self, key: str, version: str | None) -> tuple[Path, str]:
|
|
177
|
+
"""Resolve path and version for a versioned pattern; latest version when unpinned."""
|
|
178
|
+
if version is not None:
|
|
179
|
+
path = self.root / self.pattern.format(key=key, version=version)
|
|
180
|
+
if not path.is_file():
|
|
181
|
+
raise PromptNotFound(key=f"{key} (version {version})", source=str(self.root))
|
|
182
|
+
return path, version
|
|
183
|
+
|
|
184
|
+
# Glob for every version of the key, then parse the version segment back
|
|
185
|
+
# out of each path with a regex built from the pattern
|
|
186
|
+
regex = re.compile(
|
|
187
|
+
re.escape(self.pattern)
|
|
188
|
+
.replace(re.escape("{key}"), re.escape(key))
|
|
189
|
+
.replace(re.escape("{version}"), r"(?P<version>[^/]+)")
|
|
190
|
+
)
|
|
191
|
+
found: dict[str, Path] = {}
|
|
192
|
+
for path in self.root.glob(self.pattern.format(key=key, version="*")):
|
|
193
|
+
match = regex.fullmatch(path.relative_to(self.root).as_posix())
|
|
194
|
+
if match:
|
|
195
|
+
found[match.group("version")] = path
|
|
196
|
+
|
|
197
|
+
if not found:
|
|
198
|
+
raise PromptNotFound(key=key, source=str(self.root))
|
|
199
|
+
latest = max(found, key=_natural_sort_key)
|
|
200
|
+
return found[latest], latest
|
|
201
|
+
|
|
202
|
+
def load(self, key: str, version: str | None = None) -> PromptSource:
|
|
203
|
+
"""
|
|
204
|
+
Load a prompt from a file laid out per the engine's pattern.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
key (str): Prompt identifier substituted into the pattern's `{key}`.
|
|
208
|
+
version (str | None): Specific version to load. Only valid for
|
|
209
|
+
versioned patterns; the latest version is used when omitted.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
PromptSource: The file contents. Versioned patterns take the version
|
|
213
|
+
from the file structure; unversioned patterns use a content hash.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
PromptNotFound: If no matching file exists under the root.
|
|
217
|
+
ValueError: If a version is requested but the pattern has no
|
|
218
|
+
`{version}` placeholder.
|
|
219
|
+
"""
|
|
220
|
+
if self.versioned:
|
|
221
|
+
path, resolved_version = self._resolve_versioned(key, version)
|
|
222
|
+
else:
|
|
223
|
+
if version is not None:
|
|
224
|
+
raise ValueError(
|
|
225
|
+
f"Engine pattern {self.pattern!r} has no '{{version}}' placeholder; "
|
|
226
|
+
"a specific version cannot be requested"
|
|
227
|
+
)
|
|
228
|
+
path = self.root / self.pattern.format(key=key)
|
|
229
|
+
if not path.is_file():
|
|
230
|
+
raise PromptNotFound(key=key, source=str(self.root))
|
|
231
|
+
resolved_version = None
|
|
232
|
+
|
|
233
|
+
text = path.read_text()
|
|
234
|
+
return PromptSource(
|
|
235
|
+
text=text,
|
|
236
|
+
source=str(path),
|
|
237
|
+
source_type="local",
|
|
238
|
+
version=resolved_version or _version_hash(text),
|
|
239
|
+
loaded_at=datetime.now(timezone.utc),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def __repr__(self) -> str:
|
|
243
|
+
return f"LocalPromptEngine(root={str(self.root)!r}, pattern={self.pattern!r})"
|
pyagentic/_base/_ref.py
CHANGED
pyagentic/_base/_spec.py
CHANGED
pyagentic/_base/_state.py
CHANGED
pyagentic/_base/_tool.py
CHANGED
|
@@ -256,7 +256,7 @@ def tool(description: str, condition: Callable[[Any], bool] = None, phases: list
|
|
|
256
256
|
Example:
|
|
257
257
|
```python
|
|
258
258
|
class FileAgent(BaseAgent):
|
|
259
|
-
|
|
259
|
+
__instructions__ = "You help manage files"
|
|
260
260
|
|
|
261
261
|
@tool("Read the contents of a file given its path")
|
|
262
262
|
def read_file(self, path: str) -> str:
|
pyagentic/models/response.py
CHANGED
|
@@ -2,6 +2,7 @@ from pydantic import BaseModel, Field, create_model
|
|
|
2
2
|
from typing import Type, Self, Union, Any, Literal, Optional
|
|
3
3
|
|
|
4
4
|
from pyagentic._base._tool import _ToolDefinition
|
|
5
|
+
from pyagentic._base._prompts import PromptSource
|
|
5
6
|
from pyagentic._base._agent._agent_state import _AgentState
|
|
6
7
|
|
|
7
8
|
from pyagentic._utils._typing import TypeCategory, analyze_type
|
|
@@ -97,6 +98,14 @@ class AgentResponse(BaseModel):
|
|
|
97
98
|
|
|
98
99
|
final_output: Union[str, Type[BaseModel]]
|
|
99
100
|
provider_info: ProviderInfo
|
|
101
|
+
prompt: Optional[PromptSource] = Field(
|
|
102
|
+
default=None,
|
|
103
|
+
description=(
|
|
104
|
+
"The prompt behind the agent's instructions: loaded from a PromptEngine "
|
|
105
|
+
"(e.g. source_type 'local'), or an 'inline' source named after the agent "
|
|
106
|
+
"for instructions declared as a plain string."
|
|
107
|
+
),
|
|
108
|
+
)
|
|
100
109
|
|
|
101
110
|
@classmethod
|
|
102
111
|
def from_agent_class(
|
pyagentic/policies/_policy.py
CHANGED
|
@@ -69,7 +69,7 @@ class Policy(Protocol[T]):
|
|
|
69
69
|
|
|
70
70
|
# Use in agent
|
|
71
71
|
class CounterAgent(BaseAgent):
|
|
72
|
-
|
|
72
|
+
__instructions__ = "You manage a counter"
|
|
73
73
|
counter: State[int] = spec.State(default=0, policies=[ValidationPolicy()])
|
|
74
74
|
```
|
|
75
75
|
"""
|
pyagentic/policies/messages.py
CHANGED
|
@@ -4,7 +4,7 @@ Prewritten message-context policies for managing agent context growth.
|
|
|
4
4
|
Attach these to an agent via the `__message_policies__` class attribute:
|
|
5
5
|
|
|
6
6
|
class MyAgent(BaseAgent):
|
|
7
|
-
|
|
7
|
+
__instructions__ = "..."
|
|
8
8
|
__message_policies__ = [
|
|
9
9
|
ToolOutputClipPolicy(max_chars=8000),
|
|
10
10
|
ToolEvictionPolicy(keep_last_n=5),
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
pyagentic/__init__.py,sha256=
|
|
1
|
+
pyagentic/__init__.py,sha256=eHUKxFTSj5UH8ncFubnmXpS1TomGgHHrwRlFnNzl_3U,2265
|
|
2
2
|
pyagentic/_version_scheme.py,sha256=hUYPvGhWz5L7RSuVxIqW3h10i4zNjfYXMofmoIk8ZBM,1569
|
|
3
3
|
pyagentic/logging.py,sha256=vWLKGx0jZurWQT5pMr6fLH5NgJnZd_eaB97geBnqlwI,1718
|
|
4
4
|
pyagentic/updates.py,sha256=JrBxbmTRxS-4tfSrai_wPK-dxSyCiNrYwC5MgrzGj54,870
|
|
5
5
|
pyagentic/_base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
pyagentic/_base/_depends.py,sha256=
|
|
7
|
-
pyagentic/_base/_exceptions.py,sha256=
|
|
6
|
+
pyagentic/_base/_depends.py,sha256=e9j6GiYvPJAmFExTHMeg-xcFGYvvfF9KtCstX5HDS7Y,2186
|
|
7
|
+
pyagentic/_base/_exceptions.py,sha256=Z1AvSEGJu0s8dE6F8iNGBHcNOmwlaS1gA1R15ezQYg4,4298
|
|
8
8
|
pyagentic/_base/_info.py,sha256=LeXn3VbEJkP4KmMpjY6bqXjHYnE-ZHLuKoRcrmrqqhs,3020
|
|
9
|
-
pyagentic/_base/_mcp.py,sha256=
|
|
10
|
-
pyagentic/_base/_metaclasses.py,sha256=
|
|
11
|
-
pyagentic/_base/
|
|
12
|
-
pyagentic/_base/
|
|
13
|
-
pyagentic/_base/
|
|
14
|
-
pyagentic/_base/
|
|
9
|
+
pyagentic/_base/_mcp.py,sha256=jclmFb5yB1szg9PTskPE2hLO3fvnFKJQFnnwTLdf0IY,8327
|
|
10
|
+
pyagentic/_base/_metaclasses.py,sha256=jvDuFoz2DAaVxu2vFkAbyT6W61euaDyKW3V0w5dS3kI,37067
|
|
11
|
+
pyagentic/_base/_prompts.py,sha256=aSUptdlKhUuJVrIb20FOvICzxp6pcmeb65Xf2goFpvk,9118
|
|
12
|
+
pyagentic/_base/_ref.py,sha256=E-1DGVoM2xub476BrHOkxTqzdw-8t_N6XuEerXDYYE8,3177
|
|
13
|
+
pyagentic/_base/_spec.py,sha256=Mx8tXF8ERpsuxizRXuq3wJavbXvCCGUn17YbjiOKP9Y,7670
|
|
14
|
+
pyagentic/_base/_state.py,sha256=-xYVIaKiaU9ERuPf-33nbUhbvI3DgIp36y1bYdYTwJU,1792
|
|
15
|
+
pyagentic/_base/_tool.py,sha256=03VQmY_Q_2NkNJxrHFkAzJJ09679Fb00h8ZHYrYDGUU,11038
|
|
15
16
|
pyagentic/_base/_validation.py,sha256=BBscZMrXB09LNNP17pYOAY6I4iCW8r2GBVStqBd-9f8,3833
|
|
16
17
|
pyagentic/_base/_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
pyagentic/_base/_agent/_agent.py,sha256=
|
|
18
|
-
pyagentic/_base/_agent/_agent_linking.py,sha256=
|
|
19
|
-
pyagentic/_base/_agent/_agent_state.py,sha256=
|
|
18
|
+
pyagentic/_base/_agent/_agent.py,sha256=eyPqISReeTC-yOjva5BNFTs5dr3Qtw-is_yNyt6JWo0,43659
|
|
19
|
+
pyagentic/_base/_agent/_agent_linking.py,sha256=2lVICn9SSk2dgDmdk_awIS7NaSLp-LXvpF_9L3Jaud8,1837
|
|
20
|
+
pyagentic/_base/_agent/_agent_state.py,sha256=0YkbUZT-dWHWgBUoMaPjTJ3rNxE-cX0_pIqrLa7nLWM,18628
|
|
20
21
|
pyagentic/_utils/_typing.py,sha256=5aRZZOVzR0ZgnBfejkw__1yoARhn70P5QzA81sxUkls,3681
|
|
21
22
|
pyagentic/_utils/_warnings.py,sha256=mjlBiYHGnnh5599Gsq5ke8mBFS6WsdhNa7jk20TT9_U,1330
|
|
22
23
|
pyagentic/api/__init__.py,sha256=7CklNYjxVj5XOyqTWvvWKZU6IYuU8CAfpOhNeGMH1d4,1712
|
|
@@ -44,21 +45,21 @@ pyagentic/llm/_mock.py,sha256=X_Rj7DP_RV_hKJth6exjl90bJGHC3xTy8-7da6lOg3k,3282
|
|
|
44
45
|
pyagentic/llm/_openai.py,sha256=y1rY35O2X78fbDgj8XNVRs6JDEvtBQ2w3NdXyb9_BbA,5994
|
|
45
46
|
pyagentic/llm/_provider.py,sha256=McxbFcRD7UTtpnmGwYieGGBrHb6JfzZMkfnFxhj6Vrw,2508
|
|
46
47
|
pyagentic/models/llm.py,sha256=hx8x4d2SJGxlqGSiN7Ewdopvo87IUyJELoR3wR8KV0s,5126
|
|
47
|
-
pyagentic/models/response.py,sha256=
|
|
48
|
+
pyagentic/models/response.py,sha256=nnSEzuH-DpFTWhV5mC5WZstX_Gm44YxZ96V3Zq7qeQU,6281
|
|
48
49
|
pyagentic/models/tracing.py,sha256=5_cGxOAb5I9qjLVwSdvIcxUzrZy56OUe5fxlxOkud_w,1218
|
|
49
50
|
pyagentic/policies/__init__.py,sha256=WCgpEA1KZ23iJW5bkT32sfP8jLSLso21xJY5ZUKJVTQ,451
|
|
50
51
|
pyagentic/policies/_events.py,sha256=hTZH4VT9n2Ba4ICNiec32CMPGptyO02tjsui01XEqUY,2586
|
|
51
52
|
pyagentic/policies/_list.py,sha256=Whz4T5lxodzKqZNrr7opolJPvfMhYfM6gn-0GTwuqeQ,5865
|
|
52
|
-
pyagentic/policies/_policy.py,sha256=
|
|
53
|
-
pyagentic/policies/messages.py,sha256=
|
|
53
|
+
pyagentic/policies/_policy.py,sha256=xsvm3NBraTBcNr8RcHRAasmvc6ttiAF1qCKFvG-kkRQ,3905
|
|
54
|
+
pyagentic/policies/messages.py,sha256=RVIr9GwUXjw3We1fZaXPxjByqZIvu4uNYcFti5YT6V4,8190
|
|
54
55
|
pyagentic/tracing/__init__.py,sha256=ddMOl_-Nf3WOhu-PYxSJ3kqHZ3eQXAOM5aySn9YmDtA,150
|
|
55
56
|
pyagentic/tracing/_basic.py,sha256=jUBKdkOPveCjBzEhHKS_Rvp5l38iZg-5nFIl6xEwOVk,8906
|
|
56
57
|
pyagentic/tracing/_langfuse.py,sha256=5b5tdT-gyRJP5HDcvCS4tn_k3AFDI6eNsXOd6On5gKQ,13986
|
|
57
58
|
pyagentic/tracing/_tracer.py,sha256=0uQeqJxPRjfQtvgglF2BPDQM_1fpLBxWB0PpU6KY1eA,8157
|
|
58
|
-
pyagentic_core-2.
|
|
59
|
-
pyagentic_core-2.
|
|
60
|
-
pyagentic_core-2.
|
|
61
|
-
pyagentic_core-2.
|
|
62
|
-
pyagentic_core-2.
|
|
63
|
-
pyagentic_core-2.
|
|
64
|
-
pyagentic_core-2.
|
|
59
|
+
pyagentic_core-2.15.1.dev1.dist-info/licenses/LICENSE,sha256=wcOzTj82hOc96HztJk2VzLB2hFHpdIGpjz8vvbpP1_s,1069
|
|
60
|
+
pyagentic_core-2.15.1.dev1.dist-info/METADATA,sha256=KY3iq_69tHikWHrDebuiW-f_br1APh1m4P80R_Rdhws,8212
|
|
61
|
+
pyagentic_core-2.15.1.dev1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
62
|
+
pyagentic_core-2.15.1.dev1.dist-info/scm_file_list.json,sha256=8UMtz5WpdhOSRNM1zB-dxNzobbueYJYYqfBJoM4n6_w,4883
|
|
63
|
+
pyagentic_core-2.15.1.dev1.dist-info/scm_version.json,sha256=ALVMEOHx9YqLEgimG8GUehjgTfSpP-8g_W6SqxQYmSE,162
|
|
64
|
+
pyagentic_core-2.15.1.dev1.dist-info/top_level.txt,sha256=lAWfd-ay434uEpSg2FM0ySN0o4vgNGE6D9dJkIhGyfY,10
|
|
65
|
+
pyagentic_core-2.15.1.dev1.dist-info/RECORD,,
|
{pyagentic_core-2.14.1.dev1.dist-info → pyagentic_core-2.15.1.dev1.dist-info}/scm_file_list.json
RENAMED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"docs/tools.md",
|
|
17
17
|
"docs/execution-modes.md",
|
|
18
18
|
"docs/structured-output.md",
|
|
19
|
+
"docs/prompts.md",
|
|
19
20
|
"docs/agent-linking.md",
|
|
20
21
|
"docs/index.md",
|
|
21
22
|
"docs/inheritance.md",
|
|
@@ -75,6 +76,7 @@
|
|
|
75
76
|
"tests/_base/test_policy_list.py",
|
|
76
77
|
"tests/_base/test_params.py",
|
|
77
78
|
"tests/_base/test_agent_forking.py",
|
|
79
|
+
"tests/_base/test_prompts.py",
|
|
78
80
|
"tests/_base/test_state.py",
|
|
79
81
|
"tests/_base/test_agent_provider.py",
|
|
80
82
|
"pyagentic/__init__.py",
|
|
@@ -128,6 +130,7 @@
|
|
|
128
130
|
"pyagentic/_base/_state.py",
|
|
129
131
|
"pyagentic/_base/_exceptions.py",
|
|
130
132
|
"pyagentic/_base/_ref.py",
|
|
133
|
+
"pyagentic/_base/_prompts.py",
|
|
131
134
|
"pyagentic/_base/_validation.py",
|
|
132
135
|
"pyagentic/_base/_depends.py",
|
|
133
136
|
"pyagentic/_base/_agent/__init__.py",
|
|
File without changes
|
{pyagentic_core-2.14.1.dev1.dist-info → pyagentic_core-2.15.1.dev1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|