pyagentic-core 2.1.0a2__tar.gz → 2.2.0__tar.gz
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_core-2.1.0a2 → pyagentic_core-2.2.0}/PKG-INFO +2 -1
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/_agent.py +115 -60
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/_agent_state.py +44 -4
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_tool.py +5 -4
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/PKG-INFO +2 -1
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/requires.txt +1 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyproject.toml +3 -2
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/LICENSE +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/README.md +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/__init__.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/__init__.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/__init__.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_agent/_agent_linking.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_exceptions.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_info.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_metaclasses.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_ref.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_spec.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_state.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_base/_validation.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_utils/_typing.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/_utils/_warnings.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/__init__.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_anthropic.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_gemini.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_mock.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_openai.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_openaiv1.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/llm/_provider.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/logging.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/models/llm.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/models/response.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/models/tracing.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/policies/__init__.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/policies/_events.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/policies/_policy.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/tracing/__init__.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/tracing/_basic.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/tracing/_langfuse.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/tracing/_tracer.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic/updates.py +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/SOURCES.txt +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/dependency_links.txt +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/top_level.txt +0 -0
- {pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyagentic-core
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Build LLM Agents in a Pythonic way
|
|
5
5
|
Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -20,6 +20,7 @@ Requires-Dist: typeguard>=4.4.4
|
|
|
20
20
|
Requires-Dist: c3linearize>=0.1.0
|
|
21
21
|
Requires-Dist: anthropic>=0.62.0
|
|
22
22
|
Requires-Dist: google-generativeai>=0.8.0
|
|
23
|
+
Requires-Dist: transitions>=0.9.3
|
|
23
24
|
Dynamic: license-file
|
|
24
25
|
|
|
25
26
|
# PyAgentic
|
|
@@ -13,6 +13,7 @@ from typing import (
|
|
|
13
13
|
Union,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
+
from transitions import Machine
|
|
16
17
|
from pydantic import BaseModel, ValidationError
|
|
17
18
|
|
|
18
19
|
from pyagentic.logging import get_logger
|
|
@@ -126,8 +127,6 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
126
127
|
api_key (str, optional): API key matching the model provider
|
|
127
128
|
provider (LLMProvider, optional): Pre-configured provider instance. Overrides
|
|
128
129
|
`model` and `api_key` if provided.
|
|
129
|
-
emitter (Callable, optional): Callback function to receive real-time updates
|
|
130
|
-
about the agent's execution (useful for WebSocket streaming)
|
|
131
130
|
tracer (AgentTracer, optional): Tracer instance for observability. Defaults
|
|
132
131
|
to BasicTracer if not provided.
|
|
133
132
|
max_call_depth (int): Maximum number of tool calling loops per run. Defaults to 1.
|
|
@@ -167,6 +166,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
167
166
|
__description__: ClassVar[str] # Optional: description for linked agents
|
|
168
167
|
__input_template__: ClassVar[str] = None # Optional: template for user input
|
|
169
168
|
__response_format__: ClassVar[Type[BaseModel]] = None # Optional: structured output format
|
|
169
|
+
phases: ClassVar[list[tuple[str, str, Callable]]] = None
|
|
170
170
|
|
|
171
171
|
# Generated Class Attributes (built by metaclass)
|
|
172
172
|
__response_model__: ClassVar[Type[AgentResponse]] = None # Pydantic response model
|
|
@@ -177,7 +177,6 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
177
177
|
model: str = None
|
|
178
178
|
api_key: str = None
|
|
179
179
|
provider: LLMProvider = None
|
|
180
|
-
emitter: Callable[[Any], str] = None
|
|
181
180
|
tracer: AgentTracer = None
|
|
182
181
|
max_call_depth: int = 1
|
|
183
182
|
|
|
@@ -254,6 +253,9 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
254
253
|
"""
|
|
255
254
|
self._check_llm_provider()
|
|
256
255
|
|
|
256
|
+
if self.phases:
|
|
257
|
+
self.state._build_phase_machine(self.phases)
|
|
258
|
+
|
|
257
259
|
# Use BasicTracer as default if no tracer provided
|
|
258
260
|
if not self.tracer:
|
|
259
261
|
self.tracer = BasicTracer()
|
|
@@ -314,13 +316,18 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
314
316
|
except Exception as e:
|
|
315
317
|
# Handle inference errors gracefully
|
|
316
318
|
logger.exception(e)
|
|
317
|
-
if self.emitter:
|
|
318
|
-
await _safe_run(self.emitter, EmitUpdate(status=Status.ERROR))
|
|
319
319
|
# Add error message to conversation history
|
|
320
320
|
self.state._messages.append(
|
|
321
321
|
Message(role="assistant", content="Failed to generate a response")
|
|
322
322
|
)
|
|
323
|
-
|
|
323
|
+
response = LLMResponse(
|
|
324
|
+
text=f"The LLM failed to generate a response: {e}", tool_calls=[]
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
if self.phases:
|
|
328
|
+
self.state._update_state_machine(phases=self.phases)
|
|
329
|
+
|
|
330
|
+
return response
|
|
324
331
|
|
|
325
332
|
@traced(SpanKind.AGENT)
|
|
326
333
|
async def _process_agent_call(self, tool_call: ToolCall) -> AgentResponse:
|
|
@@ -367,6 +374,8 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
367
374
|
self.state._messages.append(
|
|
368
375
|
self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
|
|
369
376
|
)
|
|
377
|
+
if self.phases:
|
|
378
|
+
self.state._update_state_machine(phases=self.phases)
|
|
370
379
|
return response
|
|
371
380
|
|
|
372
381
|
@traced(SpanKind.TOOL)
|
|
@@ -402,37 +411,15 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
402
411
|
# Handle validation errors for tool arguments
|
|
403
412
|
result = f"Function Args were invalid: {str(e)}"
|
|
404
413
|
compiled_args = {}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
logger.exception(e)
|
|
408
|
-
if self.emitter:
|
|
409
|
-
await _safe_run(
|
|
410
|
-
self.emitter,
|
|
411
|
-
ToolUpdate(
|
|
412
|
-
status=Status.ERROR, tool_call=tool_call.name, tool_args=kwargs
|
|
413
|
-
),
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
# Execute the tool, emitting status updates
|
|
414
|
+
self.tracer.record_exception(str(e))
|
|
415
|
+
logger.exception(e)
|
|
417
416
|
try:
|
|
418
|
-
if self.emitter:
|
|
419
|
-
await _safe_run(
|
|
420
|
-
self.emitter,
|
|
421
|
-
ToolUpdate(
|
|
422
|
-
status=Status.PROCESSING, tool_call=tool_call.name, tool_args=kwargs
|
|
423
|
-
),
|
|
424
|
-
)
|
|
425
417
|
if compiled_args:
|
|
426
418
|
result = await _safe_run(handler, **compiled_args)
|
|
427
419
|
self.tracer.set_attributes(result=result)
|
|
428
420
|
except TypeError as e:
|
|
429
421
|
self.tracer.record_exception(str(e))
|
|
430
422
|
logger.exception(e)
|
|
431
|
-
if self.emitter:
|
|
432
|
-
await _safe_run(
|
|
433
|
-
self.emitter,
|
|
434
|
-
ToolUpdate(status=Status.ERROR, tool_call=tool_call.name, tool_args=kwargs),
|
|
435
|
-
)
|
|
436
423
|
raise InvalidToolDefinition(
|
|
437
424
|
tool_name=tool_call.name,
|
|
438
425
|
message=f"Tool must have a serializable return type; {tool_def.return_type} failed to be casted to a string.",
|
|
@@ -442,11 +429,6 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
442
429
|
self.tracer.record_exception(str(e))
|
|
443
430
|
logger.exception(e)
|
|
444
431
|
result = f"Tool `{tool_call.name}` failed: {e}. Please kindly state to the user that is failed, provide state, and ask if they want to try again." # noqa E501
|
|
445
|
-
if self.emitter:
|
|
446
|
-
await _safe_run(
|
|
447
|
-
self.emitter,
|
|
448
|
-
ToolUpdate(status=Status.ERROR, tool_call=tool_call.name, tool_args=kwargs),
|
|
449
|
-
)
|
|
450
432
|
|
|
451
433
|
stringified_result = (
|
|
452
434
|
result.model_dump_json(indent=2)
|
|
@@ -458,6 +440,8 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
458
440
|
self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
|
|
459
441
|
)
|
|
460
442
|
|
|
443
|
+
if self.phases:
|
|
444
|
+
self.state._update_state_machine(phases=self.phases)
|
|
461
445
|
# Build and return the structured tool response
|
|
462
446
|
ToolResponseModel = self.__tool_response_models__[tool_call.name]
|
|
463
447
|
return ToolResponseModel(
|
|
@@ -479,7 +463,12 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
479
463
|
# Add all @tool decorated methods
|
|
480
464
|
for tool_def in self.__tool_defs__.values():
|
|
481
465
|
# Resolve StateRefs in parameters (e.g., ref.self.user_name -> actual value)
|
|
482
|
-
|
|
466
|
+
|
|
467
|
+
if self.phases and tool_def.phases:
|
|
468
|
+
if self.state.phase in tool_def.phases:
|
|
469
|
+
tool_defs.append(tool_def.resolve(self.agent_reference))
|
|
470
|
+
else:
|
|
471
|
+
tool_defs.append(tool_def.resolve(self.agent_reference))
|
|
483
472
|
|
|
484
473
|
# Add linked agents as tools
|
|
485
474
|
for name, linked_def in self.__linked_agents__.items():
|
|
@@ -492,10 +481,12 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
492
481
|
self, input_: str
|
|
493
482
|
) -> AsyncGenerator[Union[ToolResponse, AgentResponse, LLMResponse]]:
|
|
494
483
|
"""
|
|
495
|
-
|
|
496
|
-
|
|
484
|
+
Streams all intermediate responses as the agent executes. Yields LLMResponse for each
|
|
485
|
+
inference, ToolResponse for each tool execution, and finally AgentResponse with the
|
|
486
|
+
complete result.
|
|
497
487
|
|
|
498
|
-
|
|
488
|
+
This is the core execution method that enables real-time streaming and fine-grained
|
|
489
|
+
control over agent execution. The agent follows an agentic loop pattern:
|
|
499
490
|
1. Send user input and conversation history to the LLM
|
|
500
491
|
2. LLM decides to either call tools or respond with final output
|
|
501
492
|
3. If tools are called, execute them and feed results back to LLM
|
|
@@ -504,19 +495,23 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
504
495
|
Args:
|
|
505
496
|
input_ (str): The user input/query for the agent to process
|
|
506
497
|
|
|
507
|
-
|
|
508
|
-
AgentResponse:
|
|
509
|
-
-
|
|
510
|
-
-
|
|
511
|
-
-
|
|
512
|
-
- provider_info: Information about the LLM provider used
|
|
498
|
+
Yields:
|
|
499
|
+
Union[LLMResponse, ToolResponse, AgentResponse]: Responses in sequence:
|
|
500
|
+
- LLMResponse: Yielded each time the LLM is called (may happen multiple times)
|
|
501
|
+
- ToolResponse: Yielded for each tool execution
|
|
502
|
+
- AgentResponse: Final response with complete execution summary
|
|
513
503
|
|
|
514
504
|
Example:
|
|
515
505
|
```python
|
|
516
506
|
agent = MyAgent(model="openai::gpt-4o", api_key=API_KEY)
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
507
|
+
|
|
508
|
+
async for response in agent.step("Analyze this data"):
|
|
509
|
+
if isinstance(response, LLMResponse):
|
|
510
|
+
print(f"LLM thinking: {response.text}")
|
|
511
|
+
elif isinstance(response, ToolResponse):
|
|
512
|
+
print(f"Tool executed: {response.output}")
|
|
513
|
+
elif isinstance(response, AgentResponse):
|
|
514
|
+
print(f"Final: {response.final_output}")
|
|
520
515
|
```
|
|
521
516
|
"""
|
|
522
517
|
async with self.tracer.agent(
|
|
@@ -538,10 +533,6 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
538
533
|
agent_responses: list = []
|
|
539
534
|
processed_call_ids: set[str] = set()
|
|
540
535
|
|
|
541
|
-
# Emit initial status
|
|
542
|
-
if self.emitter:
|
|
543
|
-
await _safe_run(self.emitter, EmitUpdate(status=Status.GENERATING))
|
|
544
|
-
|
|
545
536
|
# Main agentic loop: LLM -> Tools -> LLM -> ...
|
|
546
537
|
depth = 0
|
|
547
538
|
final_ai_output: str | None = None
|
|
@@ -584,12 +575,6 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
584
575
|
response = await self._process_llm_inference()
|
|
585
576
|
final_ai_output = response.parsed if response.parsed else response.text
|
|
586
577
|
|
|
587
|
-
# Emit final success status
|
|
588
|
-
if self.emitter:
|
|
589
|
-
await _safe_run(
|
|
590
|
-
self.emitter, AiUpdate(status=Status.SUCCEDED, message=final_ai_output)
|
|
591
|
-
)
|
|
592
|
-
|
|
593
578
|
# Build the structured response
|
|
594
579
|
response_fields = {
|
|
595
580
|
"final_output": final_ai_output,
|
|
@@ -607,6 +592,32 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
607
592
|
yield response
|
|
608
593
|
|
|
609
594
|
async def run(self, input_: str) -> AgentResponse:
|
|
595
|
+
"""
|
|
596
|
+
Executes the agent with a message string and returns the final result.
|
|
597
|
+
|
|
598
|
+
This method consumes the entire step() generator and returns only the final
|
|
599
|
+
AgentResponse. Use this when you don't need intermediate streaming responses
|
|
600
|
+
and just want the final output.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
input_ (str): The user input/query for the agent to process
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
AgentResponse: Structured response containing:
|
|
607
|
+
- final_output: The final text or structured output from the LLM
|
|
608
|
+
- state: Current agent state after execution
|
|
609
|
+
- tool_responses: List of all tool calls and their outputs
|
|
610
|
+
- agent_responses: List of linked agent calls (if any)
|
|
611
|
+
- provider_info: Information about the LLM provider used
|
|
612
|
+
|
|
613
|
+
Example:
|
|
614
|
+
```python
|
|
615
|
+
agent = MyAgent(model="openai::gpt-4o", api_key=API_KEY)
|
|
616
|
+
response = await agent.run("What's the weather in San Francisco?")
|
|
617
|
+
print(response.final_output) # LLM's final answer
|
|
618
|
+
print(response.tool_responses) # Tools that were called
|
|
619
|
+
```
|
|
620
|
+
"""
|
|
610
621
|
final_response = None
|
|
611
622
|
async for res in self.step(input_):
|
|
612
623
|
final_response = res
|
|
@@ -614,13 +625,57 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
614
625
|
|
|
615
626
|
async def __call__(self, user_input: str) -> BaseModel:
|
|
616
627
|
"""
|
|
617
|
-
|
|
628
|
+
Customizable callable interface for the agent. Override this method to accept
|
|
629
|
+
structured, typed parameters that match your agent's purpose.
|
|
630
|
+
|
|
631
|
+
When this agent is linked to another agent, the parameters of this method become
|
|
632
|
+
the tool parameters that the LLM sees. This enables type-safe, structured agent
|
|
633
|
+
composition in multi-agent systems.
|
|
634
|
+
|
|
635
|
+
The default implementation accepts a single user_input string and forwards it to
|
|
636
|
+
run(). Override to provide a custom interface:
|
|
618
637
|
|
|
619
638
|
Args:
|
|
620
|
-
user_input (str): The user input to process
|
|
639
|
+
user_input (str): The user input to process (default implementation)
|
|
621
640
|
|
|
622
641
|
Returns:
|
|
623
642
|
AgentResponse: The agent's response
|
|
643
|
+
|
|
644
|
+
Example (Default Usage):
|
|
645
|
+
```python
|
|
646
|
+
agent = MyAgent(model="openai::gpt-4o", api_key=API_KEY)
|
|
647
|
+
response = await agent("What's the weather?")
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Example (Custom Implementation):
|
|
651
|
+
```python
|
|
652
|
+
class CoursePlannerAgent(BaseAgent):
|
|
653
|
+
__system_message__ = "You design course curricula"
|
|
654
|
+
__description__ = "Creates structured course plans"
|
|
655
|
+
|
|
656
|
+
async def __call__(
|
|
657
|
+
self,
|
|
658
|
+
goal: str,
|
|
659
|
+
experience: str,
|
|
660
|
+
context: Optional[str] = None
|
|
661
|
+
) -> CoursePlan:
|
|
662
|
+
# Build structured prompt from parameters
|
|
663
|
+
prompt = f"Goal: {goal}\\nExperience: {experience}"
|
|
664
|
+
if context:
|
|
665
|
+
prompt += f"\\nContext: {context}"
|
|
666
|
+
return await self.run(prompt)
|
|
667
|
+
|
|
668
|
+
# Call with structured parameters
|
|
669
|
+
planner = CoursePlannerAgent(model="openai::gpt-4o", api_key=API_KEY)
|
|
670
|
+
course = await planner(
|
|
671
|
+
goal="Learn ML",
|
|
672
|
+
experience="Python beginner",
|
|
673
|
+
context="Prefer hands-on projects"
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
# When linked to another agent, the LLM sees:
|
|
677
|
+
# Tool: planner(goal: str, experience: str, context: Optional[str])
|
|
678
|
+
```
|
|
624
679
|
"""
|
|
625
680
|
return await self.run(input_=user_input)
|
|
626
681
|
|
|
@@ -1,9 +1,10 @@
|
|
|
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, create_model, Field, PrivateAttr, computed_field
|
|
5
5
|
from jinja2 import Template
|
|
6
|
-
from typing import Optional, Literal
|
|
6
|
+
from typing import Optional, Literal, Callable
|
|
7
|
+
from transitions import Machine
|
|
7
8
|
|
|
8
9
|
from pyagentic._base._exceptions import InvalidStateRefNotFoundInState
|
|
9
10
|
from pyagentic._base._state import _StateDefinition
|
|
@@ -30,12 +31,41 @@ class _AgentState(BaseModel):
|
|
|
30
31
|
|
|
31
32
|
instructions: str
|
|
32
33
|
input_template: Optional[str] = "{{ user_message }}"
|
|
34
|
+
_machine: Machine = PrivateAttr(default=None)
|
|
33
35
|
_messages: list[Message] = PrivateAttr(default_factory=list)
|
|
34
36
|
_instructions_template: Template = PrivateAttr(default_factory=lambda: Template(source=""))
|
|
35
37
|
_input_template: Template = PrivateAttr(
|
|
36
38
|
default_factory=lambda: Template(source="{{ user_message }}")
|
|
37
39
|
)
|
|
38
40
|
|
|
41
|
+
def _build_phase_machine(self, phases: list[tuple[str, str, Callable]]) -> Machine:
|
|
42
|
+
if not phases:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
states = []
|
|
46
|
+
for source, dest, _ in phases:
|
|
47
|
+
if source not in states:
|
|
48
|
+
states.append(source)
|
|
49
|
+
if dest not in states:
|
|
50
|
+
states.append(dest)
|
|
51
|
+
|
|
52
|
+
machine = Machine(states=states, initial=states[0])
|
|
53
|
+
|
|
54
|
+
for source, dest, _ in phases:
|
|
55
|
+
machine.add_transition(
|
|
56
|
+
trigger=f"{source}_to_{dest}",
|
|
57
|
+
source=source,
|
|
58
|
+
dest=dest,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self._machine = machine
|
|
62
|
+
|
|
63
|
+
def _update_state_machine(self, phases):
|
|
64
|
+
for to_, from_, condition in phases:
|
|
65
|
+
if condition(self):
|
|
66
|
+
trigger = f"{to_}_to_{from_}"
|
|
67
|
+
getattr(self._machine, trigger)()
|
|
68
|
+
|
|
39
69
|
def model_post_init(self, state):
|
|
40
70
|
self._instructions_template = Template(source=self.instructions)
|
|
41
71
|
if self.input_template:
|
|
@@ -221,6 +251,10 @@ class _AgentState(BaseModel):
|
|
|
221
251
|
# now build the dataclass
|
|
222
252
|
return create_model(f"AgentState[{name}]", __base__=cls, **pydantic_fields)
|
|
223
253
|
|
|
254
|
+
@property
|
|
255
|
+
def phase(self) -> str:
|
|
256
|
+
return self._machine.state if self._machine else None
|
|
257
|
+
|
|
224
258
|
@property
|
|
225
259
|
def recent_message(self) -> Message:
|
|
226
260
|
"""
|
|
@@ -242,7 +276,10 @@ class _AgentState(BaseModel):
|
|
|
242
276
|
# start with all the normal dataclass fields
|
|
243
277
|
|
|
244
278
|
# now format your instruction template
|
|
245
|
-
|
|
279
|
+
if self.phase:
|
|
280
|
+
return self._instructions_template.render(phase=self.phase, **self.model_dump())
|
|
281
|
+
else:
|
|
282
|
+
return self._instructions_template.render(**self.model_dump())
|
|
246
283
|
|
|
247
284
|
@property
|
|
248
285
|
def messages(self) -> list[Message]:
|
|
@@ -270,7 +307,10 @@ class _AgentState(BaseModel):
|
|
|
270
307
|
if self.input_template:
|
|
271
308
|
data = self.model_dump()
|
|
272
309
|
data["user_message"] = message
|
|
273
|
-
|
|
310
|
+
if self.phase:
|
|
311
|
+
content = self._input_template.render(phase=self.phase, **self.model_dump())
|
|
312
|
+
else:
|
|
313
|
+
content = self._input_template.render(**self.model_dump())
|
|
274
314
|
else:
|
|
275
315
|
content = message
|
|
276
316
|
self._messages.append(Message(role="user", content=content))
|
|
@@ -43,12 +43,14 @@ class _ToolDefinition:
|
|
|
43
43
|
parameters: dict[str, tuple[TypeVar, ParamInfo]],
|
|
44
44
|
return_type: Type[Any],
|
|
45
45
|
condition: Callable[[Any], bool] = None,
|
|
46
|
+
phases: list[str] = None,
|
|
46
47
|
):
|
|
47
48
|
self.name: str = name
|
|
48
49
|
self.description: str = description
|
|
49
50
|
self.parameters: dict[str, tuple[TypeVar, ParamInfo]] = parameters
|
|
50
51
|
self.condition = condition
|
|
51
52
|
self.return_type = return_type
|
|
53
|
+
self.phases = phases if phases else []
|
|
52
54
|
|
|
53
55
|
def resolve(self, agent_reference: dict) -> Self:
|
|
54
56
|
new_parameters = {}
|
|
@@ -67,6 +69,7 @@ class _ToolDefinition:
|
|
|
67
69
|
parameters=new_parameters,
|
|
68
70
|
condition=self.condition,
|
|
69
71
|
return_type=self.return_type,
|
|
72
|
+
phases=self.phases,
|
|
70
73
|
)
|
|
71
74
|
|
|
72
75
|
def to_openai_spec(self) -> dict:
|
|
@@ -210,10 +213,7 @@ class _ToolDefinition:
|
|
|
210
213
|
return compiled_args
|
|
211
214
|
|
|
212
215
|
|
|
213
|
-
def tool(
|
|
214
|
-
description: str,
|
|
215
|
-
condition: Callable[[Any], bool] = None,
|
|
216
|
-
):
|
|
216
|
+
def tool(description: str, condition: Callable[[Any], bool] = None, phases: list[str] = None):
|
|
217
217
|
"""
|
|
218
218
|
Decorator to mark an agent method as a tool that the LLM can call.
|
|
219
219
|
|
|
@@ -281,6 +281,7 @@ def tool(
|
|
|
281
281
|
parameters=params,
|
|
282
282
|
condition=condition,
|
|
283
283
|
return_type=return_type,
|
|
284
|
+
phases=phases,
|
|
284
285
|
)
|
|
285
286
|
return fn
|
|
286
287
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyagentic-core
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Build LLM Agents in a Pythonic way
|
|
5
5
|
Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -20,6 +20,7 @@ Requires-Dist: typeguard>=4.4.4
|
|
|
20
20
|
Requires-Dist: c3linearize>=0.1.0
|
|
21
21
|
Requires-Dist: anthropic>=0.62.0
|
|
22
22
|
Requires-Dist: google-generativeai>=0.8.0
|
|
23
|
+
Requires-Dist: transitions>=0.9.3
|
|
23
24
|
Dynamic: license-file
|
|
24
25
|
|
|
25
26
|
# PyAgentic
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyagentic-core"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.2.0"
|
|
4
4
|
description = "Build LLM Agents in a Pythonic way"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -23,6 +23,7 @@ dependencies = [
|
|
|
23
23
|
"c3linearize>=0.1.0",
|
|
24
24
|
"anthropic>=0.62.0",
|
|
25
25
|
"google-generativeai>=0.8.0",
|
|
26
|
+
"transitions>=0.9.3",
|
|
26
27
|
]
|
|
27
28
|
|
|
28
29
|
[dependency-groups]
|
|
@@ -105,4 +106,4 @@ include = ["pyagentic*"]
|
|
|
105
106
|
compile-diagrams = "for file in docs/diagrams/source/*.d2; do d2 --layout elk \"$file\" \"docs/diagrams/$(basename \"$file\" .d2).svg\"; done"
|
|
106
107
|
build-docs = "task compile-diagrams && mkdocs build --clean"
|
|
107
108
|
serve-docs = "task compile-diagrams && mkdocs serve"
|
|
108
|
-
deploy-docs = "task compile-diagrams && mkdocs gh-deploy --force"
|
|
109
|
+
deploy-docs = "task compile-diagrams && mkdocs gh-deploy --force"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyagentic_core-2.1.0a2 → pyagentic_core-2.2.0}/pyagentic_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|