mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-10 17:29:39 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6df3e32640 | ||
|
|
fb36e1c50c | ||
|
|
ac5454cb00 | ||
|
|
bbe95795d1 | ||
|
|
bdaf5340f9 | ||
|
|
5579614001 | ||
|
|
2c4dd0e2d4 | ||
|
|
fd23d62841 | ||
|
|
5f3bcb78ac | ||
|
|
3c40c2278e | ||
|
|
1921b03aab | ||
|
|
f33bf57662 | ||
|
|
df21c419ea | ||
|
|
fe997d525c | ||
|
|
ca26988774 | ||
|
|
3600594316 | ||
|
|
097cd42890 | ||
|
|
595add2991 | ||
|
|
03a3aa420b | ||
|
|
9533abd8a4 | ||
|
|
39c9e50875 | ||
|
|
585b68d02c | ||
|
|
66481cc51c | ||
|
|
8e637daecd | ||
|
|
ba482895f7 |
No files matched your search
@@ -0,0 +1,57 @@
|
||||
name: Update API Reference
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/praisonai-agents/praisonaiagents/**/*.py'
|
||||
- 'src/praisonai/praisonai/**/*.py'
|
||||
- 'src/praisonai-ts/src/**/*.ts'
|
||||
- '.github/workflows/update-api-md.yml'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-api-md:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Generate api.md
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
python3 src/praisonai/scripts/generate_api_md.py
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if git diff --quiet api.md; then
|
||||
echo "changes=false" >> $GITHUB_OUTPUT
|
||||
echo "No changes to api.md"
|
||||
else
|
||||
echo "changes=true" >> $GITHUB_OUTPUT
|
||||
echo "api.md has been updated"
|
||||
git diff api.md | head -50
|
||||
fi
|
||||
|
||||
- name: Commit and push changes
|
||||
if: steps.check_changes.outputs.changes == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "docs: auto-update api.md [skip ci]"
|
||||
file_pattern: 'api.md'
|
||||
commit_user_name: "github-actions[bot]"
|
||||
commit_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
commit_author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
+2
-1
@@ -83,4 +83,5 @@ issue_details.json
|
||||
# MCP Registry tokens
|
||||
.mcpregistry_*
|
||||
|
||||
*.log
|
||||
*.log
|
||||
.internal
|
||||
@@ -1,105 +0,0 @@
|
||||
# Agent Migration Ledger
|
||||
|
||||
Generated: 2026-01-09
|
||||
|
||||
## 1. Agent Definition Files (Source of Truth)
|
||||
|
||||
### Core SDK (praisonai-agents)
|
||||
| File | Line | Description |
|
||||
|------|------|-------------|
|
||||
| `/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/agent/agent.py:50` | `class Agent` | Main Agent class definition |
|
||||
|
||||
### Related Classes
|
||||
| File | Line | Description |
|
||||
|------|------|-------------|
|
||||
| `/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/agents/agents.py:129` | `class Agents` | Multi-agent orchestrator (PraisonAIAgents) |
|
||||
| `/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/agents/autoagents.py:26` | `class AgentConfig` | Auto-generated agent config |
|
||||
|
||||
## 2. Agent Example Files Count
|
||||
|
||||
- **Total example files with Agent()**: 467+
|
||||
- **Key example directories**:
|
||||
- `/examples/python/` - Main Python examples
|
||||
- `/examples/context/` - Context management examples
|
||||
- `/examples/knowledge/` - Knowledge/RAG examples
|
||||
- `/examples/guardrails/` - Guardrail examples
|
||||
- `/examples/execution/` - Execution config examples
|
||||
- `/examples/eval/` - Evaluation examples
|
||||
|
||||
## 3. Agent Docs Pages Count
|
||||
|
||||
- **Total docs pages with Agent()**: 806+
|
||||
- **Key docs directories**:
|
||||
- `/docs/features/` - Feature documentation
|
||||
- `/docs/sdk/` - SDK reference
|
||||
- `/docs/guides/` - User guides
|
||||
- `/docs/examples/` - Example documentation
|
||||
|
||||
## 4. Agent API Signature (Current)
|
||||
|
||||
The Agent class uses a consolidated API pattern. Key parameters:
|
||||
|
||||
```python
|
||||
class Agent:
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Agent",
|
||||
role: str = "Assistant",
|
||||
goal: str = "Help the user",
|
||||
backstory: str = "",
|
||||
instructions: str = "",
|
||||
llm: Optional[str] = None,
|
||||
tools: Optional[List] = None,
|
||||
|
||||
# Consolidated feature params (Instance > Config > String > Bool > Default)
|
||||
memory: Optional[Any] = None, # bool | str | MemoryConfig | Memory instance
|
||||
knowledge: Optional[Any] = None, # bool | str | KnowledgeConfig | Knowledge instance
|
||||
output: Optional[Any] = None, # bool | str | OutputConfig
|
||||
execution: Optional[Any] = None, # bool | str | ExecutionConfig
|
||||
guardrails: Optional[Any] = None, # bool | str | GuardrailConfig | List
|
||||
context: Optional[Any] = None, # bool | str | ContextConfig
|
||||
|
||||
# Other params
|
||||
verbose: bool = False,
|
||||
markdown: bool = True,
|
||||
self_reflect: bool = False,
|
||||
max_reflect: int = 3,
|
||||
min_reflect: int = 1,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
## 5. Migration Status
|
||||
|
||||
| Category | Total | Verified | Status |
|
||||
|----------|-------|----------|--------|
|
||||
| Agent Definition | 1 | 1 | ✅ Complete |
|
||||
| Agent Examples | 467+ | 0 | ⏳ Pending |
|
||||
| Agent Docs | 806+ | 0 | ⏳ Pending |
|
||||
| CLI | TBD | 0 | ⏳ Pending |
|
||||
|
||||
## 6. Action Log
|
||||
|
||||
| Date | Action | Status |
|
||||
|------|--------|--------|
|
||||
| 2026-01-09 | Created Agent Migration Ledger | ✅ Complete |
|
||||
| 2026-01-09 | Verified Agent class definition | ✅ Complete |
|
||||
| 2026-01-09 | Workflow markdown parser fixed | ✅ Complete |
|
||||
|
||||
## 7. Final Verification (2026-01-09)
|
||||
|
||||
```
|
||||
=== AGENT API VERIFICATION ===
|
||||
✅ Agent class has consolidated params: memory, knowledge, planning, reflection, guardrails, web, context, autonomy, output, execution, caching, hooks, skills
|
||||
✅ Agent follows Instance > Config > String > Bool > Default precedence
|
||||
✅ Agent creation with consolidated params works correctly
|
||||
```
|
||||
|
||||
## 8. Notes
|
||||
|
||||
The Agent class already uses a well-designed consolidated API pattern:
|
||||
- All feature params support: `False` (disabled), `True` (defaults), `Config` (custom)
|
||||
- Precedence: Instance > Config > String > Bool > Default
|
||||
- Agent-centric imports: `from praisonaiagents import Agent`
|
||||
|
||||
No legacy patterns found in examples or docs that require updating.
|
||||
@@ -1,258 +0,0 @@
|
||||
# Workflow Migration Ledger
|
||||
|
||||
Generated: 2026-01-09
|
||||
|
||||
## 1. Workflow Definition Files (Source of Truth)
|
||||
|
||||
### Core SDK (praisonai-agents)
|
||||
| File | Classes/Configs Defined |
|
||||
|------|------------------------|
|
||||
| `/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/workflows/workflows.py:184` | `WorkflowStep` dataclass |
|
||||
| `/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/workflows/workflows.py:381` | `Workflow` dataclass |
|
||||
| `/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/workflows/workflow_configs.py` | `WorkflowOutputConfig`, `WorkflowPlanningConfig`, `WorkflowMemoryConfig`, `WorkflowHooksConfig`, `WorkflowStepContextConfig`, `WorkflowStepOutputConfig`, `WorkflowStepExecutionConfig`, `WorkflowStepRoutingConfig` |
|
||||
|
||||
### Wrapper/CLI (praisonai)
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `/Users/praison/praisonai-package/src/praisonai/praisonai/cli/features/workflow.py` | `WorkflowHandler` CLI handler |
|
||||
| `/Users/praison/praisonai-package/src/praisonai/praisonai/agents_generator.py` | Uses `YAMLWorkflowParser` |
|
||||
|
||||
## 2. Workflow Callsites (Files Using Workflow/WorkflowStep)
|
||||
|
||||
### Core SDK Internal
|
||||
- `/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/workflows/__init__.py`
|
||||
- `/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/workflows/yaml_parser.py`
|
||||
- `/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/__init__.py`
|
||||
|
||||
### Wrapper/CLI
|
||||
- `/Users/praison/praisonai-package/src/praisonai/praisonai/agents_generator.py`
|
||||
- `/Users/praison/praisonai-package/src/praisonai/praisonai/cli/features/workflow.py`
|
||||
- `/Users/praison/praisonai-package/src/praisonai/praisonai/cli/features/templates.py`
|
||||
- `/Users/praison/praisonai-package/src/praisonai/praisonai/cli/features/benchmark.py`
|
||||
- `/Users/praison/praisonai-package/src/praisonai/praisonai/cli/features/flow_display.py`
|
||||
- `/Users/praison/praisonai-package/src/praisonai/praisonai/cli/features/n8n.py`
|
||||
|
||||
### Tests
|
||||
- `/Users/praison/praisonai-package/src/praisonai-agents/tests/test_workflow_patterns.py`
|
||||
- `/Users/praison/praisonai-package/src/praisonai-agents/tests/unit/workflows/test_workflow_consolidated_api.py`
|
||||
|
||||
## 3. Workflow-Related Examples (67 files)
|
||||
|
||||
### Primary Workflow Examples (examples/python/workflows/)
|
||||
1. `/Users/praison/praisonai-package/examples/python/workflows/simple_workflow.py`
|
||||
2. `/Users/praison/praisonai-package/examples/python/workflows/simple_workflow_non_agentic.py`
|
||||
3. `/Users/praison/praisonai-package/examples/python/workflows/workflow_routing.py`
|
||||
4. `/Users/praison/praisonai-package/examples/python/workflows/workflow_routing_non_agentic.py`
|
||||
5. `/Users/praison/praisonai-package/examples/python/workflows/workflow_parallel.py`
|
||||
6. `/Users/praison/praisonai-package/examples/python/workflows/workflow_parallel_non_agentic.py`
|
||||
7. `/Users/praison/praisonai-package/examples/python/workflows/workflow_repeat.py`
|
||||
8. `/Users/praison/praisonai-package/examples/python/workflows/workflow_repeat_non_agentic.py`
|
||||
9. `/Users/praison/praisonai-package/examples/python/workflows/workflow_loop_csv.py`
|
||||
10. `/Users/praison/praisonai-package/examples/python/workflows/workflow_loop_csv_non_agentic.py`
|
||||
11. `/Users/praison/praisonai-package/examples/python/workflows/workflow_loop_list.py`
|
||||
12. `/Users/praison/praisonai-package/examples/python/workflows/workflow_loops.py`
|
||||
13. `/Users/praison/praisonai-package/examples/python/workflows/workflow_mixed_steps.py`
|
||||
14. `/Users/praison/praisonai-package/examples/python/workflows/workflow_branching.py`
|
||||
15. `/Users/praison/praisonai-package/examples/python/workflows/workflow_conditional.py`
|
||||
16. `/Users/praison/praisonai-package/examples/python/workflows/workflow_conditional_non_agentic.py`
|
||||
17. `/Users/praison/praisonai-package/examples/python/workflows/workflow_early_stop.py`
|
||||
18. `/Users/praison/praisonai-package/examples/python/workflows/workflow_checkpoints.py`
|
||||
19. `/Users/praison/praisonai-package/examples/python/workflows/workflow_with_agents.py`
|
||||
20. `/Users/praison/praisonai-package/examples/python/workflows/task_callbacks.py`
|
||||
|
||||
### Other Examples with Workflow References
|
||||
21. `/Users/praison/praisonai-package/examples/python/general/workflow_example_basic.py`
|
||||
22. `/Users/praison/praisonai-package/examples/python/general/workflow_example_detailed.py`
|
||||
23. `/Users/praison/praisonai-package/examples/python/general/prompt_chaining.py`
|
||||
24. `/Users/praison/praisonai-package/examples/python/general/async_example.py`
|
||||
25. `/Users/praison/praisonai-package/examples/python/general/autonomous-agent.py`
|
||||
26. `/Users/praison/praisonai-package/examples/python/processes/advanced-workflow-patterns.py`
|
||||
27. `/Users/praison/praisonai-package/examples/python/agents/autoagents_workflow_patterns.py`
|
||||
28. `/Users/praison/praisonai-package/examples/python/concepts/context-engineering-workflow.py`
|
||||
29. `/Users/praison/praisonai-package/examples/python/stateful/workflow-state-example.py`
|
||||
30. `/Users/praison/praisonai-package/examples/routing/routellm_workflow.py`
|
||||
31. `/Users/praison/praisonai-package/examples/yaml/workflows/example_usage.py`
|
||||
|
||||
### Use Case Examples (mention workflow in comments/prints)
|
||||
32-67. Various use case examples in `/Users/praison/praisonai-package/examples/python/usecases/`
|
||||
|
||||
## 4. Workflow-Related Docs Pages (Mintlify)
|
||||
|
||||
### Primary Workflow Docs
|
||||
1. `/Users/praison/PraisonAIDocs/docs/features/workflows.mdx`
|
||||
2. `/Users/praison/PraisonAIDocs/docs/features/workflow-patterns.mdx`
|
||||
3. `/Users/praison/PraisonAIDocs/docs/features/workflow-routing.mdx`
|
||||
4. `/Users/praison/PraisonAIDocs/docs/features/workflow-parallel.mdx`
|
||||
5. `/Users/praison/PraisonAIDocs/docs/features/workflow-loop.mdx`
|
||||
6. `/Users/praison/PraisonAIDocs/docs/features/workflow-repeat.mdx`
|
||||
7. `/Users/praison/PraisonAIDocs/docs/features/workflow-validation.mdx`
|
||||
8. `/Users/praison/PraisonAIDocs/docs/features/yaml-workflows.mdx`
|
||||
9. `/Users/praison/PraisonAIDocs/docs/features/autonomous-workflow.mdx`
|
||||
10. `/Users/praison/PraisonAIDocs/docs/cli/workflow.mdx`
|
||||
|
||||
### SDK Reference Docs
|
||||
11. `/Users/praison/PraisonAIDocs/docs/sdk/praisonaiagents/workflows/workflows.mdx`
|
||||
12. `/Users/praison/PraisonAIDocs/docs/sdk/praisonaiagents/workflows/workflow-manager.mdx`
|
||||
|
||||
### Guide Docs
|
||||
13. `/Users/praison/PraisonAIDocs/docs/guides/workflows/index.mdx`
|
||||
14. `/Users/praison/PraisonAIDocs/docs/guides/workflows/sequential.mdx`
|
||||
15. `/Users/praison/PraisonAIDocs/docs/guides/workflows/parallel.mdx`
|
||||
16. `/Users/praison/PraisonAIDocs/docs/guides/workflows/routing.mdx`
|
||||
17. `/Users/praison/PraisonAIDocs/docs/guides/workflows/orchestrator.mdx`
|
||||
|
||||
### JS/TypeScript Docs
|
||||
18. `/Users/praison/PraisonAIDocs/docs/js/workflows.mdx`
|
||||
19. `/Users/praison/PraisonAIDocs/docs/js/workflows-cli.mdx`
|
||||
|
||||
### Other Docs with Workflow References
|
||||
20. `/Users/praison/PraisonAIDocs/docs/concepts/process.mdx`
|
||||
21. `/Users/praison/PraisonAIDocs/docs/features/routing.mdx`
|
||||
22. `/Users/praison/PraisonAIDocs/docs/features/promptchaining.mdx`
|
||||
23. `/Users/praison/PraisonAIDocs/docs/features/evaluator-optimiser.mdx`
|
||||
24. `/Users/praison/PraisonAIDocs/docs/features/parallelisation.mdx`
|
||||
|
||||
## 5. Google Docs URLs
|
||||
|
||||
**None found in repo code/docs** (only in node_modules third-party dependencies)
|
||||
|
||||
## 6. Baseline API Signatures
|
||||
|
||||
### Workflow Dataclass Fields (Consolidated API - CURRENT)
|
||||
```
|
||||
name: str
|
||||
description: str = ""
|
||||
steps: List[Any] = field(default_factory=list)
|
||||
variables: Dict[str, Any] = field(default_factory=dict)
|
||||
file_path: Optional[str] = None
|
||||
default_agent_config: Optional[Dict[str, Any]] = None
|
||||
default_llm: Optional[str] = None
|
||||
context: Optional[Any] = None # Consolidated context param
|
||||
|
||||
# CONSOLIDATED FEATURE PARAMS
|
||||
output: Optional[Any] = None # Union[bool, str, WorkflowOutputConfig]
|
||||
planning: Optional[Any] = None # Union[bool, WorkflowPlanningConfig]
|
||||
memory: Optional[Any] = None # Union[bool, WorkflowMemoryConfig]
|
||||
hooks: Optional[Any] = None # WorkflowHooksConfig
|
||||
|
||||
# Internal resolved fields (private)
|
||||
_verbose, _stream, _planning_enabled, _planning_llm, _reasoning, _memory_config, _hooks_config
|
||||
```
|
||||
|
||||
### WorkflowStep Dataclass Fields (Consolidated API - CURRENT)
|
||||
```
|
||||
name: str
|
||||
description: str = ""
|
||||
action: str = ""
|
||||
condition: Optional[str] = None
|
||||
should_run: Optional[Callable] = None
|
||||
handler: Optional[Callable] = None
|
||||
agent_config: Optional[Dict[str, Any]] = None
|
||||
agent: Optional[Any] = None
|
||||
tools: Optional[List[Any]] = None
|
||||
loop_over: Optional[str] = None
|
||||
loop_var: str = "item"
|
||||
guardrail: Optional[Callable] = None
|
||||
images: Optional[List[str]] = None
|
||||
|
||||
# CONSOLIDATED FEATURE PARAMS
|
||||
context: Optional[Any] = None # Union[bool, List[str], WorkflowStepContextConfig]
|
||||
output: Optional[Any] = None # Union[str, WorkflowStepOutputConfig]
|
||||
execution: Optional[Any] = None # Union[str, WorkflowStepExecutionConfig]
|
||||
routing: Optional[Any] = None # Union[List[str], WorkflowStepRoutingConfig]
|
||||
|
||||
# Internal resolved fields (private)
|
||||
_context_from, _retain_full_context, _output_variable, _output_file, _output_json, _output_pydantic
|
||||
_async_execution, _quality_check, _rerun, _max_retries, _on_error, _next_steps, _branch_condition
|
||||
```
|
||||
|
||||
## 7. Migration Status
|
||||
|
||||
| Category | Total | Updated | Remaining |
|
||||
|----------|-------|---------|-----------|
|
||||
| Definition Files | 2 | 2 | 0 |
|
||||
| Callsites | 10 | 10 | 0 |
|
||||
| Examples | 67 | 67 | 0 |
|
||||
| Docs Pages | 24+ | 24+ | 0 |
|
||||
| CLI | 1 | 1 | 0 |
|
||||
| Tests | 2 | 2 | 0 |
|
||||
|
||||
## 8. Verification Commands
|
||||
|
||||
```bash
|
||||
# Check for legacy params in Workflow/WorkflowStep definitions
|
||||
grep -rn "verbose=\|stream=\|planning_llm=\|memory_config=\|on_workflow_start=\|on_step_" \
|
||||
/Users/praison/praisonai-package/src/praisonai-agents/praisonaiagents/workflows/workflows.py
|
||||
|
||||
# Check examples for legacy API usage
|
||||
grep -rn "Workflow(" /Users/praison/praisonai-package/examples/ | grep -E "verbose=|stream=|planning_llm="
|
||||
|
||||
# Run workflow tests
|
||||
cd /Users/praison/praisonai-package/src/praisonai-agents && python -m pytest tests/unit/workflows/ -v
|
||||
```
|
||||
|
||||
## 9. Action Log
|
||||
|
||||
| Date | Action | Status |
|
||||
|------|--------|--------|
|
||||
| 2026-01-09 | Created workflow_configs.py with consolidated config classes | ✅ Complete |
|
||||
| 2026-01-09 | Updated Workflow dataclass with consolidated params | ✅ Complete |
|
||||
| 2026-01-09 | Updated WorkflowStep dataclass with consolidated params | ✅ Complete |
|
||||
| 2026-01-09 | Updated workflows/__init__.py exports | ✅ Complete |
|
||||
| 2026-01-09 | Added test_workflow_consolidated_api.py (39 tests) | ✅ Complete |
|
||||
| 2026-01-09 | Updated test_workflow_patterns.py to use new API | ✅ Complete |
|
||||
| 2026-01-09 | Updated workflow examples (fixed imports) | ✅ Complete |
|
||||
| 2026-01-09 | Updated workflow_branching.py to use routing= param | ✅ Complete |
|
||||
| 2026-01-09 | Updated workflows.mdx docs with consolidated API | ✅ Complete |
|
||||
| 2026-01-09 | Updated SDK reference docs | ✅ Complete |
|
||||
| 2026-01-09 | Updated WorkflowManager._load_workflow to use consolidated API | ✅ Complete |
|
||||
| 2026-01-09 | Final verification: missing=0 for legacy dataclass fields | ✅ Complete |
|
||||
|
||||
## 10. Final Verification Results
|
||||
|
||||
```
|
||||
=== Workflow Dataclass Fields ===
|
||||
Total fields: 23
|
||||
Legacy params found: 0
|
||||
✅ No legacy params in dataclass fields
|
||||
|
||||
=== WorkflowStep Dataclass Fields ===
|
||||
Total fields: 32
|
||||
Legacy params found: 0
|
||||
✅ No legacy params in dataclass fields
|
||||
|
||||
=== Consolidated Params Present ===
|
||||
Workflow consolidated params: ['output', 'planning', 'memory', 'hooks', 'context']
|
||||
WorkflowStep consolidated params: ['context', 'output', 'execution', 'routing']
|
||||
|
||||
✅ VERIFICATION PASSED: missing=0 for legacy dataclass fields
|
||||
```
|
||||
|
||||
## 11. Test Results
|
||||
|
||||
- **39 consolidated API tests**: All passed
|
||||
- **86 workflow pattern tests**: All passed
|
||||
- **6 markdown parser tests**: All passed (FIXED)
|
||||
|
||||
**Total: 125 tests passed**
|
||||
|
||||
## 12. Final Verification (2026-01-09)
|
||||
|
||||
```
|
||||
=== WORKFLOW API VERIFICATION ===
|
||||
✅ Workflow consolidated params: verbose=True, planning=True
|
||||
✅ WorkflowStep consolidated params: max_retries=1, context_from=['step1']
|
||||
✅ WorkflowManager._parse_steps uses consolidated API
|
||||
✅ WorkflowManager._load_workflow uses consolidated API
|
||||
✅ All 199 workflow tests pass
|
||||
|
||||
=== TEST BREAKDOWN ===
|
||||
- test_workflow_patterns.py: 125 tests passed
|
||||
- test_workflow_planning.py: 16 tests passed
|
||||
- test_workflow_context_passing.py: 13 tests passed
|
||||
- test_workflow_markdown_parser.py: 10 tests passed
|
||||
- test_workflow_async.py: 6 tests passed
|
||||
- test_workflow_agents.py: 10 tests passed
|
||||
- test_workflow_tools.py: 10 tests passed
|
||||
- test_workflow_consolidated_api.py: 39 tests passed
|
||||
```
|
||||
@@ -0,0 +1,881 @@
|
||||
# PraisonAI API Reference
|
||||
|
||||
This file is auto-generated. Do not edit manually.
|
||||
Regenerate with: `praisonai docs api-md --write`
|
||||
|
||||
# Shared Types
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import ContextPolicy, GuardrailResult, HandoffConfig, HandoffCycleError, HandoffDepthError, HandoffError, HandoffInputData, HandoffResult, HandoffTimeoutError, ReflectionOutput, StepResult, TaskOutput, ToolResult, ToolValidationError, WorkflowContext
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class GuardrailResult">GuardrailResult.<a href="./src/praisonai-agents/praisonaiagents/guardrails/guardrail_result.py">from_tuple</a>(result: Tuple[bool, Any]) -> 'GuardrailResult'</code>
|
||||
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_dict</a>(data: Dict[str, Any]) -> 'HandoffConfig'</code>
|
||||
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">json</a>() -> Optional[str]</code>
|
||||
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">to_dict</a>() -> dict</code>
|
||||
* <code title="class ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
|
||||
# Agents
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import Agent, Agents, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PraisonAIAgents, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">achat</a>(prompt: str, temperature = 1.0, tools = None, output_json = None, output_pydantic = None, reasoning_steps = False, task_name = None, task_description = None, task_id = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">aexecute</a>(task, context = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">agent_id</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">analyze_prompt</a>(prompt: str) -> set</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">arun</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">astart</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat</a>(prompt, temperature = 1.0, tools = None, output_json = None, output_pydantic = None, reasoning_steps = False, stream = None, task_name = None, task_description = None, task_id = None, config = None, force_retrieval = False, skip_retrieval = False)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_with_context</a>(message: str, context: 'ContextPack', **kwargs) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">clean_json_output</a>(output: str) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">clear_history</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">console</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">context_manager</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">context_manager</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">display_generating</a>(content: str, start_time: float)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute</a>(task, context = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute_tool</a>(function_name, arguments)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">execute_tool_async</a>(function_name: str, arguments: Dict[str, Any]) -> Any</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">from_template</a>(uri: str, config: Optional[Dict[str, Any]] = None, offline: bool = False, **kwargs) -> 'Agent'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">generate_task</a>() -> 'Task'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_available_tools</a>() -> List[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_memory_context</a>(query: Optional[str] = None) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_recommended_stage</a>(prompt: str) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_rules_context</a>(file_path: Optional[str] = None, include_manual: Optional[List[str]] = None) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_skills_prompt</a>() -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to_async</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">iter_stream</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">launch</a>(path: str = '/', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_model</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">output_style</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">policy</a>(value)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">query</a>(question: str, **kwargs) -> 'RAGResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag_query</a>(question: str, **kwargs) -> 'RAGResult'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieval_config</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieve</a>(query: str, **kwargs) -> 'ContextPack'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rules_manager</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">session_id</a>() -> Optional[str]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">start</a>(prompt: str, **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', **kwargs)</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">switch_model</a>(new_model: str) -> None</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>()</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>(value)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">add_task</a>(task)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aexecute_task</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">append_to_state</a>(key: str, value: Any, max_length: Optional[int] = None) -> List[Any]</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_all_tasks</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">context_manager</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">current_plan</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">default_completion_checker</a>(task, agent_output)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">delete_state</a>(key: str) -> bool</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">display_token_usage</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">execute_task</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_agent_details</a>(agent_name)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_state</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_tasks_status</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_detailed_token_report</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_plan_markdown</a>() -> str</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_status</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_todo_markdown</a>() -> str</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_token_usage_summary</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">has_state</a>(key: str) -> bool</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">increment_state</a>(key: str, amount: float = 1, default: float = 0) -> float</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">launch</a>(path: str = '/agents', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">restore_session_state</a>(session_id: str) -> bool</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_all_tasks</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_task</a>(task_id)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
|
||||
* <code title="class Agents">Agents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
|
||||
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">astart</a>()</code>
|
||||
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">start</a>()</code>
|
||||
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">achat</a>(message: str, **kwargs) -> str</code>
|
||||
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">chat</a>(message: str, **kwargs) -> str</code>
|
||||
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">name</a>() -> str</code>
|
||||
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">rag</a>() -> Optional['RAG']</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">analyze_codebase_with_gitingest</a>(project_path: str) -> Dict[str, Any]</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">analyze_integration_points</a>(project_path: str) -> Dict[str, Any]</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">analyze_test_patterns</a>(project_path: str) -> Dict[str, Any]</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">build_implementation_blueprint</a>(feature_request: str, context_analysis: Dict[str, Any] = None) -> Dict[str, Any]</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">compile_context_documentation</a>(project_path: str) -> Dict[str, Any]</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">create_quality_gates</a>(requirements: List[str]) -> Dict[str, Any]</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">create_validation_framework</a>(project_path: str) -> Dict[str, Any]</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">execute_prp</a>(prp_file_path: str) -> Dict[str, Any]</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">extract_implementation_patterns</a>(project_path: str, ast_analysis: Dict[str, Any] = None) -> Dict[str, Any]</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">generate_comprehensive_prp</a>(feature_request: str, context_analysis: Dict[str, Any] = None) -> str</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">generate_feature_prp</a>(feature_request: str) -> str</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">get_agent_interaction_summary</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">log_debug</a>(message: str, **kwargs)</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">perform_ast_analysis</a>(project_path: str) -> Dict[str, Any]</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">save_comprehensive_session_report</a>()</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">save_markdown_output</a>(content: str, filename: str, section_title: str = 'Output')</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">setup_logging</a>()</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">setup_output_directories</a>()</code>
|
||||
* <code title="class ContextAgent">ContextAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">start</a>(input_text: str) -> str</code>
|
||||
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">aresearch</a>(query: str, instructions: Optional[str] = None, model: Optional[str] = None, summary_mode: Optional[Literal['auto', 'detailed', 'concise']] = None, web_search: Optional[bool] = None, code_interpreter: Optional[bool] = None, mcp_servers: Optional[List[Dict[str, Any]]] = None, file_ids: Optional[List[str]] = None, file_search: Optional[bool] = None, file_search_stores: Optional[List[str]] = None) -> DeepResearchResponse</code>
|
||||
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">async_openai_client</a>()</code>
|
||||
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">clarify</a>(query: str, model: Optional[str] = None) -> str</code>
|
||||
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">follow_up</a>(query: str, previous_interaction_id: str, model: Optional[str] = None) -> DeepResearchResponse</code>
|
||||
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">gemini_client</a>()</code>
|
||||
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">openai_client</a>()</code>
|
||||
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">research</a>(query: str, instructions: Optional[str] = None, model: Optional[str] = None, summary_mode: Optional[Literal['auto', 'detailed', 'concise']] = None, web_search: Optional[bool] = None, code_interpreter: Optional[bool] = None, mcp_servers: Optional[List[Dict[str, Any]]] = None, file_ids: Optional[List[str]] = None, file_search: Optional[bool] = None, file_search_stores: Optional[List[str]] = None, stream: bool = True) -> DeepResearchResponse</code>
|
||||
* <code title="class DeepResearchAgent">DeepResearchAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">rewrite_query</a>(query: str, model: Optional[str] = None) -> str</code>
|
||||
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">achat</a>(prompt: str, temperature: float = 0.2, tools: Optional[List[Any]] = None, output_json: Optional[str] = None, output_pydantic: Optional[Any] = None, reasoning_steps: bool = False, **kwargs) -> Union[str, Dict[str, Any]]</code>
|
||||
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">agenerate_image</a>(prompt: str, **kwargs) -> Dict[str, Any]</code>
|
||||
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">chat</a>(prompt: str, **kwargs) -> Dict[str, Any]</code>
|
||||
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">generate_image</a>(prompt: str, **kwargs) -> Dict[str, Any]</code>
|
||||
* <code title="class ImageAgent">ImageAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/image_agent.py">litellm</a>()</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">add_task</a>(task)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aexecute_task</a>(task_id)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">append_to_state</a>(key: str, value: Any, max_length: Optional[int] = None) -> List[Any]</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_all_tasks</a>()</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clean_json_output</a>(output: str) -> str</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">context_manager</a>()</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">current_plan</a>()</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">default_completion_checker</a>(task, agent_output)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">delete_state</a>(key: str) -> bool</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">display_token_usage</a>()</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">execute_task</a>(task_id)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_agent_details</a>(agent_name)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_state</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_all_tasks_status</a>()</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_detailed_token_report</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_plan_markdown</a>() -> str</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_status</a>(task_id)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_todo_markdown</a>() -> str</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_token_usage_summary</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">has_state</a>(key: str) -> bool</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">increment_state</a>(key: str, amount: float = 1, default: float = 0) -> float</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">launch</a>(path: str = '/agents', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">restore_session_state</a>(session_id: str) -> bool</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_all_tasks</a>()</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_task</a>(task_id)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, **kwargs)</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
|
||||
* <code title="class PraisonAIAgents">PraisonAIAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
|
||||
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">agent</a>()</code>
|
||||
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand</a>(prompt: str, strategy: ExpandStrategy = ..., context: Optional[str] = None) -> ExpandResult</code>
|
||||
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_basic</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
|
||||
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_creative</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
|
||||
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_detailed</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
|
||||
* <code title="class PromptExpanderAgent">PromptExpanderAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/prompt_expander_agent.py">expand_structured</a>(prompt: str, context: Optional[str] = None) -> ExpandResult</code>
|
||||
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">add_abbreviation</a>(abbrev: str, expansion: str) -> None</code>
|
||||
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">add_abbreviations</a>(abbreviations: Dict[str, str]) -> None</code>
|
||||
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">agent</a>()</code>
|
||||
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite</a>(query: str, strategy: RewriteStrategy = ..., chat_history: Optional[List[Dict[str, str]]] = None, context: Optional[str] = None, num_queries: int = None) -> RewriteResult</code>
|
||||
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_basic</a>(query: str) -> RewriteResult</code>
|
||||
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_contextual</a>(query: str, chat_history: List[Dict[str, str]]) -> RewriteResult</code>
|
||||
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_hyde</a>(query: str) -> RewriteResult</code>
|
||||
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_multi_query</a>(query: str, num_queries: int = None) -> RewriteResult</code>
|
||||
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_step_back</a>(query: str) -> RewriteResult</code>
|
||||
* <code title="class QueryRewriterAgent">QueryRewriterAgent.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">rewrite_sub_queries</a>(query: str) -> RewriteResult</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/context_agent.py">create_context_agent</a>(llm: Optional[Union[str, Any]] = None, **kwargs) -> ContextAgent</code>
|
||||
|
||||
# Tools
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import BaseTool, FunctionTool, ToolRegistry, Tools, get_registry, get_tool, register_tool, tool, validate_tool
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">__call__</a>(**kwargs) -> Any</code>
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">get_schema</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">run</a>(**kwargs) -> Any</code>
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">safe_run</a>(**kwargs) -> ToolResult</code>
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate</a>() -> bool</code>
|
||||
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_class</a>() -> bool</code>
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">__call__</a>(*args, **kwargs) -> Any</code>
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">injected_params</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">run</a>(**kwargs) -> Any</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">clear</a>() -> None</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">discover_plugins</a>() -> int</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_all</a>() -> Dict[str, Union[BaseTool, Callable]]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_base_tools</a>() -> List[BaseTool]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_tools</a>() -> List[str]</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None, overwrite: bool = False) -> None</code>
|
||||
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">unregister</a>(name: str) -> bool</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_registry</a>() -> ToolRegistry</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">register_tool</a>(tool: Union[BaseTool, Callable], name: Optional[str] = None) -> None</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">tool</a>(func: Optional[Callable] = None) -> Union[FunctionTool, Callable[[Callable], FunctionTool]]</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_tool</a>(tool: Any) -> bool</code>
|
||||
|
||||
# Workflows
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import Loop, Parallel, Pipeline, Repeat, Route, Workflow, WorkflowStep, loop, parallel, repeat, route
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">arun</a>(input: str = '', llm: Optional[str] = None, verbose: bool = False) -> Dict[str, Any]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">astart</a>(input: str = '', llm: Optional[str] = None, verbose: bool = False) -> Dict[str, Any]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">from_template</a>(uri: str, config: Optional[Dict[str, Any]] = None, offline: bool = False, **kwargs) -> 'Workflow'</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">memory_config</a>() -> Optional[Dict[str, Any]]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_step_complete</a>() -> Optional[Callable]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_step_error</a>() -> Optional[Callable]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_step_start</a>() -> Optional[Callable]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_workflow_complete</a>() -> Optional[Callable]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_workflow_start</a>() -> Optional[Callable]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">planning_llm</a>() -> Optional[str]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">reasoning</a>() -> bool</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">run</a>(input: str = '', llm: Optional[str] = None, verbose: bool = False, stream: bool = None) -> Dict[str, Any]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">start</a>(input: str = '', **kwargs) -> Dict[str, Any]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">stream</a>() -> bool</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Workflow">Workflow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">verbose</a>() -> bool</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">async_execution</a>() -> bool</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">branch_condition</a>() -> Optional[Dict[str, List[str]]]</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">context_from</a>() -> Optional[List[str]]</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">evaluator</a>()</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">executor</a>()</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">max_retries</a>() -> int</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">next_steps</a>() -> Optional[List[str]]</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">on_error</a>() -> str</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">output_file</a>() -> Optional[str]</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">output_json</a>() -> Optional[Any]</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">output_pydantic</a>() -> Optional[Any]</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">output_variable</a>() -> Optional[str]</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">quality_check</a>() -> bool</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">rerun</a>() -> bool</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">retain_full_context</a>() -> bool</code>
|
||||
* <code title="class WorkflowStep">WorkflowStep.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">loop</a>(step: Any, over: Optional[str] = None, from_csv: Optional[str] = None, from_file: Optional[str] = None, var_name: str = 'item') -> Loop</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">parallel</a>(steps: List) -> Parallel</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">repeat</a>(step: Any, until: Optional[Callable[[WorkflowContext], bool]] = None, max_iterations: int = 10) -> Repeat</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">route</a>(routes: Dict[str, List], default: Optional[List] = None) -> Route</code>
|
||||
|
||||
# DB
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import db
|
||||
```
|
||||
|
||||
# Memory
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import Memory
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_context_for_task</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_in_output: Optional[bool] = None) -> str</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">calculate_quality_metrics</a>(output: str, expected_output: str, llm: Optional[str] = None, custom_prompt: Optional[str] = None) -> Dict[str, float]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">compute_quality_score</a>(completeness: float, relevance: float, clarity: float, accuracy: float, weights: Dict[str, float] = None) -> float</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">finalize_task_output</a>(content: str, agent_name: str, quality_score: float, threshold: float = 0.7, metrics: Dict[str, Any] = None, task_id: str = None)</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_all_memories</a>() -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_learn_context</a>() -> str</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">learn</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_all</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_entity_only</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_long_term</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_short_term</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_user_memory</a>()</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search</a>(query: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_entity</a>(query: str, limit: int = 5) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_long_term</a>(query: str, limit: int = 5, relevance_cutoff: float = 0.0, min_quality: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_short_term</a>(query: str, limit: int = 5, min_quality: float = 0.0, relevance_cutoff: float = 0.0, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_user_memory</a>(user_id: str, query: str, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_with_quality</a>(query: str, min_quality: float = 0.0, memory_type: Literal['short', 'long'] = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_entity</a>(name: str, type_: str, desc: str, relations: str)</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_long_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None)</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_quality</a>(text: str, quality_score: float, task_id: Optional[str] = None, iteration: Optional[int] = None, metrics: Optional[Dict[str, float]] = None, memory_type: Literal['short', 'long'] = 'long') -> None</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_short_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None)</code>
|
||||
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_user_memory</a>(user_id: str, text: str, extra: Dict[str, Any] = None)</code>
|
||||
|
||||
# Knowledge
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import Chunking, Knowledge
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">SUPPORTED_CHUNKERS</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">__call__</a>(text: Union[str, List[str]], **kwargs) -> Union[List[Any], List[List[Any]]]</code>
|
||||
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">chunk</a>(text: Union[str, List[str]], **kwargs) -> Union[List[Any], List[List[Any]]]</code>
|
||||
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">chunker</a>()</code>
|
||||
* <code title="class Chunking">Chunking.<a href="./src/praisonai-agents/praisonaiagents/knowledge/chunking.py">embedding_model</a>()</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">add</a>(file_path, user_id = None, agent_id = None, run_id = None, metadata = None)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">chunker</a>()</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">config</a>()</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">delete</a>(memory_id)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">delete_all</a>(user_id = None, agent_id = None, run_id = None)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">get</a>(memory_id)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">get_all</a>(user_id = None, agent_id = None, run_id = None)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">get_corpus_stats</a>()</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">history</a>(memory_id)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">index</a>(path: str, incremental: bool = True, force: bool = False, include_glob: list = None, exclude_glob: list = None, user_id: str = None, agent_id: str = None, run_id: str = None)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">markdown</a>()</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">memory</a>()</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">normalize_content</a>(content)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">reset</a>()</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">search</a>(query, user_id = None, agent_id = None, run_id = None, rerank = None, **kwargs)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">store</a>(content, user_id = None, agent_id = None, run_id = None, metadata = None)</code>
|
||||
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">update</a>(memory_id, data)</code>
|
||||
|
||||
# RAG
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import RAG, RAGCitation, RAGConfig, RAGResult
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">aquery</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> RAGResult</code>
|
||||
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">aretrieve</a>(query: str, **kwargs) -> ContextPack</code>
|
||||
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">astream</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> AsyncIterator[str]</code>
|
||||
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">get_citations</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> List[Citation]</code>
|
||||
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">llm</a>()</code>
|
||||
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">query</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> RAGResult</code>
|
||||
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">retrieve</a>(query: str, **kwargs) -> ContextPack</code>
|
||||
* <code title="class RAG">RAG.<a href="./src/praisonai-agents/praisonaiagents/rag/pipeline.py">stream</a>(question: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, **kwargs) -> Iterator[str]</code>
|
||||
* <code title="class RAGCitation">RAGCitation.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'Citation'</code>
|
||||
* <code title="class RAGCitation">RAGCitation.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class RAGConfig">RAGConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'RAGConfig'</code>
|
||||
* <code title="class RAGConfig">RAGConfig.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">format_answer_with_citations</a>() -> str</code>
|
||||
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'RAGResult'</code>
|
||||
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">has_citations</a>() -> bool</code>
|
||||
* <code title="class RAGResult">RAGResult.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
|
||||
# Handoff
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import Handoff, RECOMMENDED_PROMPT_PREFIX, handoff, handoff_filters, prompt_with_handoff_instructions
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">default_tool_description</a>() -> str</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">default_tool_name</a>() -> str</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">execute_async</a>(source_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None) -> HandoffResult</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">execute_programmatic</a>(source_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None) -> HandoffResult</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_tool_function</a>(source_agent: 'Agent') -> Callable</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_description</a>() -> str</code>
|
||||
* <code title="class Handoff">Handoff.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">tool_name</a>() -> str</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">handoff</a>(agent: 'Agent', tool_name_override: Optional[str] = None, tool_description_override: Optional[str] = None, on_handoff: Optional[Callable] = None, input_type: Optional[type] = None, input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None, config: Optional[HandoffConfig] = None, context_policy: Optional[str] = None, timeout_seconds: Optional[float] = None, max_concurrent: Optional[int] = None, detect_cycles: Optional[bool] = None, max_depth: Optional[int] = None) -> Handoff</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">keep_last_n_messages</a>(n: int) -> Callable[[HandoffInputData], HandoffInputData]</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">remove_all_tools</a>(data: HandoffInputData) -> HandoffInputData</code>
|
||||
* <code title="class handoff_filters">handoff_filters.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">remove_system_messages</a>(data: HandoffInputData) -> HandoffInputData</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">prompt_with_handoff_instructions</a>(base_prompt: str, agent: 'Agent') -> str</code>
|
||||
|
||||
# Guardrails
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import LLMGuardrail
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">__call__</a>(task_output: TaskOutput) -> Tuple[bool, Union[str, TaskOutput]]</code>
|
||||
|
||||
# Skills
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import SkillLoader, SkillManager, SkillMetadata, SkillProperties
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">activate</a>(skill: LoadedSkill) -> bool</code>
|
||||
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load</a>(skill_path: str, activate: bool = False) -> Optional[LoadedSkill]</code>
|
||||
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_all_resources</a>(skill: LoadedSkill) -> None</code>
|
||||
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_assets</a>(skill: LoadedSkill) -> dict</code>
|
||||
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_metadata</a>(skill_path: str) -> Optional[LoadedSkill]</code>
|
||||
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_references</a>(skill: LoadedSkill) -> dict</code>
|
||||
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_scripts</a>(skill: LoadedSkill) -> dict</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate</a>(skill: LoadedSkill) -> bool</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate_by_name</a>(name: str) -> bool</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_skill</a>(skill_path: str) -> Optional[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">clear</a>() -> None</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">discover</a>(skill_dirs: Optional[List[str]] = None, include_defaults: bool = True) -> int</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_available_skills</a>() -> List[SkillMetadata]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_instructions</a>(name: str) -> Optional[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_skill</a>(name: str) -> Optional[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">load_resources</a>(name: str) -> bool</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">skill_names</a>() -> List[str]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">skills</a>() -> List[LoadedSkill]</code>
|
||||
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">to_prompt</a>() -> str</code>
|
||||
* <code title="class SkillMetadata">SkillMetadata.<a href="./src/praisonai-agents/praisonaiagents/skills/models.py">from_properties</a>(props: SkillProperties) -> 'SkillMetadata'</code>
|
||||
* <code title="class SkillProperties">SkillProperties.<a href="./src/praisonai-agents/praisonaiagents/skills/models.py">to_dict</a>() -> dict</code>
|
||||
|
||||
# Session
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import Session
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">Agent</a>(name: str, role: str = 'Assistant', instructions: Optional[str] = None, tools: Optional[List[Any]] = None, memory: bool = True, knowledge: Optional[List[str]] = None, **kwargs) -> 'Agent'</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">add_knowledge</a>(source: str) -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">add_memory</a>(text: str, memory_type: str = 'long', **metadata) -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">chat</a>(message: str, **kwargs) -> str</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">clear_memory</a>(memory_type: str = 'all') -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">create_agent</a>(*args, **kwargs) -> 'Agent'</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_context</a>(query: str, max_items: int = 3) -> str</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">get_state</a>(key: str, default: Any = None) -> Any</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">increment_state</a>(key: str, increment: int = 1, default: int = 0) -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">knowledge</a>() -> 'Knowledge'</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">memory</a>() -> 'Memory'</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">restore_state</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">save_state</a>(state_data: Dict[str, Any]) -> None</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">search_knowledge</a>(query: str, limit: int = 5) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">search_memory</a>(query: str, memory_type: str = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">send_message</a>(message: str, **kwargs) -> str</code>
|
||||
* <code title="class Session">Session.<a href="./src/praisonai-agents/praisonaiagents/session/api.py">set_state</a>(key: str, value: Any) -> None</code>
|
||||
|
||||
# Telemetry
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import get_telemetry
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/telemetry/__init__.py">get_telemetry</a>() -> 'MinimalTelemetry'</code>
|
||||
|
||||
# Observability
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import FlowDisplay, obs, track_workflow
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class FlowDisplay">FlowDisplay.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">display</a>()</code>
|
||||
* <code title="class FlowDisplay">FlowDisplay.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">start</a>()</code>
|
||||
* <code title="class FlowDisplay">FlowDisplay.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">stop</a>()</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/flow_display.py">track_workflow</a>()</code>
|
||||
|
||||
# Context
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import ContextManager, ManagerConfig
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">capture_llm_boundary</a>(messages: List[Dict[str, Any]], tools: List[Dict[str, Any]]) -> SnapshotHookData</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">estimate_tokens</a>(text: str, validate: bool = False) -> Tuple[int, Optional[EstimationMetrics]]</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_history</a>() -> List[Dict[str, Any]]</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_last_snapshot_hook</a>() -> Optional[SnapshotHookData]</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_resolved_config</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_stats</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">get_tool_budget</a>(tool_name: str) -> int</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">process</a>(messages: List[Dict[str, Any]], system_prompt: str = '', tools: Optional[List[Dict[str, Any]]] = None, trigger: Literal['turn', 'tool_call', 'manual', 'overflow'] = 'turn') -> Dict[str, Any]</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">register_snapshot_callback</a>(callback: Callable[[SnapshotHookData], None]) -> None</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">reset</a>() -> None</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">set_tool_budget</a>(tool_name: str, max_tokens: int, protected: bool = False) -> None</code>
|
||||
* <code title="class ContextManager">ContextManager.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">truncate_tool_output</a>(tool_name: str, output: str) -> str</code>
|
||||
* <code title="class ManagerConfig">ManagerConfig.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">from_env</a>() -> 'ManagerConfig'</code>
|
||||
* <code title="class ManagerConfig">ManagerConfig.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">merge</a>(**overrides) -> 'ManagerConfig'</code>
|
||||
* <code title="class ManagerConfig">ManagerConfig.<a href="./src/praisonai-agents/praisonaiagents/context/manager.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
|
||||
# Display
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import async_display_callbacks, clean_triple_backticks, display_error, display_generating, display_instruction, display_interaction, display_self_reflection, display_tool_call, error_logs, register_display_callback, sync_display_callbacks
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">clean_triple_backticks</a>(text: str) -> str</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_error</a>(message: str, console = None)</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_generating</a>(content: str = '', start_time: Optional[float] = None)</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_instruction</a>(message: str, console = None, agent_name: str = None, agent_role: str = None, agent_tools: List[str] = None)</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_interaction</a>(message, response, markdown = True, generation_time = None, console = None, agent_name = None, agent_role = None, agent_tools = None, task_name = None, task_description = None, task_id = None)</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_self_reflection</a>(message: str, console = None)</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">display_tool_call</a>(message: str, console = None, tool_name: str = None, tool_input: dict = None, tool_output: str = None)</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/main.py">register_display_callback</a>(display_type: str, callback_fn, is_async: bool = False)</code>
|
||||
|
||||
# Other
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonaiagents import AutoRagConfig, Citation, CodeExecutionStep, DeepResearchResponse, ExpandResult, ExpandStrategy, FileSearchCall, MCPCall, Provider, RagRetrievalPolicy, ReasoningStep, RewriteResult, RewriteStrategy, Task, WebSearchCall, warmup
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="class AutoRagConfig">AutoRagConfig.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">from_dict</a>(data: Dict[str, Any]) -> 'AutoRagConfig'</code>
|
||||
* <code title="class AutoRagConfig">AutoRagConfig.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class Citation">Citation.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">from_dict</a>(data: Dict[str, Any]) -> 'Citation'</code>
|
||||
* <code title="class Citation">Citation.<a href="./src/praisonai-agents/praisonaiagents/rag/models.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class DeepResearchResponse">DeepResearchResponse.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">get_all_sources</a>() -> List[Dict[str, str]]</code>
|
||||
* <code title="class DeepResearchResponse">DeepResearchResponse.<a href="./src/praisonai-agents/praisonaiagents/agent/deep_research_agent.py">get_citation_text</a>(citation: Citation) -> str</code>
|
||||
* <code title="class RewriteResult">RewriteResult.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">all_queries</a>() -> List[str]</code>
|
||||
* <code title="class RewriteResult">RewriteResult.<a href="./src/praisonai-agents/praisonaiagents/agent/query_rewriter_agent.py">primary_query</a>() -> str</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">execute_callback</a>(task_output: TaskOutput) -> None</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">execute_callback_sync</a>(task_output: TaskOutput) -> None</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">initialize_memory</a>()</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">store_in_memory</a>(content: str, agent_name: str = None, task_id: str = None)</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/__init__.py">warmup</a>(include_litellm: bool = False, include_openai: bool = True) -> dict</code>
|
||||
|
||||
# Wrapper (praisonai)
|
||||
|
||||
Types:
|
||||
```python
|
||||
from praisonai import CloudProvider, Deploy, DeployConfig, DeployType, PraisonAI, __version__
|
||||
```
|
||||
|
||||
# CLI
|
||||
|
||||
Methods:
|
||||
|
||||
* <code title="cli">praisonai <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai acp acp-main <a href="./src/praisonai/praisonai/cli/commands/acp.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents create <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents info <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai agents list <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
|
||||
* <code title="cli">praisonai audit agent-centric <a href="./src/praisonai/praisonai/cli/commands/audit.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark agent <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark benchmark-callback <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark cli <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark compare <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark litellm <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark profile <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark sdk <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai benchmark workflow <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
|
||||
* <code title="cli">praisonai call call-main <a href="./src/praisonai/praisonai/cli/commands/call.py">--help</a></code>
|
||||
* <code title="cli">praisonai chat <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai chat chat-main <a href="./src/praisonai/praisonai/cli/commands/chat.py">--help</a></code>
|
||||
* <code title="cli">praisonai code <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai code code-main <a href="./src/praisonai/praisonai/cli/commands/code.py">--help</a></code>
|
||||
* <code title="cli">praisonai commit commit-main <a href="./src/praisonai/praisonai/cli/commands/commit.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion bash <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion completion-callback <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion fish <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai completion zsh <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
|
||||
* <code title="cli">praisonai config get <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config list <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config path <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config reset <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai config set <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
|
||||
* <code title="cli">praisonai context add <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context clear <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context compact <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context export <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context grep <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context list <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context show <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context stats <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai context tail <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug acp <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug debug-callback <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug interactive <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug lsp <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai debug trace <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy aws <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy azure <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy docker <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai deploy gcp <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
|
||||
* <code title="cli">praisonai diag diag-callback <a href="./src/praisonai/praisonai/cli/commands/diag.py">--help</a></code>
|
||||
* <code title="cli">praisonai diag export <a href="./src/praisonai/praisonai/cli/commands/diag.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs api-md <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs generate <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs list <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs report <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs run <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs run-all <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs serve <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai docs stats <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor config <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor db <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor doctor-callback <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor env <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor mcp <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor network <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor performance <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor selftest <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai doctor tools <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
|
||||
* <code title="cli">praisonai endpoints list <a href="./src/praisonai/praisonai/cli/commands/endpoints.py">--help</a></code>
|
||||
* <code title="cli">praisonai endpoints test <a href="./src/praisonai/praisonai/cli/commands/endpoints.py">--help</a></code>
|
||||
* <code title="cli">praisonai environment check <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
|
||||
* <code title="cli">praisonai environment doctor <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
|
||||
* <code title="cli">praisonai environment view <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
|
||||
* <code title="cli">praisonai eval accuracy <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
|
||||
* <code title="cli">praisonai eval performance <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples info <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples list <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples report <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples run <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples run-all <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples stats <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks add <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks list <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks remove <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge add <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge index <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge list <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai knowledge search <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp logs <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp lsp-callback <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp start <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp status <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai lsp stop <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp add <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp describe <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp list <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp mcp-callback <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp remove <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp status <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp sync <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp test <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai mcp tools <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory add <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory clear <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory search <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory show <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai memory status <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
|
||||
* <code title="cli">praisonai package install <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai package list <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai package uninstall <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile imports <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile optimize <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile profile-callback <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile query <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile snapshot <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile startup <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai profile suite <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag chat <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag eval <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag index <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag query <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai rag serve <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
|
||||
* <code title="cli">praisonai realtime realtime-main <a href="./src/praisonai/praisonai/cli/commands/realtime.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe install <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe list <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai recipe run <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
|
||||
* <code title="cli">praisonai registry list <a href="./src/praisonai/praisonai/cli/commands/registry.py">--help</a></code>
|
||||
* <code title="cli">praisonai registry serve <a href="./src/praisonai/praisonai/cli/commands/registry.py">--help</a></code>
|
||||
* <code title="cli">praisonai research research-main <a href="./src/praisonai/praisonai/cli/commands/research.py">--help</a></code>
|
||||
* <code title="cli">praisonai retrieval index <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
|
||||
* <code title="cli">praisonai retrieval query <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
|
||||
* <code title="cli">praisonai retrieval search <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
|
||||
* <code title="cli">praisonai rules add <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
|
||||
* <code title="cli">praisonai rules clear <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
|
||||
* <code title="cli">praisonai rules list <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
|
||||
* <code title="cli">praisonai run <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
|
||||
* <code title="cli">praisonai run run-main <a href="./src/praisonai/praisonai/cli/commands/run.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule delete <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule describe <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule logs <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule restart <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule schedule-callback <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule start <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule stats <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule stop <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve serve-callback <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve start <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve status <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai serve stop <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
|
||||
* <code title="cli">praisonai session delete <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session export <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session import <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session list <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session resume <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai session show <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills create <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills info <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills install <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills list <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills search <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai skills validate <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise check <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise fix <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise init <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai standardise report <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
|
||||
* <code title="cli">praisonai templates create <a href="./src/praisonai/praisonai/cli/commands/templates.py">--help</a></code>
|
||||
* <code title="cli">praisonai templates list <a href="./src/praisonai/praisonai/cli/commands/templates.py">--help</a></code>
|
||||
* <code title="cli">praisonai test info <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
|
||||
* <code title="cli">praisonai test interactive <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
|
||||
* <code title="cli">praisonai test run <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
|
||||
* <code title="cli">praisonai todo add <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
|
||||
* <code title="cli">praisonai todo done <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
|
||||
* <code title="cli">praisonai todo list <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
|
||||
* <code title="cli">praisonai tools info <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
|
||||
* <code title="cli">praisonai tools list <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
|
||||
* <code title="cli">praisonai tools test <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces disable <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces enable <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces list <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai traces status <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
|
||||
* <code title="cli">praisonai train train-main <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui chat <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui code <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui gradio <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui realtime <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai ui ui-main <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
|
||||
* <code title="cli">praisonai version check <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
* <code title="cli">praisonai version show <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
* <code title="cli">praisonai version version-callback <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
|
||||
* <code title="cli">praisonai workflow create <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
|
||||
* <code title="cli">praisonai workflow list <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
|
||||
* <code title="cli">praisonai workflow run <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
|
||||
|
||||
# TypeScript
|
||||
|
||||
Types/Exports:
|
||||
```ts
|
||||
export { Agent, Agents, PraisonAIAgents, Router } from "./agent";
|
||||
export type { PraisonAIAgentsConfig, SimpleAgentConfig, SimpleRouteConfig, SimpleRouterConfig } from "./agent";
|
||||
export { ContextAgent, createContextAgent } from "./agent/context";
|
||||
export { Handoff, handoff, handoffFilters } from "./agent/handoff";
|
||||
export { ImageAgent, createImageAgent } from "./agent/image";
|
||||
export { PromptExpanderAgent, createPromptExpanderAgent } from "./agent/prompt-expander";
|
||||
export { QueryRewriterAgent, createQueryRewriterAgent } from "./agent/query-rewriter";
|
||||
export { DeepResearchAgent, createDeepResearchAgent } from "./agent/research";
|
||||
export { RouterAgent, createRouter, routeConditions } from "./agent/router";
|
||||
export { // Agent loop
|
||||
createAgentLoop, // DevTools
|
||||
enableDevTools, // MCP
|
||||
createMCP, // Middleware (renamed to avoid conflicts)
|
||||
createCachingMiddleware, // Models
|
||||
createModel, // Multimodal
|
||||
createImagePart, // Next.js
|
||||
createRouteHandler, // OAuth for MCP
|
||||
type OAuthClientProvider, // Server adapters
|
||||
createHttpHandler, // Speech & Transcription
|
||||
generateSpeech, // Telemetry (AI SDK v6 parity)
|
||||
configureTelemetry, // Tool Approval (AI SDK v6 parity)
|
||||
ApprovalManager, // Tools
|
||||
defineTool, // UI Message (AI SDK v6 parity)
|
||||
convertToModelMessages, AIAgentStep, AIEmbedManyResult, AIEmbedOptions, AIEmbedResult, AIFilePart, AIGenerateImageOptions, AIGenerateImageResult, AIGenerateObjectOptions, AIGenerateObjectResult, AIGenerateTextOptions, AIGenerateTextResult, AIImagePart, AIMiddleware, AIMiddlewareConfig, AIModelMessage, AISpan, AISpanKind, AISpanOptions, AISpanStatus, AIStreamObjectOptions, AIStreamObjectResult, AIStreamTextOptions, AIStreamTextResult, AITelemetryEvent, AITelemetrySettings, AITextPart, AIToolDefinition, AITracer, AgentLoop, DANGEROUS_PATTERNS, MODEL_ALIASES, SPEECH_MODELS, TRANSCRIPTION_MODELS, ToolApprovalDeniedError, ToolApprovalTimeoutError, aiEmbed, aiEmbedMany, aiGenerateImage, aiGenerateObject, aiGenerateText, aiStreamObject, aiStreamText, applyMiddleware, autoEnableDevTools, base64ToUint8Array, clearAICache, clearEvents, closeAllMCPClients, closeMCPClient, convertToUIMessages, createAILoggingMiddleware, createAISpan, createApprovalResponse, createDangerousPatternChecker, createDevToolsMiddleware, createExpressHandler, createFastifyHandler, createFilePart, createHonoHandler, createMultimodalMessage, createNestHandler, createPagesHandler, createPdfPart, createSystemMessage, createTelemetryMiddleware, createTelemetrySettings, createTextMessage, createTextPart, createToolSet, disableAITelemetry, disableDevTools, enableAITelemetry, functionToTool, getAICacheStats, getApprovalManager, getDevToolsState, getDevToolsUrl, getEvents, getMCPClient, getModel, getTelemetrySettings, getToolsNeedingApproval, getTracer, hasModelAlias, hasPendingApprovals, initOpenTelemetry, isDangerous, isDataUrl, isDevToolsEnabled, isTelemetryEnabled, isUrl, listModelAliases, mcpToolsToAITools, parseModel, pipeUIMessageStreamToResponse, recordEvent, resolveModelAlias, safeValidateUIMessages, setApprovalManager, stopAfterSteps, stopWhen, stopWhenNoToolCalls, toMessageContent, toUIMessageStreamResponse, transcribe, uint8ArrayToBase64, validateUIMessages, withApproval, withSpan, wrapModel } from "./ai";
|
||||
export { AutoAgents, createAutoAgents } from "./auto";
|
||||
export { BaseCache, FileCache, MemoryCache, createFileCache, createMemoryCache } from "./cache";
|
||||
export { CLI_SPEC_VERSION, executeCommand, parseArgs } from "./cli";
|
||||
export { // Autonomy Mode
|
||||
AutonomyManager, // Background Jobs
|
||||
JobQueue, // Checkpoints
|
||||
CheckpointManager, // Cost Tracker
|
||||
CostTracker, // External Agents
|
||||
BaseExternalAgent, // Fast Context
|
||||
FastContext, // Flow Display
|
||||
FlowDisplay, // Git Integration
|
||||
GitManager, // Interactive TUI
|
||||
InteractiveTUI, // N8N Integration
|
||||
N8NIntegration, // Repo Map
|
||||
RepoMap, // Sandbox Executor
|
||||
SandboxExecutor, // Scheduler
|
||||
Scheduler, // Slash Commands
|
||||
SlashCommandHandler, AiderAgent, ClaudeCodeAgent, CodexCliAgent, CommandValidator, CostTokenUsage, DEFAULT_BLOCKED_COMMANDS, DEFAULT_BLOCKED_PATHS, DEFAULT_IGNORE_PATTERNS, DiffViewer, FileCheckpointStorage, FileJobStorage, GeminiCliAgent, GenericExternalAgent, HistoryManager, MODEL_PRICING, MODE_POLICIES, MemoryCheckpointStorage, MemoryJobStorage, StatusDisplay, cliApprovalPrompt, createAutonomyManager, createCheckpointManager, createCostTracker, createDiffViewer, createExternalAgent, createFastContext, createFileCheckpointStorage, createFileJobStorage, createFlowDisplay, createGitManager, createHistoryManager, createInteractiveTUI, createJobQueue, createN8NIntegration, createRepoMap, createSandboxExecutor, createScheduler, createSlashCommandHandler, createStatusDisplay, cronExpressions, estimateTokens, executeSlashCommand, externalAgentAsTool, formatCost, getExternalAgentRegistry, getQuickContext, getRepoTree, isSlashCommand, parseSlashCommand, registerCommand, renderWorkflow, sandboxExec, triggerN8NWebhook } from "./cli/features";
|
||||
export { createDbAdapter, db, getDefaultDbAdapter, setDefaultDbAdapter } from "./db";
|
||||
export type { DbAdapter, DbConfig, DbMessage, DbRun, DbTrace } from "./db";
|
||||
export { MemoryPostgresAdapter, NeonPostgresAdapter, PostgresSessionStorage, createMemoryPostgres, createNeonPostgres, createPostgresSessionStorage } from "./db/postgres";
|
||||
export { MemoryRedisAdapter, UpstashRedisAdapter, createMemoryRedis, createUpstashRedis } from "./db/redis";
|
||||
export { SQLiteAdapter, createSQLiteAdapter } from "./db/sqlite";
|
||||
export { EvalSuite, accuracyEval, performanceEval, reliabilityEval } from "./eval";
|
||||
export { AgentEventBus, AgentEvents, EventEmitterPubSub, PubSub, createEventBus, createPubSub } from "./events";
|
||||
export { LLMGuardrail, createLLMGuardrail } from "./guardrails/llm-guardrail";
|
||||
export { // Computer Use
|
||||
createComputerUse, ComputerUseClient, createCLIApprovalPrompt, createComputerUseAgent } from "./integrations/computer-use";
|
||||
export { BaseObservabilityProvider, ConsoleObservabilityProvider, LangfuseObservabilityProvider, MemoryObservabilityProvider, ObservabilityTraceContext, createConsoleObservability, createLangfuseObservability, createMemoryObservability } from "./integrations/observability";
|
||||
export { // Natural Language Postgres
|
||||
createNLPostgres, NLPostgresClient, NLPostgresConfig, createPostgresTool } from "./integrations/postgres";
|
||||
export { // Slack
|
||||
createSlackBot, SlackBot, parseSlackMessage, verifySlackSignature } from "./integrations/slack";
|
||||
export { BaseVectorStore, ChromaVectorStore, MemoryVectorStore, PineconeVectorStore, QdrantVectorStore, VectorQueryResult, WeaviateVectorStore, createChromaStore, createMemoryVectorStore, createPineconeStore, createQdrantStore, createWeaviateStore } from "./integrations/vector";
|
||||
export { BaseVoiceProvider, ElevenLabsVoiceProvider, OpenAIVoiceProvider, createElevenLabsVoice, createOpenAIVoice } from "./integrations/voice";
|
||||
export { GraphRAG, GraphStore, createGraphRAG } from "./knowledge/graph-rag";
|
||||
export { BaseReranker, CohereReranker, CrossEncoderReranker, LLMReranker, createCohereReranker, createCrossEncoderReranker, createLLMReranker } from "./knowledge/reranker";
|
||||
export { // Provider classes
|
||||
OpenAIProvider, // Provider factory and utilities
|
||||
createProvider, // Provider registry (extensibility API)
|
||||
ProviderRegistry, // Types
|
||||
type LLMProvider, AnthropicProvider, BaseProvider, GoogleProvider, ProviderMessage, ProviderToolDefinition, createProviderRegistry, getAvailableProviders, getDefaultProvider, getDefaultRegistry, hasProvider, isProviderAvailable, listProviders, parseModelString, registerBuiltinProviders, registerProvider, unregisterProvider } from "./llm/providers";
|
||||
export { ADAPTERS, AISDK_PROVIDERS, COMMUNITY_PROVIDERS, PROVIDER_ALIASES } from "./llm/providers/ai-sdk/types";
|
||||
export { AutoMemory, AutoMemoryKnowledgeBase, AutoMemoryVectorStore, DEFAULT_POLICIES, createAutoMemory, createLLMSummarizer } from "./memory/auto-memory";
|
||||
export { FileMemory, createFileMemory } from "./memory/file-memory";
|
||||
export { Memory, createMemory } from "./memory/memory";
|
||||
export type { MemoryConfig, MemoryEntry } from "./memory/memory";
|
||||
export { // Adapters
|
||||
NoopObservabilityAdapter, // Constants
|
||||
OBSERVABILITY_TOOLS, // Global adapter management
|
||||
setObservabilityAdapter, // Types
|
||||
type SpanKind, ConsoleObservabilityAdapter, MemoryObservabilityAdapter, clearAdapterCache, createConsoleAdapter, createMemoryAdapter, createObservabilityAdapter, getObservabilityAdapter, getObservabilityToolInfo, hasObservabilityToolEnvVar, listObservabilityTools, noopAdapter, resetObservabilityAdapter, trace } from "./observability";
|
||||
export { Plan, PlanStep, PlanStorage, PlanningAgent, TaskAgent, TodoItem, TodoList, createPlan, createPlanStorage, createPlanningAgent, createTaskAgent, createTodoList } from "./planning";
|
||||
export { SkillManager, createSkillManager, parseSkillFile } from "./skills";
|
||||
export { AgentTelemetry, TelemetryCollector, cleanupTelemetry, createAgentTelemetry, disableTelemetry, enableTelemetry, getTelemetry } from "./telemetry";
|
||||
export { BaseTool, FunctionTool, ToolRegistry, ToolResult, ToolValidationError, createTool, getRegistry, getTool, registerTool, tool, validateTool } from "./tools";
|
||||
export { airweaveSearch, bedrockBrowserClick, bedrockBrowserFill, bedrockBrowserNavigate, bedrockCodeInterpreter, codeExecution, codeMode, createCustomTool, exaSearch, firecrawlCrawl, firecrawlScrape, parallelSearch, perplexitySearch, registerCustomTool, registerLocalTool, registerNpmTool, superagentGuard, superagentRedact, superagentVerify, tavilyCrawl, tavilyExtract, tavilySearch, valyuBioSearch, valyuCompanyResearch, valyuEconomicsSearch, valyuFinanceSearch, valyuPaperSearch, valyuPatentSearch, valyuSecSearch, valyuWebSearch } from "./tools/builtins";
|
||||
export { MissingDependencyError, MissingEnvVarError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getToolsRegistry, resetToolsRegistry } from "./tools/registry";
|
||||
export type { InstallHints, PraisonTool, RedactionHooks, RegisteredTool, ToolCapabilities, ToolExecutionContext, ToolExecutionResult, ToolFactory, ToolHooks, ToolInstallStatus, ToolLimits, ToolLogger, ToolMetadata, ToolMiddleware, ToolParameterProperty, ToolParameterSchema } from "./tools/registry";
|
||||
export { registerBuiltinTools, tools } from "./tools/tools";
|
||||
export { Workflow, loop, parallel, repeat, route } from "./workflows";
|
||||
export type { StepResult, WorkflowContext, WorkflowStep } from "./workflows";
|
||||
export { createWorkflowFromYAML, loadWorkflowFromFile, parseYAMLWorkflow, validateWorkflowDefinition } from "./workflows/yaml-parser";
|
||||
```
|
||||
|
||||
# Optional Plugins
|
||||
|
||||
External tools are available via `praisonai-tools` package:
|
||||
|
||||
```bash
|
||||
pip install praisonai-tools
|
||||
```
|
||||
|
||||
See [PraisonAI-tools](https://github.com/MervinPraison/PraisonAI-tools) for available tools.
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.5.8" \
|
||||
"praisonai>=3.8.2" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.5.8" \
|
||||
"praisonai>=3.8.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.5.8" \
|
||||
"praisonai>=3.8.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ examples/
|
||||
|
||||
| Category | Examples | Docs |
|
||||
|----------|----------|------|
|
||||
| **Consolidated Params** | [consolidated_params/](consolidated_params/) | [📖](https://docs.praison.ai/api/consolidated-params) |
|
||||
| **Agents** | [python/agents/](python/agents/) | [📖](https://docs.praison.ai/concepts/agents) |
|
||||
| **Workflows** | [python/workflows/](python/workflows/) | [📖](https://docs.praison.ai/features/workflows) |
|
||||
| **Model Router** | [python/agents/router-agent-cost-optimization.py](python/agents/router-agent-cost-optimization.py) | [📖](https://docs.praison.ai/features/model-router) |
|
||||
@@ -35,6 +36,21 @@ examples/
|
||||
| **Serve** | [serve/](serve/) | [📖](https://docs.praison.ai/cli/serve) |
|
||||
| **Endpoints** | [serve/](serve/) | [📖](https://docs.praison.ai/cli/endpoints) |
|
||||
|
||||
## Consolidated Params Examples
|
||||
|
||||
Agent-centric API with unified parameter resolution. Precedence: Instance > Config > Array > Dict > String > Bool > Default
|
||||
|
||||
| Example | Description |
|
||||
|---------|-------------|
|
||||
| [basic_agent.py](consolidated_params/basic_agent.py) | Minimal agent with memory |
|
||||
| [basic_agents.py](consolidated_params/basic_agents.py) | Multi-agent with memory+planning |
|
||||
| [basic_workflow.py](consolidated_params/basic_workflow.py) | Workflow with consolidated params |
|
||||
| [basic_memory.py](consolidated_params/basic_memory.py) | Memory presets (file, redis, postgres) |
|
||||
| [basic_guardrails.py](consolidated_params/basic_guardrails.py) | Guardrails with callable or config |
|
||||
| [basic_workflow_agentlike.py](consolidated_params/basic_workflow_agentlike.py) | Workflow with agent-like params |
|
||||
| [basic_step_override.py](consolidated_params/basic_step_override.py) | Step-level override of workflow defaults |
|
||||
| [advanced_workflow_full_features.py](consolidated_params/advanced_workflow_full_features.py) | All consolidated params |
|
||||
|
||||
## Serve Examples
|
||||
|
||||
| Example | Description | CLI Command |
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Consolidated Params Examples
|
||||
|
||||
Examples demonstrating the **Agent-Centric API** with consolidated parameters.
|
||||
|
||||
## Precedence Ladder
|
||||
|
||||
```
|
||||
Instance > Config > Array > Dict > String > Bool > Default
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Feature | Presets | Example |
|
||||
|---------|---------|---------|
|
||||
| `memory` | file, sqlite, redis, postgres, mem0, mongodb | `memory="sqlite"` |
|
||||
| `output` | minimal, normal, verbose, debug, silent | `output="verbose"` |
|
||||
| `execution` | fast, balanced, thorough, unlimited | `execution="balanced"` |
|
||||
| `planning` | reasoning, read_only, auto | `planning="reasoning"` |
|
||||
| `reflection` | minimal, standard, thorough | `reflection="standard"` |
|
||||
| `guardrails` | strict, permissive, safety | `guardrails="strict"` |
|
||||
| `web` | duckduckgo, tavily, google, bing, serper | `web="duckduckgo"` |
|
||||
| `context` | sliding_window, summarize, truncate | `context="sliding_window"` |
|
||||
| `autonomy` | suggest, auto_edit, full_auto | `autonomy="suggest"` |
|
||||
| `caching` | enabled, disabled, prompt | `caching="prompt"` |
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Examples (Single Feature)
|
||||
|
||||
| File | Feature | Description |
|
||||
|------|---------|-------------|
|
||||
| `basic_agent.py` | Agent | Minimal agent with memory |
|
||||
| `basic_memory.py` | memory | SQLite memory preset |
|
||||
| `basic_output.py` | output | Verbose output preset |
|
||||
| `basic_execution.py` | execution | Execution control |
|
||||
| `basic_guardrails.py` | guardrails | Callable validator |
|
||||
| `basic_reflection.py` | reflection | Self-reflection preset |
|
||||
| `basic_web.py` | web | Web search preset |
|
||||
| `basic_planning.py` | planning | Planning preset |
|
||||
| `basic_autonomy.py` | autonomy | Autonomy preset |
|
||||
| `basic_caching.py` | caching | Caching preset |
|
||||
| `basic_knowledge.py` | knowledge | RAG with sources |
|
||||
| `basic_context.py` | context | Context management |
|
||||
| `basic_hooks.py` | hooks | Lifecycle callbacks |
|
||||
|
||||
### Multi-Agent Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `basic_agents.py` | Multi-agent with memory and planning |
|
||||
|
||||
### Workflow Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `basic_workflow.py` | Simple workflow with output preset |
|
||||
| `basic_workflow_agentlike.py` | Workflow with agent-like params |
|
||||
| `basic_step_override.py` | Step-level param overrides |
|
||||
| `advanced_workflow_full_features.py` | All consolidated params |
|
||||
|
||||
### Advanced Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `advanced_output_execution.py` | Combined output + execution |
|
||||
|
||||
## Usage Forms
|
||||
|
||||
Each consolidated param supports multiple input forms:
|
||||
|
||||
```python
|
||||
# Bool - enable with defaults
|
||||
memory=True
|
||||
|
||||
# String preset
|
||||
memory="sqlite"
|
||||
|
||||
# Dict config
|
||||
memory={"backend": "sqlite", "user_id": "user123"}
|
||||
|
||||
# Config instance
|
||||
memory=MemoryConfig(backend="sqlite", user_id="user123")
|
||||
|
||||
# Array with overrides (for some params)
|
||||
guardrails=["strict", {"max_retries": 10}]
|
||||
```
|
||||
|
||||
## Running Examples
|
||||
|
||||
```bash
|
||||
# Set API key
|
||||
export OPENAI_API_KEY=your_key_here
|
||||
|
||||
# Run any example
|
||||
python basic_agent.py
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Advanced Output + Execution Example - Agent-Centric API
|
||||
|
||||
Demonstrates preset + override array syntax.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Advanced: Preset with overrides using array syntax
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
output=["verbose", {"stream": True, "metrics": True}],
|
||||
execution=["thorough", {"max_iter": 30}],
|
||||
)
|
||||
|
||||
response = agent.start("Explain quantum computing in simple terms.")
|
||||
print(response)
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Advanced Workflow with all consolidated params configured."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.workflows import Workflow, WorkflowStep
|
||||
from praisonaiagents.workflows.workflow_configs import (
|
||||
WorkflowOutputConfig, WorkflowPlanningConfig, WorkflowMemoryConfig,
|
||||
)
|
||||
|
||||
# Full-featured workflow with all consolidated params
|
||||
workflow = Workflow(
|
||||
name="FullFeaturedWorkflow",
|
||||
description="Demonstrates all agent-like consolidated params",
|
||||
steps=[
|
||||
WorkflowStep(
|
||||
name="researcher",
|
||||
action="Research: {{input}}",
|
||||
agent=Agent(instructions="You are a researcher."),
|
||||
knowledge=["research_docs/"], # Step-specific knowledge
|
||||
web="duckduckgo", # Use DuckDuckGo for this step
|
||||
),
|
||||
WorkflowStep(
|
||||
name="analyzer",
|
||||
action="Analyze the research",
|
||||
agent=Agent(instructions="You are an analyst."),
|
||||
reflection="thorough", # Thorough reflection for analysis
|
||||
guardrails="strict", # Strict validation
|
||||
),
|
||||
WorkflowStep(
|
||||
name="writer",
|
||||
action="Write the final report",
|
||||
agent=Agent(instructions="You are a writer."),
|
||||
autonomy=True, # Enable autonomy for writing
|
||||
),
|
||||
],
|
||||
# Workflow-level consolidated params
|
||||
output=WorkflowOutputConfig(verbose=True, stream=True),
|
||||
planning=WorkflowPlanningConfig(enabled=True, reasoning=True),
|
||||
memory=WorkflowMemoryConfig(backend="file"),
|
||||
# Agent-like params
|
||||
autonomy=True,
|
||||
knowledge=["shared_docs/"],
|
||||
guardrails=True,
|
||||
web=True,
|
||||
reflection=True,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Workflow: {workflow.name}")
|
||||
print(f"Output verbose: {workflow._verbose}")
|
||||
print(f"Planning enabled: {workflow._planning_enabled}")
|
||||
print(f"Memory config: {workflow._memory_config}")
|
||||
print(f"Autonomy: {workflow.autonomy}")
|
||||
print(f"Knowledge: {workflow.knowledge}")
|
||||
print(f"Guardrails: {workflow.guardrails}")
|
||||
print(f"Web: {workflow.web}")
|
||||
print(f"Reflection: {workflow.reflection}")
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Basic Agent example with consolidated params."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Minimal agent with memory
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
memory=True,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.chat("Hello!")
|
||||
print(response)
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Basic Agents (multi-agent) example with consolidated params."""
|
||||
from praisonaiagents import Agent, Agents
|
||||
|
||||
# Create agents
|
||||
writer = Agent(instructions="You write content.")
|
||||
editor = Agent(instructions="You edit content.")
|
||||
|
||||
# Multi-agent with consolidated params
|
||||
agents = Agents(
|
||||
agents=[writer, editor],
|
||||
memory=True,
|
||||
planning=True,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = agents.start("Write a haiku about AI")
|
||||
print(result)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Autonomy Example - Agent-Centric API
|
||||
|
||||
Demonstrates autonomy with consolidated params.
|
||||
Presets: suggest, auto_edit, full_auto
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable autonomy with preset
|
||||
agent = Agent(
|
||||
instructions="You are an autonomous assistant.",
|
||||
autonomy="suggest", # Presets: suggest, auto_edit, full_auto
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Help me organize my project files.")
|
||||
print(response)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Basic Caching Example - Agent-Centric API
|
||||
|
||||
Demonstrates caching with consolidated params.
|
||||
Presets: enabled, disabled, prompt
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable caching with preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with caching enabled.",
|
||||
caching="prompt", # Presets: enabled, disabled, prompt
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# First call - will cache
|
||||
response = agent.start("What is the capital of France?")
|
||||
print(response)
|
||||
|
||||
# Second call - may use cache
|
||||
response = agent.start("What is the capital of France?")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Context Example - Agent-Centric API
|
||||
|
||||
Demonstrates context management with consolidated params.
|
||||
Presets: sliding_window, summarize, truncate
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable context management with preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with context management.",
|
||||
context="sliding_window", # Presets: sliding_window, summarize, truncate
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Tell me a long story about a dragon.")
|
||||
print(response)
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Basic Execution Example - Agent-Centric API
|
||||
|
||||
Demonstrates execution preset usage.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Use fast execution preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
execution="fast", # Presets: fast, balanced, thorough, unlimited
|
||||
)
|
||||
|
||||
response = agent.start("What is the capital of France?")
|
||||
print(response)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Basic Guardrails Example - Agent-Centric API
|
||||
|
||||
Demonstrates guardrails with callable validator.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Define a simple guardrail validator
|
||||
def validate_response(output):
|
||||
"""Validate that response doesn't contain forbidden words."""
|
||||
forbidden = ["error", "fail", "cannot"]
|
||||
text = str(output).lower()
|
||||
for word in forbidden:
|
||||
if word in text:
|
||||
return (False, f"Response contains forbidden word: {word}")
|
||||
return (True, output)
|
||||
|
||||
# Basic: Use callable guardrail
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
guardrails=validate_response,
|
||||
)
|
||||
|
||||
response = agent.start("What is 2 + 2?")
|
||||
print(response)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Basic Hooks Example - Agent-Centric API
|
||||
|
||||
Demonstrates lifecycle hooks with consolidated params.
|
||||
Valid keys: on_step, on_tool_call, middleware
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Define hook callbacks
|
||||
def on_step_callback(step_info):
|
||||
print(f"Step: {step_info}")
|
||||
|
||||
def on_tool_call_callback(tool_info):
|
||||
print(f"Tool called: {tool_info}")
|
||||
|
||||
# Basic: Enable hooks with dict
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
hooks={
|
||||
"on_step": on_step_callback,
|
||||
"on_tool_call": on_tool_call_callback,
|
||||
},
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("What is 2 + 2?")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Knowledge (RAG) Example - Agent-Centric API
|
||||
|
||||
Demonstrates knowledge with consolidated params.
|
||||
Supports: bool, list of sources, dict config, KnowledgeConfig
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable knowledge with list of sources
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with document knowledge.",
|
||||
knowledge=["docs/"], # List of file/folder paths
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("What does the documentation say about installation?")
|
||||
print(response)
|
||||
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Basic Memory Example - Agent-Centric API
|
||||
|
||||
Demonstrates the simplest memory usage with consolidated params.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable memory with preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with memory.",
|
||||
memory="sqlite", # Preset: sqlite, redis, postgres, file
|
||||
)
|
||||
|
||||
# Run a simple task
|
||||
response = agent.start("Remember that my favorite color is blue.")
|
||||
print(response)
|
||||
|
||||
# Follow-up to test memory
|
||||
response = agent.start("What is my favorite color?")
|
||||
print(response)
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Basic Output Example - Agent-Centric API
|
||||
|
||||
Demonstrates output preset usage.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Use verbose output preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
output="verbose", # Presets: minimal, normal, verbose, debug, silent
|
||||
)
|
||||
|
||||
response = agent.start("What is 2 + 2?")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Planning Example - Agent-Centric API
|
||||
|
||||
Demonstrates planning with consolidated params.
|
||||
Presets: reasoning, read_only, auto
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable planning with preset
|
||||
agent = Agent(
|
||||
instructions="You are a strategic planner.",
|
||||
planning="reasoning", # Presets: reasoning, read_only, auto
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Plan a 3-day trip to Tokyo.")
|
||||
print(response)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Reflection Example - Agent-Centric API
|
||||
|
||||
Demonstrates reflection (self-reflect) with consolidated params.
|
||||
Presets: minimal, standard, thorough
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable reflection with preset
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant that reflects on your answers.",
|
||||
reflection="standard", # Presets: minimal, standard, thorough
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("Explain quantum computing in simple terms.")
|
||||
print(response)
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Basic example showing step-level override of workflow defaults."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.workflows import Workflow, WorkflowStep
|
||||
|
||||
# Workflow with defaults
|
||||
workflow = Workflow(
|
||||
name="StepOverrideWorkflow",
|
||||
steps=[
|
||||
WorkflowStep(
|
||||
name="step1",
|
||||
action="Process with workflow defaults",
|
||||
agent=Agent(instructions="You are a processor."),
|
||||
# Uses workflow-level web=True (inherited)
|
||||
),
|
||||
WorkflowStep(
|
||||
name="step2",
|
||||
action="Process with step override",
|
||||
agent=Agent(instructions="You are a processor."),
|
||||
# Override workflow default
|
||||
web=False, # Disable web for this step
|
||||
reflection="thorough", # Use thorough reflection preset
|
||||
),
|
||||
],
|
||||
# Workflow defaults
|
||||
web=True,
|
||||
reflection=True,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Workflow web default: {workflow.web}")
|
||||
print(f"Step1 web: {workflow.steps[0].web}") # None (uses workflow default)
|
||||
print(f"Step2 web: {workflow.steps[1].web}") # False (overridden)
|
||||
print(f"Step2 reflection: {workflow.steps[1].reflection}") # thorough
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Basic Web Search Example - Agent-Centric API
|
||||
|
||||
Demonstrates web search with consolidated params.
|
||||
Presets: duckduckgo, tavily, google, bing, serper, search_only, fetch_only
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Basic: Enable web search with preset
|
||||
agent = Agent(
|
||||
instructions="You are a research assistant with web access.",
|
||||
web="duckduckgo", # Presets: duckduckgo, tavily, google, bing, serper
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.start("What are the latest AI developments in 2024?")
|
||||
print(response)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Basic Workflow Example - Agent-Centric API
|
||||
|
||||
Demonstrates minimal workflow with consolidated params.
|
||||
"""
|
||||
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.workflows import Workflow, WorkflowStep
|
||||
|
||||
# Create a simple workflow
|
||||
workflow = Workflow(
|
||||
name="simple_workflow",
|
||||
output="verbose", # Workflow-level output preset
|
||||
)
|
||||
|
||||
# Add a step
|
||||
step = WorkflowStep(
|
||||
name="writer",
|
||||
action="Write a haiku about programming",
|
||||
agent=Agent(instructions="You are a creative poet."),
|
||||
output="result.txt", # Save output to file
|
||||
)
|
||||
|
||||
# Note: This is a structural example - actual execution requires workflow.run()
|
||||
print("Workflow created with output preset:", workflow.output)
|
||||
print("Step output file:", step.output_file)
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Basic Workflow with Agent-like consolidated params."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.workflows import Workflow, WorkflowStep
|
||||
|
||||
# Workflow with agent-like params (knowledge, web, guardrails, reflection)
|
||||
workflow = Workflow(
|
||||
name="AgentLikeWorkflow",
|
||||
steps=[
|
||||
WorkflowStep(
|
||||
name="researcher",
|
||||
action="Research the topic: {{input}}",
|
||||
agent=Agent(instructions="You are a researcher."),
|
||||
),
|
||||
WorkflowStep(
|
||||
name="writer",
|
||||
action="Write about the research findings",
|
||||
agent=Agent(instructions="You are a writer."),
|
||||
),
|
||||
],
|
||||
# Agent-like consolidated params
|
||||
knowledge=["docs/"], # Enable RAG
|
||||
web=True, # Enable web search
|
||||
guardrails=True, # Enable guardrails
|
||||
reflection=True, # Enable self-reflection
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Workflow: {workflow.name}")
|
||||
print(f"Knowledge: {workflow.knowledge}")
|
||||
print(f"Web: {workflow.web}")
|
||||
print(f"Guardrails: {workflow.guardrails}")
|
||||
print(f"Reflection: {workflow.reflection}")
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Sample application."""
|
||||
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
class Calculator:
|
||||
"""Simple calculator."""
|
||||
|
||||
def __init__(self):
|
||||
self.value = 0
|
||||
|
||||
def add(self, x):
|
||||
self.value += x
|
||||
return self.value
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(greet("World"))
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
demo_output.py - Auto-generated file
|
||||
"""
|
||||
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
test_demo.py - Auto-generated file
|
||||
"""
|
||||
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dict Config Example - Demonstrates dict shorthand for consolidated parameters.
|
||||
|
||||
This example shows how to use dict shorthand instead of importing config classes.
|
||||
Dict input is strictly validated - unknown keys raise helpful errors.
|
||||
|
||||
Requirements:
|
||||
- Set OPENAI_API_KEY environment variable
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def example_dict_output_config():
|
||||
"""Example: Using dict for output configuration."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Dict shorthand - equivalent to OutputConfig(verbose=True, stream=False)
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
output={"verbose": True, "stream": False},
|
||||
)
|
||||
|
||||
result = agent.start("Say hello in one word.")
|
||||
print(f"Result: {result}")
|
||||
return agent
|
||||
|
||||
|
||||
def example_dict_execution_config():
|
||||
"""Example: Using dict for execution configuration."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Dict shorthand for execution settings
|
||||
agent = Agent(
|
||||
name="FastAgent",
|
||||
instructions="You are a fast responder.",
|
||||
execution={"max_iter": 5, "timeout": 30},
|
||||
)
|
||||
|
||||
result = agent.start("What is 2+2?")
|
||||
print(f"Result: {result}")
|
||||
return agent
|
||||
|
||||
|
||||
def example_dict_validation_error():
|
||||
"""Example: Dict with unknown key raises helpful error."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
print("\n--- Testing dict validation error ---")
|
||||
try:
|
||||
# This will raise TypeError with helpful message
|
||||
Agent(
|
||||
name="Test",
|
||||
instructions="Test agent",
|
||||
output={"verbose": True, "invalid_key": "value"},
|
||||
)
|
||||
except TypeError as e:
|
||||
print(f"Expected error caught: {e}")
|
||||
# Shows: Unknown keys for output: ['invalid_key']. Valid keys: ...
|
||||
|
||||
|
||||
def example_array_preset_override():
|
||||
"""Example: Array [preset, {overrides}] pattern still works."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Array pattern: preset name + override dict
|
||||
agent = Agent(
|
||||
name="VerboseAgent",
|
||||
instructions="You are verbose.",
|
||||
output=["verbose", {"stream": False}],
|
||||
)
|
||||
|
||||
result = agent.start("Say hi.")
|
||||
print(f"Result: {result}")
|
||||
return agent
|
||||
|
||||
|
||||
def example_base_url_api_key_separate():
|
||||
"""Example: base_url and api_key remain separate parameters."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# base_url and api_key are NOT consolidated - they stay separate
|
||||
# This is intentional for security and clarity
|
||||
agent = Agent(
|
||||
name="CustomEndpoint",
|
||||
instructions="You use a custom endpoint.",
|
||||
llm="gpt-4o-mini",
|
||||
# base_url="http://localhost:11434/v1", # Uncomment for Ollama
|
||||
# api_key=os.getenv("CUSTOM_API_KEY"), # Separate parameter
|
||||
)
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check for API key
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("Warning: OPENAI_API_KEY not set. Some examples may fail.")
|
||||
|
||||
print("=== Dict Config Examples ===\n")
|
||||
|
||||
# Run validation error example (doesn't need API key)
|
||||
example_dict_validation_error()
|
||||
|
||||
# Run examples that need API key
|
||||
if os.getenv("OPENAI_API_KEY"):
|
||||
print("\n--- Dict output config ---")
|
||||
example_dict_output_config()
|
||||
|
||||
print("\n--- Array preset override ---")
|
||||
example_array_preset_override()
|
||||
else:
|
||||
print("\nSkipping API examples (no OPENAI_API_KEY)")
|
||||
|
||||
print("\n=== Examples Complete ===")
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Streaming Example with Profiling
|
||||
|
||||
Key Points:
|
||||
1. output="stream" preset enables streaming by default
|
||||
2. stream=True in start() is redundant when using output="stream" (but doesn't hurt)
|
||||
3. Profiler.enable() MUST be called before profiling works
|
||||
4. Use Profiler.streaming() context manager for TTFT metrics
|
||||
"""
|
||||
from praisonai.profiler import Profiler, profile, profile_imports
|
||||
|
||||
# IMPORTANT: Enable profiler BEFORE any profiling operations
|
||||
Profiler.enable()
|
||||
|
||||
# Profile imports
|
||||
with profile_imports():
|
||||
from praisonaiagents import Agent
|
||||
|
||||
@profile
|
||||
def main():
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
output="stream" # This already enables streaming
|
||||
)
|
||||
|
||||
print("\nStreaming response:")
|
||||
print("-" * 40)
|
||||
|
||||
# Use Profiler.streaming() to track TTFT (Time To First Token)
|
||||
with Profiler.streaming("llm_stream") as tracker:
|
||||
first_chunk = True
|
||||
for chunk in agent.start("Write a short paragraph about the history of computing"):
|
||||
if first_chunk:
|
||||
tracker.first_token() # Mark TTFT
|
||||
first_chunk = False
|
||||
tracker.chunk() # Count chunks
|
||||
print(chunk, end="", flush=True)
|
||||
|
||||
print("\n" + "-" * 40)
|
||||
|
||||
# Run with profiling blocks
|
||||
with Profiler.block("full_workflow"):
|
||||
with Profiler.block("setup"):
|
||||
pass
|
||||
|
||||
with Profiler.block("execution"):
|
||||
result = main()
|
||||
|
||||
with Profiler.block("cleanup"):
|
||||
pass
|
||||
|
||||
# Generate report
|
||||
Profiler.report()
|
||||
|
||||
# Show streaming-specific stats
|
||||
streaming_records = Profiler.get_streaming_records()
|
||||
if streaming_records:
|
||||
print("\nStreaming Metrics:")
|
||||
for record in streaming_records:
|
||||
print(f" {record.name}: TTFT={record.ttft_ms:.2f}ms, Total={record.total_ms:.2f}ms, Chunks={record.chunk_count}")
|
||||
|
||||
stats = Profiler.get_statistics()
|
||||
print(f"\nMedian execution time: {stats['p50']:.2f}ms")
|
||||
@@ -0,0 +1,4 @@
|
||||
from praisonaiagents import Agent
|
||||
agent = Agent(instructions="You are a helpful assistant")
|
||||
result = agent.start("Why is the sky blue?")
|
||||
print(result)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Advanced memory example - with config and overrides."""
|
||||
from praisonaiagents import Agent, MemoryConfig
|
||||
|
||||
# With full config
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with memory.",
|
||||
memory=MemoryConfig(
|
||||
backend="redis",
|
||||
user_id="user123",
|
||||
session_id="session456",
|
||||
auto_memory=True,
|
||||
),
|
||||
)
|
||||
|
||||
# With array [preset, overrides]
|
||||
agent_override = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
memory=["redis", {"user_id": "custom_user"}],
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.chat("Remember that my favorite color is blue.")
|
||||
print(response)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Basic memory example - minimal usage."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Simple enable (uses default file-based memory)
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant with memory.",
|
||||
memory=True,
|
||||
)
|
||||
|
||||
# With preset
|
||||
agent_redis = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
memory="redis", # Uses redis preset
|
||||
)
|
||||
|
||||
# With URL
|
||||
agent_postgres = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
memory="postgresql://localhost/mydb", # URL auto-detected
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.chat("Remember that my favorite color is blue.")
|
||||
print(response)
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Basic output and execution example."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Simple preset usage
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant.",
|
||||
output="verbose", # Preset: verbose output
|
||||
execution="thorough", # Preset: thorough execution
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = agent.chat("What is 2+2?")
|
||||
print(response)
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Basic workflow example with consolidated params."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.workflows import Workflow, WorkflowStep
|
||||
|
||||
# Create agents
|
||||
writer = Agent(instructions="You are a content writer.")
|
||||
editor = Agent(instructions="You are an editor.")
|
||||
|
||||
# Create workflow with consolidated params
|
||||
workflow = Workflow(
|
||||
name="Content Pipeline",
|
||||
steps=[
|
||||
WorkflowStep(name="write", agent=writer, action="Write about {{topic}}"),
|
||||
WorkflowStep(name="edit", agent=editor, action="Edit the content", context=["write"]),
|
||||
],
|
||||
output="verbose",
|
||||
planning=True,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = workflow.run(variables={"topic": "AI agents"})
|
||||
print(result)
|
||||
@@ -177,6 +177,13 @@ def __getattr__(name):
|
||||
_lazy_cache["PraisonAIAgents"] = Agents # Also cache alias
|
||||
return Agents
|
||||
elif name == "PraisonAIAgents":
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"PraisonAIAgents is deprecated, use Agents instead. "
|
||||
"Example: from praisonaiagents import Agents",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
from .agents.agents import Agents
|
||||
_lazy_cache["Agents"] = Agents
|
||||
_lazy_cache[name] = Agents
|
||||
@@ -469,6 +476,62 @@ def _init_telemetry():
|
||||
_init_telemetry()
|
||||
|
||||
|
||||
def warmup(include_litellm: bool = False, include_openai: bool = True) -> dict:
|
||||
"""
|
||||
Pre-import heavy dependencies to reduce first-call latency.
|
||||
|
||||
NOTE: For default OpenAI usage (llm="gpt-4o-mini"), warmup is NOT needed.
|
||||
The default path uses the native OpenAI SDK which is fast (~100ms import).
|
||||
|
||||
Warmup is only beneficial when using LiteLLM backend, which is triggered by:
|
||||
- Using "/" in model name (e.g., llm="openai/gpt-4o-mini")
|
||||
- Passing a dict config (e.g., llm={"model": "gpt-4o-mini"})
|
||||
- Using base_url parameter
|
||||
|
||||
Args:
|
||||
include_litellm: Pre-import LiteLLM (~2-3s). Only needed for multi-provider support.
|
||||
include_openai: Pre-import OpenAI SDK (~100ms). Default path, usually fast.
|
||||
|
||||
Returns:
|
||||
dict: Timing information for each component warmed up
|
||||
|
||||
Example:
|
||||
# For LiteLLM multi-provider usage:
|
||||
from praisonaiagents import warmup
|
||||
warmup(include_litellm=True) # Pre-load LiteLLM
|
||||
|
||||
agent = Agent(llm="anthropic/claude-3-sonnet") # Now faster
|
||||
|
||||
# For default OpenAI usage, no warmup needed:
|
||||
agent = Agent(llm="gpt-4o-mini") # Already fast!
|
||||
"""
|
||||
import time
|
||||
timings = {}
|
||||
|
||||
if include_openai:
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
import openai
|
||||
timings['openai'] = (time.perf_counter() - start) * 1000
|
||||
except ImportError:
|
||||
timings['openai'] = -1 # Not available
|
||||
|
||||
if include_litellm:
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
import litellm
|
||||
# Also configure litellm to avoid first-call overhead
|
||||
litellm.telemetry = False
|
||||
litellm.set_verbose = False
|
||||
litellm.drop_params = True
|
||||
litellm.modify_params = True
|
||||
timings['litellm'] = (time.perf_counter() - start) * 1000
|
||||
except ImportError:
|
||||
timings['litellm'] = -1 # Not available
|
||||
|
||||
return timings
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Core classes (lazy loaded)
|
||||
'Agent',
|
||||
@@ -639,4 +702,19 @@ __all__ = [
|
||||
# Parse Utilities
|
||||
'is_policy_string',
|
||||
'parse_policy_string',
|
||||
# Performance utilities
|
||||
'warmup',
|
||||
# Cortex - Continuous Improvement System
|
||||
'CortexConfig',
|
||||
'CaptureStore',
|
||||
'CaptureMode',
|
||||
'CaptureScope',
|
||||
'PersonaConfig',
|
||||
'InsightConfig',
|
||||
'ThreadConfig',
|
||||
'EntityConfig',
|
||||
'PatternConfig',
|
||||
'DecisionConfig',
|
||||
'FeedbackConfig',
|
||||
'ImprovementConfig',
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Resolver helper functions for consolidated parameters.
|
||||
|
||||
These helpers wrap the canonical resolver for specific parameter types,
|
||||
handling special cases like callable validators for guardrails.
|
||||
"""
|
||||
|
||||
from typing import Any, Type
|
||||
|
||||
from .config.param_resolver import resolve, ArrayMode
|
||||
from .config.presets import GUARDRAIL_PRESETS
|
||||
|
||||
|
||||
def resolve_guardrails(value: Any, config_class: Type) -> Any:
|
||||
"""
|
||||
Resolve guardrails parameter.
|
||||
|
||||
Handles special case of callable validators which have highest precedence.
|
||||
|
||||
Args:
|
||||
value: The guardrails parameter value
|
||||
config_class: GuardrailConfig class
|
||||
|
||||
Returns:
|
||||
Resolved config, callable, or string
|
||||
"""
|
||||
# Handle None
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Handle False (disabled)
|
||||
if value is False:
|
||||
return None
|
||||
|
||||
# Handle callable (highest precedence after None check)
|
||||
# This includes functions, lambdas, and callable objects
|
||||
if callable(value) and not isinstance(value, type):
|
||||
return value
|
||||
|
||||
# Use canonical resolver for everything else
|
||||
return resolve(
|
||||
value=value,
|
||||
param_name="guardrails",
|
||||
config_class=config_class,
|
||||
presets=GUARDRAIL_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
string_mode="llm_prompt",
|
||||
default=None,
|
||||
)
|
||||
@@ -44,7 +44,7 @@ _shared_apps = {} # Dict of port -> FastAPI app
|
||||
if TYPE_CHECKING:
|
||||
from ..task.task import Task
|
||||
from ..main import TaskOutput
|
||||
from ..handoff import Handoff
|
||||
from .handoff import Handoff, HandoffConfig, HandoffResult
|
||||
from ..rag.models import RAGResult, ContextPack
|
||||
|
||||
class Agent:
|
||||
@@ -254,10 +254,11 @@ class Agent:
|
||||
instructions: Optional[str] = None,
|
||||
# LLM configuration
|
||||
llm: Optional[Union[str, Any]] = None,
|
||||
function_calling_llm: Optional[Any] = None,
|
||||
llm_config: Optional[Dict[str, Any]] = None,
|
||||
base_url: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
model: Optional[Union[str, Any]] = None, # Alias for llm=
|
||||
function_calling_llm: Optional[Any] = None, # DEPRECATED: use llm= with LLMConfig
|
||||
llm_config: Optional[Dict[str, Any]] = None, # DEPRECATED: use llm= with LLMConfig
|
||||
base_url: Optional[str] = None, # Kept separate (connection/auth)
|
||||
api_key: Optional[str] = None, # Kept separate (connection/auth)
|
||||
# Tools
|
||||
tools: Optional[List[Any]] = None,
|
||||
allow_delegation: bool = False,
|
||||
@@ -290,96 +291,87 @@ class Agent:
|
||||
"""Initialize an Agent instance.
|
||||
|
||||
Args:
|
||||
name (Optional[str], optional): Name of the agent used for identification and logging.
|
||||
If None, defaults to "Agent". Defaults to None.
|
||||
role (Optional[str], optional): Role or job title that defines the agent's expertise
|
||||
and behavior patterns. Examples: "Data Analyst", "Content Writer". Defaults to None.
|
||||
goal (Optional[str], optional): Primary objective or goal the agent aims to achieve.
|
||||
Defines the agent's purpose and success criteria. Defaults to None.
|
||||
backstory (Optional[str], optional): Background story or context that shapes the agent's
|
||||
personality and decision-making approach. Defaults to None.
|
||||
instructions (Optional[str], optional): Direct instructions that override role, goal,
|
||||
and backstory when provided. Used for simple, task-specific agents. Defaults to None.
|
||||
llm (Optional[Union[str, Any]], optional): Language model configuration. Can be a model
|
||||
name string (e.g., "gpt-5-nano", "anthropic/claude-3-sonnet") or a configured LLM object.
|
||||
Defaults to environment variable OPENAI_MODEL_NAME or "gpt-5-nano".
|
||||
tools (Optional[List[Any]], optional): List of tools, functions, or capabilities
|
||||
available to the agent for task execution. Can include callables, tool objects,
|
||||
or MCP instances. Defaults to None.
|
||||
function_calling_llm (Optional[Any], optional): Dedicated language model for function
|
||||
calling operations. If None, uses the main llm parameter. Defaults to None.
|
||||
max_iter (int, optional): Maximum number of iterations the agent can perform during
|
||||
task execution to prevent infinite loops. Defaults to 20.
|
||||
max_rpm (Optional[int], optional): Maximum requests per minute to rate limit API calls
|
||||
and prevent quota exhaustion. If None, no rate limiting is applied. Defaults to None.
|
||||
max_execution_time (Optional[int], optional): Maximum execution time in seconds for
|
||||
agent operations before timeout. If None, no time limit is enforced. Defaults to None.
|
||||
memory (Optional[Any], optional): Memory system for storing and retrieving information
|
||||
across conversations. Requires memory dependencies to be installed. Defaults to None.
|
||||
verbose (bool, optional): Enable detailed logging and status updates during agent
|
||||
execution for debugging and monitoring. Defaults to True.
|
||||
allow_delegation (bool, optional): Allow the agent to delegate tasks to other agents
|
||||
or sub-processes when appropriate. Defaults to False.
|
||||
step_callback (Optional[Any], optional): Callback function called after each step
|
||||
of agent execution for custom monitoring or intervention. Defaults to None.
|
||||
cache (bool, optional): Enable caching of responses and computations to improve
|
||||
performance and reduce API costs. Defaults to True.
|
||||
system_template (Optional[str], optional): Custom template for system prompts that
|
||||
overrides the default system prompt generation. Defaults to None.
|
||||
prompt_template (Optional[str], optional): Template for formatting user prompts
|
||||
before sending to the language model. Defaults to None.
|
||||
response_template (Optional[str], optional): Template for formatting agent responses
|
||||
before returning to the user. Defaults to None.
|
||||
allow_code_execution (Optional[bool], optional): Enable the agent to execute code
|
||||
snippets during task completion. Use with caution for security. Defaults to False.
|
||||
max_retry_limit (int, optional): Maximum number of retry attempts for failed operations
|
||||
before giving up. Helps handle transient errors. Defaults to 2.
|
||||
code_execution_mode (Literal["safe", "unsafe"], optional): Safety mode for code execution.
|
||||
"safe" restricts dangerous operations, "unsafe" allows full code execution. Defaults to "safe".
|
||||
embedder_config (Optional[Dict[str, Any]], optional): Configuration dictionary for
|
||||
text embedding models used in knowledge retrieval and similarity search. Defaults to None.
|
||||
knowledge (Optional[List[str]], optional): List of knowledge sources (file paths, URLs,
|
||||
or text content) to be processed and made available to the agent. Defaults to None.
|
||||
knowledge_config (Optional[Dict[str, Any]], optional): Configuration for knowledge
|
||||
processing and retrieval system including chunking and indexing parameters. Defaults to None.
|
||||
use_system_prompt (Optional[bool], optional): Whether to include system prompts in
|
||||
conversations to establish agent behavior and context. Defaults to True.
|
||||
markdown (bool, optional): Enable markdown formatting in agent responses for better
|
||||
readability and structure. Defaults to True.
|
||||
stream (bool, optional): Enable streaming responses from the language model for real-time
|
||||
output when using Agent.start() method. Defaults to False for backward compatibility.
|
||||
metrics (bool, optional): Enable automatic token usage tracking and display summary
|
||||
when tasks complete. Simplifies token monitoring for cost optimization. Defaults to False.
|
||||
self_reflect (bool, optional): Enable self-reflection capabilities where the agent
|
||||
evaluates and improves its own responses. Defaults to False.
|
||||
max_reflect (int, optional): Maximum number of self-reflection iterations to prevent
|
||||
excessive reflection loops. Defaults to 3.
|
||||
min_reflect (int, optional): Minimum number of self-reflection iterations required
|
||||
before accepting a response as satisfactory. Defaults to 1.
|
||||
reflect_llm (Optional[str], optional): Dedicated language model for self-reflection
|
||||
operations. If None, uses the main llm parameter. Defaults to None.
|
||||
reflect_prompt (Optional[str], optional): Custom prompt template for self-reflection
|
||||
that guides the agent's self-evaluation process. Defaults to None.
|
||||
user_id (Optional[str], optional): Unique identifier for the user or session to
|
||||
enable personalized responses and memory isolation. Defaults to "praison".
|
||||
reasoning_steps (bool, optional): Enable step-by-step reasoning output to show the
|
||||
agent's thought process during problem solving. Defaults to False.
|
||||
guardrail (Optional[Union[Callable[['TaskOutput'], Tuple[bool, Any]], str]], optional):
|
||||
Safety mechanism to validate agent outputs. Can be a validation function or
|
||||
description string for LLM-based validation. Defaults to None.
|
||||
max_guardrail_retries (int, optional): Maximum number of retry attempts when guardrail
|
||||
validation fails before giving up. Defaults to 3.
|
||||
handoffs (Optional[List[Union['Agent', 'Handoff']]], optional): List of agents or
|
||||
handoff configurations that this agent can delegate tasks to. Enables agent-to-agent
|
||||
collaboration and task specialization. Defaults to None.
|
||||
base_url (Optional[str], optional): Base URL for custom LLM endpoints (e.g., Ollama).
|
||||
If provided, automatically creates a custom LLM instance. Defaults to None.
|
||||
api_key (Optional[str], optional): API key for LLM provider. If not provided,
|
||||
falls back to environment variables. Defaults to None.
|
||||
name: Agent name for identification and logging. Defaults to "Agent".
|
||||
role: Role/job title defining expertise (e.g., "Data Analyst").
|
||||
goal: Primary objective the agent aims to achieve.
|
||||
backstory: Background context shaping personality and decisions.
|
||||
instructions: Direct instructions (overrides role/goal/backstory). Recommended for simple agents.
|
||||
llm: Model name string ("gpt-4o", "anthropic/claude-3-sonnet") or LLM object.
|
||||
Defaults to OPENAI_MODEL_NAME env var or "gpt-4o-mini".
|
||||
model: Alias for llm parameter.
|
||||
function_calling_llm: Dedicated LLM for function calling. Deprecated: use llm=.
|
||||
llm_config: LLM configuration dict. Deprecated: use llm=.
|
||||
base_url: Custom LLM endpoint URL (e.g., for Ollama). Kept separate for auth.
|
||||
api_key: API key for LLM provider. Kept separate for auth.
|
||||
tools: List of tools, functions, callables, or MCP instances.
|
||||
allow_delegation: Allow task delegation to other agents. Defaults to False.
|
||||
allow_code_execution: Enable code execution during tasks. Defaults to False.
|
||||
code_execution_mode: "safe" (restricted) or "unsafe" (full access). Defaults to "safe".
|
||||
handoffs: List of Agent or Handoff objects for agent-to-agent collaboration.
|
||||
auto_save: Session name for automatic session saving.
|
||||
rate_limiter: Rate limiter instance for API call throttling.
|
||||
memory: Memory system configuration. Accepts:
|
||||
- bool: True enables defaults, False disables
|
||||
- MemoryConfig: Custom configuration
|
||||
- MemoryManager: Pre-configured instance
|
||||
knowledge: Knowledge sources. Accepts:
|
||||
- bool: True enables defaults
|
||||
- List[str]: File paths, URLs, or text content
|
||||
- KnowledgeConfig: Custom configuration
|
||||
planning: Planning mode. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- PlanningConfig: Custom configuration
|
||||
reflection: Self-reflection. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- ReflectionConfig: Custom configuration
|
||||
guardrails: Output validation. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- Callable: Validation function
|
||||
- GuardrailConfig: Custom configuration
|
||||
web: Web search/fetch. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- WebConfig: Custom configuration
|
||||
context: Context management. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- ManagerConfig: Custom configuration
|
||||
autonomy: Autonomy settings. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- Dict: Configuration dict
|
||||
- AutonomyConfig: Custom configuration
|
||||
verification_hooks: List of VerificationHook instances for output verification.
|
||||
output: Output configuration. Accepts:
|
||||
- str: Preset name ("silent", "actions", "verbose", "json", "stream")
|
||||
- OutputConfig: Custom configuration
|
||||
Controls: verbose, markdown, stream, metrics, reasoning_steps
|
||||
execution: Execution configuration. Accepts:
|
||||
- str: Preset name ("fast", "balanced", "thorough")
|
||||
- ExecutionConfig: Custom configuration
|
||||
Controls: max_iter, max_rpm, max_execution_time, max_retry_limit
|
||||
templates: Template configuration (TemplateConfig).
|
||||
Controls: system_template, prompt_template, response_template
|
||||
caching: Caching configuration. Accepts:
|
||||
- bool: True enables with defaults
|
||||
- CachingConfig: Custom configuration
|
||||
hooks: Event hooks. Accepts:
|
||||
- List: List of hook callables
|
||||
- HooksConfig: Custom configuration
|
||||
skills: Agent skills. Accepts:
|
||||
- List[str]: Skill directory paths
|
||||
- SkillsConfig: Custom configuration
|
||||
|
||||
Raises:
|
||||
ValueError: If all of name, role, goal, backstory, and instructions are None.
|
||||
ImportError: If memory or LLM features are requested but dependencies are not installed.
|
||||
|
||||
Note:
|
||||
Many legacy parameters have been consolidated into config objects:
|
||||
- verbose, markdown, stream, metrics, reasoning_steps → output=
|
||||
- max_iter, max_rpm, max_execution_time, max_retry_limit → execution=
|
||||
- self_reflect, max_reflect, min_reflect, reflect_llm → reflection=
|
||||
- guardrail, max_guardrail_retries → guardrails=
|
||||
- system_template, prompt_template, response_template → templates=
|
||||
- cache, prompt_caching → caching=
|
||||
- web_search, web_fetch → web=
|
||||
"""
|
||||
# Add check at start if memory is requested
|
||||
if memory is not None:
|
||||
@@ -446,14 +438,27 @@ class Agent:
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve OUTPUT param using unified resolver
|
||||
# Supports: None, str preset, list [preset, overrides], OutputConfig
|
||||
# DEFAULT: "silent" mode (zero overhead, fastest performance)
|
||||
# Can be overridden by PRAISONAI_OUTPUT env var
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
from ..config.presets import DEFAULT_OUTPUT_MODE
|
||||
|
||||
# Check for env var override
|
||||
env_output = os.environ.get('PRAISONAI_OUTPUT', '').lower()
|
||||
if env_output and env_output in OUTPUT_PRESETS and output is None:
|
||||
output = env_output
|
||||
|
||||
# Use default output mode if not specified
|
||||
if output is None:
|
||||
output = DEFAULT_OUTPUT_MODE
|
||||
|
||||
_output_config = resolve(
|
||||
value=output,
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
default=OutputConfig(),
|
||||
default=OutputConfig(), # OutputConfig defaults to silent mode (zero overhead)
|
||||
)
|
||||
if _output_config:
|
||||
verbose = _output_config.verbose
|
||||
@@ -462,8 +467,24 @@ class Agent:
|
||||
metrics = _output_config.metrics
|
||||
reasoning_steps = _output_config.reasoning_steps
|
||||
output_style = getattr(_output_config, 'style', None)
|
||||
actions_trace = getattr(_output_config, 'actions_trace', False) # Default False (silent)
|
||||
json_output = getattr(_output_config, 'json_output', False)
|
||||
else:
|
||||
verbose, markdown, stream, metrics, reasoning_steps = True, True, False, False, False
|
||||
# Fallback defaults match silent mode (zero overhead)
|
||||
verbose, markdown, stream, metrics, reasoning_steps = False, False, False, False, False
|
||||
actions_trace = False # No callbacks by default
|
||||
json_output = False
|
||||
|
||||
# Enable actions output mode if configured
|
||||
# This registers callbacks to capture tool calls and final output
|
||||
if actions_trace:
|
||||
try:
|
||||
from ..output.actions import enable_actions_mode, is_actions_mode_enabled
|
||||
if not is_actions_mode_enabled():
|
||||
output_format = "jsonl" if json_output else "text"
|
||||
enable_actions_mode(redact=True, use_color=True, format=output_format)
|
||||
except ImportError:
|
||||
pass # Actions module not available
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve EXECUTION param using unified resolver
|
||||
@@ -522,34 +543,45 @@ class Agent:
|
||||
cache, prompt_caching = True, None
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve HOOKS param
|
||||
# Supports: None, list of callables, HooksConfig
|
||||
# Resolve HOOKS param using unified resolver
|
||||
# Supports: None, list of callables, HooksConfig, dict
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
_hooks_list = []
|
||||
step_callback = None
|
||||
if hooks is not None:
|
||||
if isinstance(hooks, list):
|
||||
_hooks_list = hooks
|
||||
elif isinstance(hooks, HooksConfig):
|
||||
step_callback = hooks.on_step
|
||||
_hooks_list = hooks.middleware or []
|
||||
elif isinstance(hooks, dict):
|
||||
step_callback = hooks.get('on_step')
|
||||
_hooks_list = hooks.get('middleware', [])
|
||||
_hooks_config = resolve(
|
||||
value=hooks,
|
||||
param_name="hooks",
|
||||
config_class=HooksConfig,
|
||||
array_mode=ArrayMode.PASSTHROUGH,
|
||||
default=None,
|
||||
)
|
||||
if _hooks_config is not None:
|
||||
if isinstance(_hooks_config, list):
|
||||
_hooks_list = _hooks_config
|
||||
elif isinstance(_hooks_config, HooksConfig):
|
||||
step_callback = _hooks_config.on_step
|
||||
_hooks_list = _hooks_config.middleware or []
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve SKILLS param
|
||||
# Supports: None, list of paths, SkillsConfig
|
||||
# Resolve SKILLS param using unified resolver
|
||||
# Supports: None, list of paths, SkillsConfig, str
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
_skills = None
|
||||
if skills is not None:
|
||||
if isinstance(skills, list):
|
||||
_skills = skills
|
||||
elif isinstance(skills, SkillsConfig):
|
||||
_skills = skills.paths
|
||||
skills_dirs = skills.dirs
|
||||
elif isinstance(skills, str):
|
||||
_skills = [skills]
|
||||
skills_dirs = None
|
||||
_skills_config = resolve(
|
||||
value=skills,
|
||||
param_name="skills",
|
||||
config_class=SkillsConfig,
|
||||
array_mode=ArrayMode.SOURCES,
|
||||
string_mode="path_as_source",
|
||||
default=None,
|
||||
)
|
||||
if _skills_config is not None:
|
||||
if isinstance(_skills_config, SkillsConfig):
|
||||
_skills = _skills_config.paths
|
||||
skills_dirs = _skills_config.dirs
|
||||
elif isinstance(_skills_config, list):
|
||||
_skills = _skills_config
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve MEMORY param using unified resolver
|
||||
@@ -595,36 +627,42 @@ class Agent:
|
||||
memory = None
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve KNOWLEDGE param
|
||||
# Resolve KNOWLEDGE param using unified resolver
|
||||
# Supports: None, bool, str path, list of sources, KnowledgeConfig, Instance
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
retrieval_config = None
|
||||
embedder_config = None
|
||||
|
||||
if knowledge is not None and knowledge is not False:
|
||||
if isinstance(knowledge, list):
|
||||
# List of sources - use directly
|
||||
pass
|
||||
elif hasattr(knowledge, 'search') and hasattr(knowledge, 'add'):
|
||||
_knowledge_config = resolve(
|
||||
value=knowledge,
|
||||
param_name="knowledge",
|
||||
config_class=KnowledgeConfig,
|
||||
instance_check=lambda v: hasattr(v, 'search') and hasattr(v, 'add'),
|
||||
array_mode=ArrayMode.SOURCES_WITH_CONFIG,
|
||||
string_mode="path_as_source",
|
||||
default=None,
|
||||
)
|
||||
|
||||
if _knowledge_config is not None:
|
||||
if hasattr(_knowledge_config, 'search') and hasattr(_knowledge_config, 'add'):
|
||||
# Knowledge instance - pass through
|
||||
pass
|
||||
elif isinstance(knowledge, KnowledgeConfig):
|
||||
embedder_config = knowledge.embedder_config
|
||||
if knowledge.config:
|
||||
retrieval_config = knowledge.config
|
||||
knowledge = _knowledge_config
|
||||
elif isinstance(_knowledge_config, KnowledgeConfig):
|
||||
embedder_config = _knowledge_config.embedder_config
|
||||
if _knowledge_config.config:
|
||||
retrieval_config = _knowledge_config.config
|
||||
else:
|
||||
retrieval_config = {
|
||||
'top_k': knowledge.retrieval_k,
|
||||
'threshold': knowledge.retrieval_threshold,
|
||||
'rerank': knowledge.rerank,
|
||||
'rerank_model': knowledge.rerank_model,
|
||||
'top_k': _knowledge_config.retrieval_k,
|
||||
'threshold': _knowledge_config.retrieval_threshold,
|
||||
'rerank': _knowledge_config.rerank,
|
||||
'rerank_model': _knowledge_config.rerank_model,
|
||||
}
|
||||
knowledge = knowledge.sources if knowledge.sources else None
|
||||
elif isinstance(knowledge, str):
|
||||
# Single path - convert to list
|
||||
knowledge = [knowledge]
|
||||
elif knowledge is True:
|
||||
knowledge = None
|
||||
knowledge = _knowledge_config.sources if _knowledge_config.sources else None
|
||||
elif isinstance(_knowledge_config, list):
|
||||
knowledge = _knowledge_config
|
||||
elif knowledge is False:
|
||||
knowledge = None
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve PLANNING param
|
||||
@@ -681,22 +719,25 @@ class Agent:
|
||||
self_reflect = False
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve GUARDRAILS param
|
||||
# Resolve GUARDRAILS param using unified resolver
|
||||
# Supports: None, bool, callable, str prompt, GuardrailConfig
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
if guardrails is not None:
|
||||
if guardrails is False:
|
||||
guardrail = None
|
||||
elif callable(guardrails):
|
||||
guardrail = guardrails
|
||||
elif isinstance(guardrails, GuardrailConfig):
|
||||
guardrail = guardrails.validator or guardrails.llm_validator
|
||||
max_guardrail_retries = guardrails.max_retries
|
||||
elif isinstance(guardrails, str):
|
||||
# Long string = LLM validator prompt
|
||||
guardrail = guardrails
|
||||
elif guardrails is True:
|
||||
pass # No-op, user must provide validator
|
||||
from .._resolver_helpers import resolve_guardrails as _resolve_guardrails
|
||||
_guardrails_config = _resolve_guardrails(
|
||||
value=guardrails,
|
||||
config_class=GuardrailConfig,
|
||||
)
|
||||
|
||||
if _guardrails_config is not None:
|
||||
if callable(_guardrails_config) and not isinstance(_guardrails_config, type):
|
||||
# Callable validator function
|
||||
guardrail = _guardrails_config
|
||||
elif isinstance(_guardrails_config, GuardrailConfig):
|
||||
guardrail = _guardrails_config.validator or _guardrails_config.llm_validator
|
||||
max_guardrail_retries = _guardrails_config.max_retries
|
||||
elif isinstance(_guardrails_config, str):
|
||||
# LLM validator prompt
|
||||
guardrail = _guardrails_config
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve WEB param using unified resolver
|
||||
@@ -763,13 +804,30 @@ class Agent:
|
||||
self._llm_config = llm_config or {}
|
||||
self._llm_configurable = self._llm_config.get('configurable', False)
|
||||
|
||||
# ============================================================
|
||||
# LLM CONSOLIDATION: Handle model= alias and deprecation warnings
|
||||
# Precedence: llm= > model= > default
|
||||
# ============================================================
|
||||
import warnings
|
||||
|
||||
# Handle model= alias for llm= (NO warnings - both are valid)
|
||||
if llm is None and model is not None:
|
||||
llm = model # model= is an alias for llm=
|
||||
|
||||
# Legacy params (llm_config, function_calling_llm) are accepted silently
|
||||
# for backward compatibility - NO deprecation warnings per policy
|
||||
|
||||
# Store rate limiter (optional, zero overhead when None)
|
||||
self._rate_limiter = rate_limiter
|
||||
|
||||
# Store OpenAI client parameters for lazy initialization
|
||||
# Store OpenAI client parameters for lazy initialization (kept separate)
|
||||
self._openai_api_key = api_key
|
||||
self._openai_base_url = base_url
|
||||
self.__openai_client = None
|
||||
|
||||
# Expose base_url and api_key as properties for tests
|
||||
self.base_url = base_url
|
||||
self.api_key = api_key
|
||||
|
||||
# If base_url is provided, always create a custom LLM instance
|
||||
if base_url:
|
||||
@@ -786,7 +844,7 @@ class Agent:
|
||||
self.llm_instance = LLM(**llm_config)
|
||||
else:
|
||||
# Create LLM with model string and base_url
|
||||
model_name = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
model_name = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
self.llm_instance = LLM(
|
||||
model=model_name,
|
||||
base_url=base_url,
|
||||
@@ -849,7 +907,7 @@ class Agent:
|
||||
) from e
|
||||
# Otherwise, fall back to OpenAI environment/name
|
||||
else:
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
# Handle tools parameter - ensure it's always a list
|
||||
if callable(tools):
|
||||
# If a single function/callable is passed, wrap it in a list
|
||||
@@ -895,7 +953,7 @@ class Agent:
|
||||
self.min_reflect = min_reflect
|
||||
self.reflect_prompt = reflect_prompt
|
||||
# Use the same model selection logic for reflect_llm
|
||||
self.reflect_llm = reflect_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.reflect_llm = reflect_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
self._console = None # Lazy load console when needed
|
||||
|
||||
# Initialize system prompt
|
||||
@@ -937,9 +995,14 @@ Your Goal: {self.goal}
|
||||
self.tools.append(internet_search)
|
||||
logging.info("Model does not support native web search. Added DuckDuckGo fallback tool.")
|
||||
|
||||
# Log warning if web_fetch is enabled but model doesn't support it
|
||||
# Raise error if web_fetch is explicitly enabled but model doesn't support it
|
||||
# Web fetch is only supported by Anthropic/Claude models
|
||||
if web_fetch and not self._model_supports_web_fetch():
|
||||
logging.warning(f"Model '{self.llm}' does not support native web fetch. Web fetch will be ignored.")
|
||||
raise ValueError(
|
||||
f"web_fetch is only supported on Anthropic/Claude models. "
|
||||
f"Model '{self.llm}' does not support web fetch. "
|
||||
f"Either use a Claude model (e.g., 'anthropic/claude-sonnet-4') or disable fetch with web=WebConfig(fetch=False)."
|
||||
)
|
||||
|
||||
# Initialize guardrail settings
|
||||
self.guardrail = guardrail
|
||||
@@ -1022,6 +1085,9 @@ Your Goal: {self.goal}
|
||||
self._context_param = context # Store raw param for lazy init
|
||||
self._context_manager = None # Lazy initialized on first use
|
||||
self._context_manager_initialized = False
|
||||
|
||||
# Action trace mode - handled via display callbacks, not separate emitter
|
||||
self._actions_trace = actions_trace
|
||||
|
||||
@property
|
||||
def auto_memory(self):
|
||||
@@ -1677,7 +1743,7 @@ Your Goal: {self.goal}
|
||||
elif hasattr(self, 'llm') and self.llm:
|
||||
model_name = self.llm
|
||||
else:
|
||||
model_name = "gpt-5-nano"
|
||||
model_name = "gpt-4o-mini"
|
||||
|
||||
return supports_web_search(model_name)
|
||||
|
||||
@@ -1699,7 +1765,7 @@ Your Goal: {self.goal}
|
||||
elif hasattr(self, 'llm') and self.llm:
|
||||
model_name = self.llm
|
||||
else:
|
||||
model_name = "gpt-5-nano"
|
||||
model_name = "gpt-4o-mini"
|
||||
|
||||
return supports_web_fetch(model_name)
|
||||
|
||||
@@ -1943,7 +2009,7 @@ Your Goal: {self.goal}
|
||||
|
||||
Returns:
|
||||
The LLM model/instance being used by this agent.
|
||||
- For standard models: returns the model string (e.g., "gpt-5-nano")
|
||||
- For standard models: returns the model string (e.g., "gpt-4o-mini")
|
||||
- For custom LLM instances: returns the LLM instance object
|
||||
- For provider models: returns the LLM instance object
|
||||
"""
|
||||
@@ -1953,7 +2019,7 @@ Your Goal: {self.goal}
|
||||
return self.llm
|
||||
else:
|
||||
# Default fallback
|
||||
return "gpt-5-nano"
|
||||
return "gpt-4o-mini"
|
||||
|
||||
def _ensure_knowledge_processed(self):
|
||||
"""Ensure knowledge is initialized and processed when first accessed."""
|
||||
@@ -3653,7 +3719,7 @@ Your Goal: {self.goal}"""
|
||||
if self.verbose:
|
||||
logging.debug(f"Agent {self.name} final response: {response_text}")
|
||||
# Return only reasoning content if reasoning_steps is True
|
||||
if reasoning_steps and hasattr(response.choices[0].message, 'reasoning_content'):
|
||||
if reasoning_steps and hasattr(response.choices[0].message, 'reasoning_content') and response.choices[0].message.reasoning_content:
|
||||
# Apply guardrail to reasoning content
|
||||
try:
|
||||
validated_reasoning = self._apply_guardrail_with_retry(response.choices[0].message.reasoning_content, original_prompt, temperature, tools, task_name, task_description, task_id)
|
||||
@@ -4178,7 +4244,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
|
||||
final_response = process_stream_chunks(chunks)
|
||||
# Return only reasoning content if reasoning_steps is True
|
||||
if reasoning_steps and hasattr(final_response.choices[0].message, 'reasoning_content'):
|
||||
if reasoning_steps and hasattr(final_response.choices[0].message, 'reasoning_content') and final_response.choices[0].message.reasoning_content:
|
||||
return final_response.choices[0].message.reasoning_content
|
||||
return final_response.choices[0].message.content if final_response else full_response_text
|
||||
|
||||
@@ -4192,16 +4258,86 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
return None
|
||||
|
||||
async def arun(self, prompt: str, **kwargs):
|
||||
"""Async alias for astart() method"""
|
||||
return await self.astart(prompt, **kwargs)
|
||||
"""Async version of run() - silent, non-streaming, returns structured result.
|
||||
|
||||
Production-friendly async execution. Does not stream or display output.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt to process
|
||||
**kwargs: Additional arguments passed to achat()
|
||||
|
||||
Returns:
|
||||
The agent's response as a string
|
||||
"""
|
||||
# Force non-streaming, non-display for production use
|
||||
kwargs['stream'] = False
|
||||
return await self.achat(prompt, **kwargs)
|
||||
|
||||
async def astart(self, prompt: str, **kwargs):
|
||||
"""Async version of start method"""
|
||||
"""Async version of start() - interactive, streaming-aware.
|
||||
|
||||
Beginner-friendly async execution. Streams by default when in TTY.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt to process
|
||||
**kwargs: Additional arguments passed to achat()
|
||||
|
||||
Returns:
|
||||
The agent's response as a string
|
||||
"""
|
||||
import sys
|
||||
|
||||
# Determine streaming behavior (same logic as start())
|
||||
stream_requested = kwargs.get('stream')
|
||||
if stream_requested is None:
|
||||
if getattr(self, 'stream', None) is not None:
|
||||
stream_requested = self.stream
|
||||
else:
|
||||
stream_requested = sys.stdout.isatty()
|
||||
|
||||
kwargs['stream'] = stream_requested
|
||||
return await self.achat(prompt, **kwargs)
|
||||
|
||||
def run(self, prompt: str, **kwargs):
|
||||
"""Alias for start() method"""
|
||||
return self.start(prompt, **kwargs)
|
||||
"""Execute agent silently and return structured result.
|
||||
|
||||
Production-friendly execution. Does not stream or display output by default.
|
||||
Use this for programmatic/scripted usage where you want the result only.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt to process
|
||||
**kwargs: Additional arguments:
|
||||
- stream (bool): Force streaming if True. Default: False
|
||||
- display (bool): Force terminal display if True. Default: False
|
||||
- output (str): Output preset override
|
||||
|
||||
Returns:
|
||||
The agent's response as a string
|
||||
|
||||
Example:
|
||||
```python
|
||||
agent = Agent(instructions="You are helpful")
|
||||
result = agent.run("What is 2+2?") # Silent, returns "4"
|
||||
print(result)
|
||||
```
|
||||
"""
|
||||
# Production defaults: no streaming, no display
|
||||
if 'stream' not in kwargs:
|
||||
kwargs['stream'] = False
|
||||
|
||||
# Load history context
|
||||
self._load_history_context()
|
||||
|
||||
# Check if planning mode is enabled
|
||||
if self.planning:
|
||||
result = self._start_with_planning(prompt, **kwargs)
|
||||
else:
|
||||
result = self.chat(prompt, **kwargs)
|
||||
|
||||
# Auto-save session if enabled
|
||||
self._auto_save_session()
|
||||
|
||||
return result
|
||||
|
||||
def _get_planning_agent(self):
|
||||
"""Lazy load PlanningAgent for planning mode."""
|
||||
@@ -4336,18 +4472,59 @@ Write the complete compiled report:"""
|
||||
# Chat history is preserved in self.chat_history (no action needed)
|
||||
|
||||
def start(self, prompt: str, **kwargs):
|
||||
"""Start the agent with a prompt. This is a convenience method that wraps chat()."""
|
||||
# Load history from past sessions (now handled via context= param)
|
||||
"""Start the agent interactively with streaming output.
|
||||
|
||||
Beginner-friendly execution. Streams output by default when running in a TTY.
|
||||
Use this for interactive/terminal usage where you want to see output in real-time.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt to process
|
||||
**kwargs: Additional arguments:
|
||||
- stream (bool | None): Override streaming. None = auto-detect TTY
|
||||
- display (bool | None): Override display. None = auto (True if streaming)
|
||||
- output (str): Output preset override
|
||||
|
||||
Returns:
|
||||
- If streaming: Generator yielding response chunks
|
||||
- If not streaming: The complete response as a string
|
||||
|
||||
Example:
|
||||
```python
|
||||
agent = Agent(instructions="You are helpful")
|
||||
|
||||
# Interactive use - streams by default in terminal
|
||||
for chunk in agent.start("Tell me a story"):
|
||||
print(chunk, end="", flush=True)
|
||||
|
||||
# Or let it handle display automatically
|
||||
result = agent.start("What is 2+2?") # Streams if TTY
|
||||
```
|
||||
"""
|
||||
import sys
|
||||
|
||||
# Load history from past sessions
|
||||
self._load_history_context()
|
||||
|
||||
# Determine streaming behavior
|
||||
# Priority: explicit kwarg > agent's stream attribute > TTY detection
|
||||
stream_requested = kwargs.get('stream')
|
||||
if stream_requested is None:
|
||||
# Check agent's stream attribute first
|
||||
if getattr(self, 'stream', None) is not None:
|
||||
stream_requested = self.stream
|
||||
else:
|
||||
# Auto-detect: stream if stdout is a TTY (interactive terminal)
|
||||
stream_requested = sys.stdout.isatty()
|
||||
|
||||
# Check if planning mode is enabled
|
||||
if self.planning:
|
||||
result = self._start_with_planning(prompt, **kwargs)
|
||||
elif kwargs.get('stream', getattr(self, 'stream', False)):
|
||||
elif stream_requested:
|
||||
# Return a generator for streaming response
|
||||
kwargs['stream'] = True
|
||||
result = self._start_stream(prompt, **kwargs)
|
||||
else:
|
||||
# Return regular chat response for backward compatibility
|
||||
# Return regular chat response
|
||||
kwargs['stream'] = False
|
||||
result = self.chat(prompt, **kwargs)
|
||||
|
||||
@@ -4356,6 +4533,48 @@ Write the complete compiled report:"""
|
||||
|
||||
return result
|
||||
|
||||
def iter_stream(self, prompt: str, **kwargs):
|
||||
"""Stream agent response as an iterator of chunks.
|
||||
|
||||
App-friendly streaming. Yields response chunks without terminal display.
|
||||
Use this for building custom UIs or processing streams programmatically.
|
||||
|
||||
Args:
|
||||
prompt: The input prompt to process
|
||||
**kwargs: Additional arguments:
|
||||
- display (bool): Show terminal output. Default: False
|
||||
- output (str): Output preset override
|
||||
|
||||
Yields:
|
||||
str: Response chunks as they are generated
|
||||
|
||||
Example:
|
||||
```python
|
||||
agent = Agent(instructions="You are helpful")
|
||||
|
||||
# Process stream programmatically
|
||||
full_response = ""
|
||||
for chunk in agent.iter_stream("Tell me a story"):
|
||||
full_response += chunk
|
||||
# Custom processing here
|
||||
|
||||
# Or collect all at once
|
||||
response = "".join(agent.iter_stream("Hello"))
|
||||
```
|
||||
"""
|
||||
# Load history context
|
||||
self._load_history_context()
|
||||
|
||||
# Force streaming, no display by default (app-friendly)
|
||||
kwargs['stream'] = True
|
||||
|
||||
# Use the internal streaming generator
|
||||
for chunk in self._start_stream(prompt, **kwargs):
|
||||
yield chunk
|
||||
|
||||
# Auto-save session if enabled
|
||||
self._auto_save_session()
|
||||
|
||||
def _load_history_context(self):
|
||||
"""Load history from past sessions into context.
|
||||
|
||||
|
||||
@@ -434,7 +434,7 @@ This report contains all agent interactions and outputs from a complete ContextA
|
||||
|
||||
Provide comprehensive analysis that follows the PRD template principles and enables
|
||||
AI assistants to implement features that perfectly match existing codebase patterns.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -515,7 +515,7 @@ codebase style and architecture following PRD template principles."""
|
||||
role="Expert Manual Codebase Analysis Specialist",
|
||||
goal="Perform comprehensive manual codebase analysis following PRD methodology",
|
||||
instructions="""Analyze the codebase samples following PRD template methodology for complete understanding.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -566,7 +566,7 @@ Analyze following PRD principles to extract patterns, conventions, and architect
|
||||
6. Design pattern implementations
|
||||
7. Code complexity metrics
|
||||
8. API and interface patterns""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -624,7 +624,7 @@ Extract comprehensive patterns that follow PRD template principles for implement
|
||||
|
||||
For each pattern, provide the pattern name, where it's used, and how to replicate it
|
||||
following PRD template principles.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -674,7 +674,7 @@ patterns and best practices for first-try success."""
|
||||
goal="Analyze testing patterns for comprehensive validation framework design",
|
||||
instructions="""Analyze testing patterns to understand validation approaches and create
|
||||
comprehensive test frameworks following PRD methodology.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -801,7 +801,7 @@ Extract testing patterns for validation framework creation following PRD princip
|
||||
Confidence level for one-pass implementation
|
||||
|
||||
Generate PRPs following this EXACT structure for first-try implementation success.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -850,7 +850,7 @@ on the first try following PRD template principles."""
|
||||
6. CODE QUALITY: Complexity analysis, maintainability
|
||||
7. DOCUMENTATION VALIDATION: Documentation completeness
|
||||
8. DEPENDENCY VALIDATION: Dependency analysis and security""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -897,7 +897,7 @@ following PRD template principles."""
|
||||
instructions="""Compile all available documentation following PRD methodology including:
|
||||
README files, API documentation, setup guides, architecture docs, and any other
|
||||
relevant documentation that provides context for implementation.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -943,7 +943,7 @@ following PRD template principles."""
|
||||
instructions="""Analyze integration points following PRD methodology including:
|
||||
APIs, databases, external services, configuration points, and any other
|
||||
integration requirements that affect implementation.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -1004,7 +1004,7 @@ following PRD template principles."""
|
||||
8. DOCUMENTATION UPDATES: Documentation to create/update
|
||||
9. INTEGRATION STEPS: How to integrate with existing systems
|
||||
10. VALIDATION CHECKPOINTS: Validation steps at each phase""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -1302,7 +1302,7 @@ Every agent interaction has been saved for full audit trail and reproducibility.
|
||||
GOAL: [extracted implementation goal]
|
||||
|
||||
Be precise and extract only what is explicitly mentioned or clearly implied.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -1536,7 +1536,7 @@ Note: Detailed function/class metadata not available due to content access limit
|
||||
5. Documentation topics
|
||||
|
||||
Make the output easy for a file selection agent to understand which files contain what functionality.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -1770,7 +1770,7 @@ Focus on creating clear, structured metadata that will help with intelligent fil
|
||||
["README.md", "src/auth/login.py", "config/settings.py", ...]
|
||||
|
||||
Maximum 50 files for efficient analysis.""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -1923,7 +1923,7 @@ Maximum 50 files.""".format(goal=goal)
|
||||
8. EXAMPLES: Similar features that can guide {goal} implementation
|
||||
|
||||
Since these files were pre-selected for relevance, provide deep analysis of how each contributes to implementing: {goal}""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -2024,7 +2024,7 @@ Since these files were pre-selected for relevance, explain how each contributes
|
||||
- Success criteria for {goal}
|
||||
|
||||
Focus everything on successfully implementing: {goal}""",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-5-nano",
|
||||
llm=self.llm if hasattr(self, 'llm') else "gpt-4o-mini",
|
||||
verbose=getattr(self, 'verbose', True)
|
||||
)
|
||||
|
||||
@@ -2303,13 +2303,13 @@ def create_context_agent(llm: Optional[Union[str, Any]] = None, **kwargs) -> Con
|
||||
Factory function to create a ContextAgent following Context Engineering and PRD methodology.
|
||||
|
||||
Args:
|
||||
llm: Language model to use (e.g., "gpt-5-nano", "claude-3-haiku")
|
||||
llm: Language model to use (e.g., "gpt-4o-mini", "claude-3-haiku")
|
||||
**kwargs: Additional arguments to pass to ContextAgent constructor
|
||||
|
||||
Returns:
|
||||
ContextAgent: Configured ContextAgent for comprehensive context generation following PRD principles
|
||||
"""
|
||||
if llm is None:
|
||||
llm = "gpt-5-nano"
|
||||
llm = "gpt-4o-mini"
|
||||
|
||||
return ContextAgent(llm=llm, **kwargs)
|
||||
@@ -17,7 +17,7 @@ Supported Expansion Strategies:
|
||||
Example:
|
||||
from praisonaiagents import PromptExpanderAgent, ExpandStrategy
|
||||
|
||||
agent = PromptExpanderAgent(verbose=True)
|
||||
agent = PromptExpanderAgent()
|
||||
|
||||
# Basic expansion
|
||||
result = agent.expand("write a movie script in 3 lines")
|
||||
@@ -29,7 +29,7 @@ Example:
|
||||
|
||||
# With tools for context gathering
|
||||
from praisonaiagents.tools import internet_search
|
||||
agent = PromptExpanderAgent(tools=[internet_search], verbose=True)
|
||||
agent = PromptExpanderAgent(tools=[internet_search])
|
||||
result = agent.expand("latest AI developments")
|
||||
"""
|
||||
|
||||
@@ -73,13 +73,12 @@ class PromptExpanderAgent:
|
||||
|
||||
Attributes:
|
||||
name: Name of the agent
|
||||
model: LLM model to use for expansion
|
||||
verbose: Whether to print detailed logs
|
||||
model: LLM model to use for expansion (alias for llm=)
|
||||
temperature: Temperature for LLM generation (higher = more creative)
|
||||
tools: Optional tools for context gathering
|
||||
|
||||
Example:
|
||||
agent = PromptExpanderAgent(model="gpt-4o-mini", verbose=True)
|
||||
agent = PromptExpanderAgent(model="gpt-4o-mini")
|
||||
result = agent.expand("write a poem", strategy=ExpandStrategy.CREATIVE)
|
||||
print(result.expanded_prompt)
|
||||
"""
|
||||
@@ -230,8 +229,7 @@ Expanded prompt:"""
|
||||
backstory="You are an expert at understanding user intent and transforming brief requests into comprehensive, detailed prompts.",
|
||||
tools=self.tools,
|
||||
llm=self.model,
|
||||
verbose=self.verbose,
|
||||
markdown=False
|
||||
output={"verbose": self.verbose, "markdown": False}
|
||||
)
|
||||
return self._agent
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Supported Rewriting Strategies:
|
||||
Example:
|
||||
from praisonaiagents import QueryRewriterAgent, RewriteStrategy
|
||||
|
||||
agent = QueryRewriterAgent(verbose=True)
|
||||
agent = QueryRewriterAgent()
|
||||
|
||||
# Basic rewriting
|
||||
result = agent.rewrite("AI trends")
|
||||
@@ -34,7 +34,7 @@ Example:
|
||||
|
||||
# With search tools for context-aware rewriting
|
||||
from praisonaiagents.tools import internet_search
|
||||
agent = QueryRewriterAgent(tools=[internet_search], verbose=True)
|
||||
agent = QueryRewriterAgent(tools=[internet_search])
|
||||
result = agent.rewrite("latest AI developments") # Searches first, then rewrites
|
||||
"""
|
||||
|
||||
@@ -96,13 +96,12 @@ class QueryRewriterAgent:
|
||||
|
||||
Attributes:
|
||||
name: Name of the agent
|
||||
model: LLM model to use for rewriting
|
||||
verbose: Whether to print detailed logs
|
||||
model: LLM model to use for rewriting (alias for llm=)
|
||||
max_queries: Maximum number of queries to generate for multi-query strategy
|
||||
abbreviations: Dictionary of common abbreviations to expand
|
||||
|
||||
Example:
|
||||
agent = QueryRewriterAgent(model="gpt-4o-mini", verbose=True)
|
||||
agent = QueryRewriterAgent(model="gpt-4o-mini")
|
||||
result = agent.rewrite("ML best practices", strategy=RewriteStrategy.MULTI_QUERY)
|
||||
for query in result.rewritten_queries:
|
||||
print(query)
|
||||
@@ -288,8 +287,7 @@ Improved version:"""
|
||||
backstory="You are an expert at understanding user intent and transforming queries for optimal information retrieval.",
|
||||
tools=self.tools,
|
||||
llm=self.model,
|
||||
verbose=self.verbose,
|
||||
markdown=False
|
||||
output={"verbose": self.verbose, "markdown": False}
|
||||
)
|
||||
return self._agent
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class RouterAgent(Agent):
|
||||
# Initialize model router
|
||||
self.model_router = model_router or ModelRouter()
|
||||
self.routing_strategy = routing_strategy
|
||||
self.fallback_model = fallback_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.fallback_model = fallback_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
|
||||
# Process models configuration
|
||||
self.available_models = self._process_models_config(models)
|
||||
|
||||
@@ -142,6 +142,13 @@ class Agents:
|
||||
output: Optional[Any] = None, # Union[str, MultiAgentOutputConfig]
|
||||
execution: Optional[Any] = None, # Union[str, MultiAgentExecutionConfig]
|
||||
hooks: Optional[Any] = None, # MultiAgentHooksConfig
|
||||
# Additional consolidated params for feature parity with Agent
|
||||
autonomy: Optional[Any] = None, # Union[bool, AutonomyConfig] - agent autonomy
|
||||
knowledge: Optional[Any] = None, # Union[bool, List[str], KnowledgeConfig] - RAG
|
||||
guardrails: Optional[Any] = None, # Union[bool, Callable, GuardrailConfig] - validation
|
||||
web: Optional[Any] = None, # Union[bool, WebConfig] - web search/fetch
|
||||
reflection: Optional[Any] = None, # Union[bool, ReflectionConfig] - self-reflection
|
||||
caching: Optional[Any] = None, # Union[bool, CachingConfig] - caching
|
||||
):
|
||||
"""
|
||||
Initialize PraisonAIAgents with consolidated feature parameters.
|
||||
@@ -159,13 +166,26 @@ class Agents:
|
||||
output: Output configuration (str | MultiAgentOutputConfig)
|
||||
execution: Execution configuration (str | MultiAgentExecutionConfig)
|
||||
hooks: Hooks configuration (MultiAgentHooksConfig)
|
||||
autonomy: Autonomy configuration (bool | AutonomyConfig)
|
||||
knowledge: Knowledge/RAG configuration (bool | List[str] | KnowledgeConfig)
|
||||
guardrails: Guardrails configuration (bool | Callable | GuardrailConfig)
|
||||
web: Web search/fetch configuration (bool | WebConfig)
|
||||
reflection: Self-reflection configuration (bool | ReflectionConfig)
|
||||
"""
|
||||
# Store new params for propagation to agents
|
||||
self._autonomy = autonomy
|
||||
self._knowledge = knowledge
|
||||
self._guardrails = guardrails
|
||||
self._web = web
|
||||
self._reflection = reflection
|
||||
self._caching = caching
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Extract values from consolidated params using unified resolver
|
||||
# Precedence: Instance > Config > Array > String > Bool > Default
|
||||
# Extract values from consolidated params using UNIFIED CANONICAL resolver
|
||||
# Precedence: Instance > Config > Dict > Array > String > Bool > Default
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Import presets for unified resolution
|
||||
# Import canonical resolver and presets
|
||||
from ..config.param_resolver import resolve, ArrayMode
|
||||
from ..config.presets import (
|
||||
MULTI_AGENT_OUTPUT_PRESETS, MULTI_AGENT_EXECUTION_PRESETS,
|
||||
MEMORY_PRESETS, MEMORY_URL_SCHEMES,
|
||||
@@ -186,156 +206,118 @@ class Agents:
|
||||
MultiAgentHooksConfig = None
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve OUTPUT param
|
||||
# Resolve OUTPUT param using canonical resolver
|
||||
# Supports: None, str preset, list [preset, overrides], Config, dict
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
_verbose = 0
|
||||
_stream = True
|
||||
if output is None:
|
||||
pass # Use defaults
|
||||
elif isinstance(output, str):
|
||||
preset = MULTI_AGENT_OUTPUT_PRESETS.get(output, {})
|
||||
_verbose = preset.get("verbose", 0)
|
||||
_stream = preset.get("stream", True)
|
||||
elif isinstance(output, list) and len(output) >= 1:
|
||||
# Array mode: [preset] or [preset, overrides]
|
||||
preset_name = output[0] if isinstance(output[0], str) else None
|
||||
overrides = output[-1] if len(output) > 1 and isinstance(output[-1], dict) else {}
|
||||
if preset_name and preset_name in MULTI_AGENT_OUTPUT_PRESETS:
|
||||
preset = MULTI_AGENT_OUTPUT_PRESETS[preset_name].copy()
|
||||
preset.update(overrides)
|
||||
_verbose = preset.get("verbose", 0)
|
||||
_stream = preset.get("stream", True)
|
||||
elif MultiAgentOutputConfig and isinstance(output, MultiAgentOutputConfig):
|
||||
_verbose = output.verbose
|
||||
_stream = output.stream
|
||||
elif isinstance(output, dict):
|
||||
_verbose = output.get("verbose", 0)
|
||||
_stream = output.get("stream", True)
|
||||
_output_config = resolve(
|
||||
value=output,
|
||||
param_name="output",
|
||||
config_class=MultiAgentOutputConfig,
|
||||
presets=MULTI_AGENT_OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
default=MultiAgentOutputConfig() if MultiAgentOutputConfig else None,
|
||||
)
|
||||
if _output_config and hasattr(_output_config, 'verbose'):
|
||||
_verbose = _output_config.verbose
|
||||
_stream = _output_config.stream
|
||||
else:
|
||||
_verbose = 0
|
||||
_stream = True
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve EXECUTION param
|
||||
# Resolve EXECUTION param using canonical resolver
|
||||
# Supports: None, str preset, list [preset, overrides], Config, dict
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
_max_iter = 10
|
||||
_max_retries = 5
|
||||
if execution is None:
|
||||
pass # Use defaults
|
||||
elif isinstance(execution, str):
|
||||
preset = MULTI_AGENT_EXECUTION_PRESETS.get(execution, {})
|
||||
_max_iter = preset.get("max_iter", 10)
|
||||
_max_retries = preset.get("max_retries", 5)
|
||||
elif isinstance(execution, list) and len(execution) >= 1:
|
||||
# Array mode: [preset] or [preset, overrides]
|
||||
preset_name = execution[0] if isinstance(execution[0], str) else None
|
||||
overrides = execution[-1] if len(execution) > 1 and isinstance(execution[-1], dict) else {}
|
||||
if preset_name and preset_name in MULTI_AGENT_EXECUTION_PRESETS:
|
||||
preset = MULTI_AGENT_EXECUTION_PRESETS[preset_name].copy()
|
||||
preset.update(overrides)
|
||||
_max_iter = preset.get("max_iter", 10)
|
||||
_max_retries = preset.get("max_retries", 5)
|
||||
elif MultiAgentExecutionConfig and isinstance(execution, MultiAgentExecutionConfig):
|
||||
_max_iter = execution.max_iter
|
||||
_max_retries = execution.max_retries
|
||||
elif isinstance(execution, dict):
|
||||
_max_iter = execution.get("max_iter", 10)
|
||||
_max_retries = execution.get("max_retries", 5)
|
||||
_exec_config = resolve(
|
||||
value=execution,
|
||||
param_name="execution",
|
||||
config_class=MultiAgentExecutionConfig,
|
||||
presets=MULTI_AGENT_EXECUTION_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
default=MultiAgentExecutionConfig() if MultiAgentExecutionConfig else None,
|
||||
)
|
||||
if _exec_config and hasattr(_exec_config, 'max_iter'):
|
||||
_max_iter = _exec_config.max_iter
|
||||
_max_retries = _exec_config.max_retries
|
||||
else:
|
||||
_max_iter = 10
|
||||
_max_retries = 5
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve HOOKS param
|
||||
# Resolve HOOKS param using canonical resolver
|
||||
# Supports: None, list, Config, dict
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
_completion_checker = None
|
||||
_on_task_start = None
|
||||
_on_task_complete = None
|
||||
if hooks is not None:
|
||||
if MultiAgentHooksConfig and isinstance(hooks, MultiAgentHooksConfig):
|
||||
_completion_checker = hooks.completion_checker
|
||||
_on_task_start = hooks.on_task_start
|
||||
_on_task_complete = hooks.on_task_complete
|
||||
elif isinstance(hooks, dict):
|
||||
_completion_checker = hooks.get("completion_checker")
|
||||
_on_task_start = hooks.get("on_task_start")
|
||||
_on_task_complete = hooks.get("on_task_complete")
|
||||
_hooks_config = resolve(
|
||||
value=hooks,
|
||||
param_name="hooks",
|
||||
config_class=MultiAgentHooksConfig,
|
||||
array_mode=ArrayMode.PASSTHROUGH,
|
||||
default=None,
|
||||
)
|
||||
if _hooks_config and hasattr(_hooks_config, 'completion_checker'):
|
||||
_completion_checker = _hooks_config.completion_checker
|
||||
_on_task_start = _hooks_config.on_task_start
|
||||
_on_task_complete = _hooks_config.on_task_complete
|
||||
else:
|
||||
_completion_checker = None
|
||||
_on_task_start = None
|
||||
_on_task_complete = None
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve MEMORY param
|
||||
# Resolve MEMORY param using canonical resolver
|
||||
# Supports: None, bool, str preset/URL, list, Config, dict, Instance
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
_memory_config_resolved = resolve(
|
||||
value=memory,
|
||||
param_name="memory",
|
||||
config_class=MultiAgentMemoryConfig,
|
||||
presets=MEMORY_PRESETS,
|
||||
url_schemes=MEMORY_URL_SCHEMES,
|
||||
instance_check=lambda v: hasattr(v, 'database_url'),
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Extract values from resolved memory config
|
||||
_user_id = "praison"
|
||||
_memory_config = None
|
||||
_embedder = None
|
||||
|
||||
if memory is None or memory is False:
|
||||
pass # Disabled
|
||||
elif memory is True:
|
||||
pass # Use defaults
|
||||
elif isinstance(memory, str):
|
||||
# Check for URL
|
||||
if "://" in memory:
|
||||
# URL - extract backend from scheme
|
||||
scheme = memory.split("://")[0].lower()
|
||||
backend = MEMORY_URL_SCHEMES.get(scheme)
|
||||
if backend:
|
||||
_memory_config = {"url": memory, "backend": backend}
|
||||
elif memory in MEMORY_PRESETS:
|
||||
# Preset name
|
||||
_memory_config = MEMORY_PRESETS[memory]
|
||||
elif isinstance(memory, list) and len(memory) >= 1:
|
||||
# Array mode: [URL] or [preset, overrides]
|
||||
first = memory[0]
|
||||
if isinstance(first, str) and "://" in first:
|
||||
scheme = first.split("://")[0].lower()
|
||||
backend = MEMORY_URL_SCHEMES.get(scheme)
|
||||
if backend:
|
||||
_memory_config = {"url": first, "backend": backend}
|
||||
elif MultiAgentMemoryConfig and isinstance(memory, MultiAgentMemoryConfig):
|
||||
_user_id = memory.user_id or "praison"
|
||||
_memory_config = memory.config
|
||||
_embedder = memory.embedder
|
||||
elif isinstance(memory, dict):
|
||||
_user_id = memory.get("user_id", "praison")
|
||||
_memory_config = memory.get("config")
|
||||
_embedder = memory.get("embedder")
|
||||
elif hasattr(memory, 'database_url'):
|
||||
# db() instance
|
||||
_memory_config = {"db_instance": memory}
|
||||
if _memory_config_resolved is not None:
|
||||
if hasattr(_memory_config_resolved, 'database_url'):
|
||||
# db() instance - pass through
|
||||
_memory_config = {"db_instance": _memory_config_resolved}
|
||||
elif MultiAgentMemoryConfig and isinstance(_memory_config_resolved, MultiAgentMemoryConfig):
|
||||
_user_id = _memory_config_resolved.user_id or "praison"
|
||||
_memory_config = _memory_config_resolved.config
|
||||
_embedder = _memory_config_resolved.embedder
|
||||
elif isinstance(_memory_config_resolved, dict):
|
||||
# Dict from preset resolution
|
||||
_memory_config = _memory_config_resolved
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Resolve PLANNING param
|
||||
# Resolve PLANNING param using canonical resolver
|
||||
# Supports: None, bool, str LLM, list, Config, dict
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
_planning_config = resolve(
|
||||
value=planning,
|
||||
param_name="planning",
|
||||
config_class=MultiAgentPlanningConfig,
|
||||
string_mode="llm_model",
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Extract values from resolved planning config
|
||||
_planning_llm = "gpt-4o-mini"
|
||||
_auto_approve_plan = False
|
||||
_planning_tools = None
|
||||
_planning_reasoning = False
|
||||
|
||||
if planning is None or planning is False:
|
||||
pass # Disabled
|
||||
elif planning is True:
|
||||
pass # Use defaults
|
||||
elif isinstance(planning, str):
|
||||
# String = LLM model name
|
||||
_planning_llm = planning
|
||||
elif isinstance(planning, list) and len(planning) >= 1:
|
||||
# Array mode: [llm] or [llm, overrides]
|
||||
if isinstance(planning[0], str):
|
||||
_planning_llm = planning[0]
|
||||
if len(planning) > 1 and isinstance(planning[-1], dict):
|
||||
overrides = planning[-1]
|
||||
_auto_approve_plan = overrides.get("auto_approve", False)
|
||||
_planning_tools = overrides.get("tools")
|
||||
_planning_reasoning = overrides.get("reasoning", False)
|
||||
elif MultiAgentPlanningConfig and isinstance(planning, MultiAgentPlanningConfig):
|
||||
_planning_llm = planning.llm or "gpt-4o-mini"
|
||||
_auto_approve_plan = planning.auto_approve
|
||||
_planning_tools = planning.tools
|
||||
_planning_reasoning = planning.reasoning
|
||||
elif isinstance(planning, dict):
|
||||
_planning_llm = planning.get("llm", "gpt-4o-mini")
|
||||
_auto_approve_plan = planning.get("auto_approve", False)
|
||||
_planning_tools = planning.get("tools")
|
||||
_planning_reasoning = planning.get("reasoning", False)
|
||||
if _planning_config is not None:
|
||||
if MultiAgentPlanningConfig and isinstance(_planning_config, MultiAgentPlanningConfig):
|
||||
_planning_llm = _planning_config.llm or "gpt-4o-mini"
|
||||
_auto_approve_plan = _planning_config.auto_approve
|
||||
_planning_tools = _planning_config.tools
|
||||
_planning_reasoning = _planning_config.reasoning
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Memory dependency check
|
||||
@@ -381,7 +363,7 @@ class Agents:
|
||||
self.variables = variables if variables else {}
|
||||
|
||||
# Check for manager_llm in environment variable if not provided
|
||||
self.manager_llm = manager_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.manager_llm = manager_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
|
||||
# Set logger level based on verbose
|
||||
if _verbose >= 5:
|
||||
|
||||
@@ -74,6 +74,17 @@ class AutoAgents(Agents):
|
||||
api_key: Optional[str] = None,
|
||||
max_agents: int = 3, # Maximum number of agents
|
||||
autonomy: Optional[Any] = None, # Union[bool, dict, AutonomyConfig] - Enable autonomy for all agents
|
||||
# ============================================================
|
||||
# CONSOLIDATED PARAMS (new SDK contract - take precedence)
|
||||
# ============================================================
|
||||
output: Optional[Any] = None, # Union[str, OutputConfig] - overrides verbose/markdown
|
||||
reflection: Optional[Any] = None, # Union[bool, ReflectionConfig] - overrides self_reflect/max_reflect/min_reflect/reflect_llm
|
||||
caching: Optional[Any] = None, # Union[bool, CachingConfig] - overrides cache
|
||||
knowledge: Optional[Any] = None, # Union[bool, List[str], KnowledgeConfig] - overrides knowledge_sources/embedder_config
|
||||
execution: Optional[Any] = None, # Union[str, ExecutionConfig] - overrides max_iter
|
||||
guardrails: Optional[Any] = None, # Union[bool, Callable, GuardrailConfig]
|
||||
web: Optional[Any] = None, # Union[bool, WebConfig]
|
||||
hooks: Optional[Any] = None, # HooksConfig
|
||||
):
|
||||
"""Initialize AutoAgents with configuration for automatic agent and task creation."""
|
||||
if max_agents < 1:
|
||||
@@ -92,7 +103,7 @@ class AutoAgents(Agents):
|
||||
self.self_reflect = self_reflect
|
||||
self.max_reflect = max_reflect
|
||||
self.min_reflect = min_reflect
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
self.function_calling_llm = function_calling_llm
|
||||
self.code_execution_mode = code_execution_mode
|
||||
self.embedder_config = embedder_config
|
||||
@@ -112,6 +123,16 @@ class AutoAgents(Agents):
|
||||
self.api_key = api_key
|
||||
self.autonomy = autonomy # Store autonomy config for agents
|
||||
|
||||
# Store consolidated params (new SDK contract - take precedence over legacy)
|
||||
self._output = output
|
||||
self._reflection = reflection
|
||||
self._caching = caching
|
||||
self._knowledge = knowledge
|
||||
self._execution = execution
|
||||
self._guardrails = guardrails
|
||||
self._web = web
|
||||
self._hooks = hooks
|
||||
|
||||
# Display initial instruction
|
||||
if self.verbose:
|
||||
display_instruction(f"🎯 Main Task: {self.instructions}")
|
||||
@@ -129,14 +150,21 @@ class AutoAgents(Agents):
|
||||
self._display_agents_and_tasks(agents, tasks)
|
||||
|
||||
# Initialize parent class with generated agents and tasks
|
||||
# Use consolidated output= instead of legacy verbose=
|
||||
super().__init__(
|
||||
agents=agents,
|
||||
tasks=tasks,
|
||||
verbose=verbose,
|
||||
completion_checker=completion_checker,
|
||||
max_retries=max_retries,
|
||||
process=process,
|
||||
manager_llm=manager_llm or self.llm
|
||||
manager_llm=manager_llm or self.llm,
|
||||
output=self._output, # Consolidated output param
|
||||
execution=self._execution,
|
||||
hooks=self._hooks,
|
||||
memory=self.memory,
|
||||
reflection=self._reflection,
|
||||
caching=self._caching,
|
||||
guardrails=self._guardrails,
|
||||
web=self._web,
|
||||
autonomy=self.autonomy,
|
||||
)
|
||||
|
||||
def _display_agents_and_tasks(self, agents: List[Agent], tasks: List[Task]):
|
||||
@@ -431,38 +459,33 @@ DO NOT use strings for tasks. Each task MUST be a complete object with all four
|
||||
agent_tools = self._assign_tools_to_agent(agent_config)
|
||||
|
||||
# Create the agent with all parameters
|
||||
# Consolidated params take precedence over legacy params
|
||||
# Use consolidated knowledge if provided, else fall back to legacy knowledge_sources
|
||||
effective_knowledge = self._knowledge if self._knowledge is not None else self.knowledge_sources
|
||||
|
||||
agent = Agent(
|
||||
name=agent_config.name,
|
||||
role=agent_config.role,
|
||||
goal=agent_config.goal,
|
||||
backstory=agent_config.backstory,
|
||||
tools=agent_tools, # Use assigned tools
|
||||
verbose=self.verbose >= 1,
|
||||
allow_code_execution=self.allow_code_execution,
|
||||
memory=self.memory,
|
||||
markdown=self.markdown,
|
||||
self_reflect=self.self_reflect,
|
||||
max_reflect=self.max_reflect,
|
||||
min_reflect=self.min_reflect,
|
||||
llm=self.llm,
|
||||
function_calling_llm=self.function_calling_llm,
|
||||
llm=self.llm, # Consolidated LLM param
|
||||
code_execution_mode=self.code_execution_mode,
|
||||
embedder_config=self.embedder_config,
|
||||
knowledge=self.knowledge_sources,
|
||||
use_system_prompt=self.use_system_prompt,
|
||||
cache=self.cache,
|
||||
allow_delegation=self.allow_delegation,
|
||||
step_callback=self.step_callback,
|
||||
system_template=self.system_template,
|
||||
prompt_template=self.prompt_template,
|
||||
response_template=self.response_template,
|
||||
max_rpm=self.max_rpm,
|
||||
max_execution_time=self.max_execution_time,
|
||||
max_iter=self.max_iter,
|
||||
reflect_llm=self.reflect_llm,
|
||||
base_url=self.base_url,
|
||||
api_key=self.api_key,
|
||||
autonomy=self.autonomy # Pass autonomy config to agents
|
||||
# Consolidated params (new SDK contract)
|
||||
output=self._output, # Overrides verbose/markdown
|
||||
reflection=self._reflection, # Overrides self_reflect/max_reflect/min_reflect/reflect_llm
|
||||
caching=self._caching, # Overrides cache
|
||||
knowledge=effective_knowledge, # Overrides knowledge_sources/embedder_config
|
||||
execution=self._execution, # Overrides max_iter
|
||||
guardrails=self._guardrails,
|
||||
web=self._web,
|
||||
hooks=self._hooks,
|
||||
autonomy=self.autonomy,
|
||||
)
|
||||
agents.append(agent)
|
||||
|
||||
|
||||
@@ -376,7 +376,7 @@ class SubagentDelegator:
|
||||
return result
|
||||
|
||||
async def _create_agent(self, profile: AgentProfile, task: DelegationTask) -> Any:
|
||||
"""Create an agent from profile."""
|
||||
"""Create an agent from profile using consolidated params."""
|
||||
if self.agent_factory:
|
||||
return self.agent_factory(profile.name)
|
||||
|
||||
@@ -387,14 +387,17 @@ class SubagentDelegator:
|
||||
# Filter tools based on profile
|
||||
tools = self._get_tools_for_profile(profile)
|
||||
|
||||
# Use consolidated params (protocol-compliant)
|
||||
# output='silent' replaces verbose=False
|
||||
# execution={max_iter: N} replaces max_iter=N
|
||||
agent = Agent(
|
||||
name=profile.name,
|
||||
role=profile.name.title(),
|
||||
goal=task.objective,
|
||||
backstory=profile.system_prompt,
|
||||
tools=tools,
|
||||
verbose=False,
|
||||
max_iter=min(task.max_steps, profile.max_steps),
|
||||
output='silent',
|
||||
execution={'max_iter': min(task.max_steps, profile.max_steps)},
|
||||
)
|
||||
|
||||
return agent
|
||||
|
||||
@@ -44,12 +44,79 @@ class MemoryBackend(str, Enum):
|
||||
MONGODB = "mongodb"
|
||||
|
||||
|
||||
class LearnScope(str, Enum):
|
||||
"""Scope for learning data visibility.
|
||||
|
||||
PRIVATE: Learning data is private to this user/agent (default, safest)
|
||||
SHARED: Learning data is shared with all agents
|
||||
"""
|
||||
PRIVATE = "private" # Private to this user/agent
|
||||
SHARED = "shared" # Shared with all agents
|
||||
|
||||
|
||||
@dataclass
|
||||
class LearnConfig:
|
||||
"""
|
||||
Configuration for continuous learning within memory system.
|
||||
|
||||
Learning captures patterns, preferences, and insights from agent interactions
|
||||
to improve future responses. All learning data is stored within the memory system.
|
||||
|
||||
Usage:
|
||||
# Simple enable
|
||||
Agent(memory=MemoryConfig(learn=True))
|
||||
|
||||
# With specific capabilities
|
||||
Agent(memory=MemoryConfig(
|
||||
learn=LearnConfig(
|
||||
persona=True, # User preferences
|
||||
insights=True, # Observations
|
||||
patterns=True, # Reusable knowledge
|
||||
)
|
||||
))
|
||||
"""
|
||||
# Learning capabilities
|
||||
persona: bool = True # User preferences and profile
|
||||
insights: bool = True # Observations and learnings
|
||||
thread: bool = True # Session/conversation context
|
||||
patterns: bool = False # Reusable knowledge patterns
|
||||
decisions: bool = False # Decision logging
|
||||
feedback: bool = False # Outcome signals
|
||||
improvements: bool = False # Self-improvement proposals
|
||||
|
||||
# Scope configuration
|
||||
scope: Union[str, LearnScope] = LearnScope.PRIVATE
|
||||
|
||||
# Storage configuration
|
||||
store_path: Optional[str] = None # Custom storage path
|
||||
|
||||
# Maintenance settings
|
||||
auto_consolidate: bool = True # Auto-consolidate learnings
|
||||
retention_days: Optional[int] = None # Days to retain (None = forever)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"persona": self.persona,
|
||||
"insights": self.insights,
|
||||
"thread": self.thread,
|
||||
"patterns": self.patterns,
|
||||
"decisions": self.decisions,
|
||||
"feedback": self.feedback,
|
||||
"improvements": self.improvements,
|
||||
"scope": self.scope.value if isinstance(self.scope, LearnScope) else self.scope,
|
||||
"store_path": self.store_path,
|
||||
"auto_consolidate": self.auto_consolidate,
|
||||
"retention_days": self.retention_days,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryConfig:
|
||||
"""
|
||||
Configuration for agent memory and session management.
|
||||
|
||||
Consolidates: memory, auto_memory, claude_memory, user_id, session_id, db
|
||||
Consolidates: memory, auto_memory, claude_memory, user_id, session_id, db, learn
|
||||
|
||||
Usage:
|
||||
# Simple enable (uses FileMemory)
|
||||
@@ -58,12 +125,22 @@ class MemoryConfig:
|
||||
# With backend
|
||||
Agent(memory=MemoryConfig(backend="redis"))
|
||||
|
||||
# Full config
|
||||
# Full config with learning
|
||||
Agent(memory=MemoryConfig(
|
||||
backend="sqlite",
|
||||
user_id="user123",
|
||||
session_id="session456",
|
||||
auto_memory=True,
|
||||
learn=True, # Enable continuous learning
|
||||
))
|
||||
|
||||
# With detailed learn config
|
||||
Agent(memory=MemoryConfig(
|
||||
learn=LearnConfig(
|
||||
persona=True,
|
||||
insights=True,
|
||||
patterns=True,
|
||||
)
|
||||
))
|
||||
"""
|
||||
# Backend selection
|
||||
@@ -85,8 +162,17 @@ class MemoryConfig:
|
||||
# Memory configuration dict (for provider-specific settings)
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Continuous learning configuration
|
||||
learn: Optional[Union[bool, LearnConfig]] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
learn_dict = None
|
||||
if self.learn is True:
|
||||
learn_dict = LearnConfig().to_dict()
|
||||
elif isinstance(self.learn, LearnConfig):
|
||||
learn_dict = self.learn.to_dict()
|
||||
|
||||
return {
|
||||
"backend": self.backend.value if isinstance(self.backend, MemoryBackend) else self.backend,
|
||||
"user_id": self.user_id,
|
||||
@@ -94,6 +180,7 @@ class MemoryConfig:
|
||||
"auto_memory": self.auto_memory,
|
||||
"claude_memory": self.claude_memory,
|
||||
"config": self.config,
|
||||
"learn": learn_dict,
|
||||
}
|
||||
|
||||
|
||||
@@ -391,10 +478,21 @@ class OutputConfig:
|
||||
|
||||
Consolidates: verbose, markdown, stream, metrics, reasoning_steps, output_style
|
||||
|
||||
DEFAULT: output="silent" (zero overhead, fastest performance)
|
||||
|
||||
Usage:
|
||||
# Simple preset
|
||||
# Default is silent mode (no output overhead, programmatic use)
|
||||
Agent(instructions="...") # Uses output="silent"
|
||||
|
||||
# Actions mode (tool calls + final output trace)
|
||||
Agent(output="actions")
|
||||
|
||||
# Verbose mode with Rich panels
|
||||
Agent(output="verbose")
|
||||
|
||||
# JSON mode for piping
|
||||
Agent(output="json")
|
||||
|
||||
# With config
|
||||
Agent(output=OutputConfig(
|
||||
verbose=True,
|
||||
@@ -404,11 +502,11 @@ class OutputConfig:
|
||||
reasoning_steps=True,
|
||||
))
|
||||
"""
|
||||
# Verbosity
|
||||
verbose: bool = True
|
||||
# Verbosity - False by default for silent mode (fastest)
|
||||
verbose: bool = False
|
||||
|
||||
# Formatting
|
||||
markdown: bool = True
|
||||
# Formatting - False by default for silent mode
|
||||
markdown: bool = False
|
||||
|
||||
# Streaming
|
||||
stream: bool = False
|
||||
@@ -422,6 +520,14 @@ class OutputConfig:
|
||||
# Output style (custom styling)
|
||||
style: Optional[Any] = None
|
||||
|
||||
# Actions trace mode - shows tool calls, agent lifecycle, final output
|
||||
# When True, registers callbacks and outputs to stderr
|
||||
# DEFAULT: False for zero overhead (silent mode)
|
||||
actions_trace: bool = False
|
||||
|
||||
# JSON output mode - emit JSONL events for piping
|
||||
json_output: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
@@ -430,6 +536,8 @@ class OutputConfig:
|
||||
"stream": self.stream,
|
||||
"metrics": self.metrics,
|
||||
"reasoning_steps": self.reasoning_steps,
|
||||
"actions_trace": self.actions_trace,
|
||||
"json_output": self.json_output,
|
||||
}
|
||||
|
||||
|
||||
@@ -857,9 +965,259 @@ SkillsParam = Union[List[str], SkillsConfig]
|
||||
AutonomyParam = Union[bool, Dict[str, Any], AutonomyConfig]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRECEDENCE LADDER RESOLVERS
|
||||
# =============================================================================
|
||||
# Precedence: Instance > Config > Array > Dict > String > Bool > Default
|
||||
#
|
||||
# These resolvers normalize various input types into the canonical Config form.
|
||||
# This allows users to pass simplified inputs while maintaining type safety.
|
||||
|
||||
|
||||
def resolve_memory(value: MemoryParam) -> Optional[MemoryConfig]:
|
||||
"""
|
||||
Resolve memory= parameter following precedence ladder.
|
||||
|
||||
Precedence: Instance > Config > Dict > String > Bool > Default
|
||||
|
||||
Args:
|
||||
value: Memory parameter in any supported form
|
||||
|
||||
Returns:
|
||||
MemoryConfig if enabled, None if disabled
|
||||
|
||||
Examples:
|
||||
>>> resolve_memory(None) # Default: disabled
|
||||
None
|
||||
>>> resolve_memory(False) # Explicit disable
|
||||
None
|
||||
>>> resolve_memory(True) # Enable with defaults
|
||||
MemoryConfig(backend='file')
|
||||
>>> resolve_memory("redis") # String shorthand
|
||||
MemoryConfig(backend='redis')
|
||||
>>> resolve_memory({"backend": "sqlite", "user_id": "alice"}) # Dict
|
||||
MemoryConfig(backend='sqlite', user_id='alice')
|
||||
>>> resolve_memory(MemoryConfig(backend="postgres")) # Config passthrough
|
||||
MemoryConfig(backend='postgres')
|
||||
"""
|
||||
# Default: disabled
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Bool: False = disabled, True = defaults
|
||||
if value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return MemoryConfig()
|
||||
|
||||
# String: backend shorthand
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
backend = MemoryBackend(value.lower())
|
||||
except ValueError:
|
||||
backend = value # Allow custom backend strings
|
||||
return MemoryConfig(backend=backend)
|
||||
|
||||
# Dict: expand to config
|
||||
if isinstance(value, dict):
|
||||
# Handle backend enum conversion
|
||||
backend = value.get("backend", MemoryBackend.FILE)
|
||||
if isinstance(backend, str):
|
||||
try:
|
||||
backend = MemoryBackend(backend.lower())
|
||||
except ValueError:
|
||||
pass # Keep as string for custom backends
|
||||
return MemoryConfig(
|
||||
backend=backend,
|
||||
user_id=value.get("user_id"),
|
||||
session_id=value.get("session_id"),
|
||||
auto_memory=value.get("auto_memory", False),
|
||||
claude_memory=value.get("claude_memory", False),
|
||||
db=value.get("db"),
|
||||
config=value.get("config"),
|
||||
)
|
||||
|
||||
# Config: passthrough
|
||||
if isinstance(value, MemoryConfig):
|
||||
return value
|
||||
|
||||
# Instance (MemoryManager): return as-is (caller handles)
|
||||
# This is the highest precedence - user provided a pre-configured instance
|
||||
return value
|
||||
|
||||
|
||||
def resolve_knowledge(value: KnowledgeParam) -> Optional[KnowledgeConfig]:
|
||||
"""
|
||||
Resolve knowledge= parameter following precedence ladder.
|
||||
|
||||
Precedence: Instance > Config > Array > Dict > String > Bool > Default
|
||||
|
||||
Args:
|
||||
value: Knowledge parameter in any supported form
|
||||
|
||||
Returns:
|
||||
KnowledgeConfig if enabled, None if disabled
|
||||
"""
|
||||
# Default: disabled
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Bool: False = disabled, True = defaults
|
||||
if value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return KnowledgeConfig()
|
||||
|
||||
# String: single source
|
||||
if isinstance(value, str):
|
||||
return KnowledgeConfig(sources=[value])
|
||||
|
||||
# Array: list of sources
|
||||
if isinstance(value, list):
|
||||
return KnowledgeConfig(sources=value)
|
||||
|
||||
# Dict: expand to config
|
||||
if isinstance(value, dict):
|
||||
return KnowledgeConfig(
|
||||
sources=value.get("sources", []),
|
||||
embedder=value.get("embedder", "openai"),
|
||||
embedder_config=value.get("embedder_config"),
|
||||
chunking_strategy=value.get("chunking_strategy", ChunkingStrategy.SEMANTIC),
|
||||
chunk_size=value.get("chunk_size", 1000),
|
||||
chunk_overlap=value.get("chunk_overlap", 200),
|
||||
retrieval_k=value.get("retrieval_k", 5),
|
||||
retrieval_threshold=value.get("retrieval_threshold", 0.0),
|
||||
rerank=value.get("rerank", False),
|
||||
rerank_model=value.get("rerank_model"),
|
||||
auto_retrieve=value.get("auto_retrieve", True),
|
||||
)
|
||||
|
||||
# Config: passthrough
|
||||
if isinstance(value, KnowledgeConfig):
|
||||
return value
|
||||
|
||||
# Instance: return as-is
|
||||
return value
|
||||
|
||||
|
||||
def resolve_planning(value: PlanningParam) -> Optional[PlanningConfig]:
|
||||
"""Resolve planning= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return PlanningConfig()
|
||||
if isinstance(value, dict):
|
||||
return PlanningConfig(**value)
|
||||
if isinstance(value, PlanningConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_reflection(value: ReflectionParam) -> Optional[ReflectionConfig]:
|
||||
"""Resolve reflection= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return ReflectionConfig()
|
||||
if isinstance(value, dict):
|
||||
return ReflectionConfig(**value)
|
||||
if isinstance(value, ReflectionConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_guardrails(value: GuardrailParam) -> Optional[GuardrailConfig]:
|
||||
"""Resolve guardrails= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return GuardrailConfig()
|
||||
if callable(value):
|
||||
return GuardrailConfig(validator=value)
|
||||
if isinstance(value, dict):
|
||||
return GuardrailConfig(**value)
|
||||
if isinstance(value, GuardrailConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_web(value: WebParam) -> Optional[WebConfig]:
|
||||
"""Resolve web= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return WebConfig()
|
||||
if isinstance(value, dict):
|
||||
return WebConfig(**value)
|
||||
if isinstance(value, WebConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_output(value: OutputParam) -> Optional[OutputConfig]:
|
||||
"""Resolve output= parameter following precedence ladder."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
preset = OutputPreset(value.lower())
|
||||
return OutputConfig.from_preset(preset)
|
||||
except ValueError:
|
||||
return None
|
||||
if isinstance(value, dict):
|
||||
return OutputConfig(**value)
|
||||
if isinstance(value, OutputConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_execution(value: ExecutionParam) -> Optional[ExecutionConfig]:
|
||||
"""Resolve execution= parameter following precedence ladder."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
preset = ExecutionPreset(value.lower())
|
||||
return ExecutionConfig.from_preset(preset)
|
||||
except ValueError:
|
||||
return None
|
||||
if isinstance(value, dict):
|
||||
return ExecutionConfig(**value)
|
||||
if isinstance(value, ExecutionConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_caching(value: CachingParam) -> Optional[CachingConfig]:
|
||||
"""Resolve caching= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return CachingConfig()
|
||||
if isinstance(value, dict):
|
||||
return CachingConfig(**value)
|
||||
if isinstance(value, CachingConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_autonomy(value: AutonomyParam) -> Optional[AutonomyConfig]:
|
||||
"""Resolve autonomy= parameter following precedence ladder."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return AutonomyConfig()
|
||||
if isinstance(value, dict):
|
||||
return AutonomyConfig(**value)
|
||||
if isinstance(value, AutonomyConfig):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Enums
|
||||
"MemoryBackend",
|
||||
"LearnScope",
|
||||
"ChunkingStrategy",
|
||||
"GuardrailAction",
|
||||
"WebSearchProvider",
|
||||
@@ -867,6 +1225,7 @@ __all__ = [
|
||||
"ExecutionPreset",
|
||||
"AutonomyLevel",
|
||||
# Config classes (Agent)
|
||||
"LearnConfig",
|
||||
"MemoryConfig",
|
||||
"KnowledgeConfig",
|
||||
"PlanningConfig",
|
||||
@@ -900,4 +1259,15 @@ __all__ = [
|
||||
"HooksParam",
|
||||
"SkillsParam",
|
||||
"AutonomyParam",
|
||||
# Precedence ladder resolvers
|
||||
"resolve_memory",
|
||||
"resolve_knowledge",
|
||||
"resolve_planning",
|
||||
"resolve_reflection",
|
||||
"resolve_guardrails",
|
||||
"resolve_web",
|
||||
"resolve_output",
|
||||
"resolve_execution",
|
||||
"resolve_caching",
|
||||
"resolve_autonomy",
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Unified Parameter Resolver for Consolidated Parameters.
|
||||
|
||||
Implements the precedence rules: Instance > Config > Array > String > Bool > Default
|
||||
Implements the precedence rules: Instance > Config > Dict > Array > String > Bool > Default
|
||||
|
||||
This is the SINGLE, DRY resolver used by:
|
||||
- Agent
|
||||
@@ -12,7 +12,8 @@ This is the SINGLE, DRY resolver used by:
|
||||
Performance: O(1) happy path. Typo suggestions only on error path.
|
||||
"""
|
||||
|
||||
from typing import Any, Callable, Dict, Optional, Type, Union
|
||||
from typing import Any, Callable, Dict, Optional, Set, Type, Union
|
||||
import inspect
|
||||
|
||||
from .parse_utils import (
|
||||
detect_url_scheme,
|
||||
@@ -53,7 +54,7 @@ def resolve(
|
||||
) -> Any:
|
||||
"""
|
||||
Resolve a consolidated parameter following precedence rules:
|
||||
Instance > Config > Array > String > Bool > Default
|
||||
Instance > Config > Array > Dict > String > Bool > Default
|
||||
|
||||
Args:
|
||||
value: The parameter value
|
||||
@@ -91,19 +92,7 @@ def resolve(
|
||||
return value
|
||||
|
||||
# =========================================================================
|
||||
# 4. Dict -> convert to config
|
||||
# =========================================================================
|
||||
if isinstance(value, dict) and config_class:
|
||||
try:
|
||||
return config_class(**value)
|
||||
except TypeError as e:
|
||||
raise ValueError(
|
||||
f"Invalid {param_name} dict: {e}. "
|
||||
f"Valid fields: {_get_config_fields(config_class)}"
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# 5. Array handling (before string to respect precedence)
|
||||
# 4. Array handling (BEFORE Dict per AC-1 precedence)
|
||||
# =========================================================================
|
||||
if isinstance(value, (list, tuple)):
|
||||
return _resolve_array(
|
||||
@@ -115,6 +104,35 @@ def resolve(
|
||||
array_mode=array_mode,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# 5. Dict -> convert to config (STRICT validation)
|
||||
# =========================================================================
|
||||
if isinstance(value, dict):
|
||||
if config_class:
|
||||
# Validate keys BEFORE construction for helpful errors
|
||||
unknown_keys = _validate_dict_keys(value, config_class)
|
||||
if unknown_keys:
|
||||
valid_fields = _get_config_fields(config_class)
|
||||
example = _get_example_dict(config_class)
|
||||
raise TypeError(
|
||||
f"Unknown keys for {param_name}: {unknown_keys}. "
|
||||
f"Valid keys: {valid_fields}. "
|
||||
f"Example: {param_name}={{{example}}}"
|
||||
)
|
||||
try:
|
||||
return config_class(**value)
|
||||
except TypeError as e:
|
||||
raise TypeError(
|
||||
f"Invalid {param_name} dict: {e}. "
|
||||
f"Valid fields: {_get_config_fields(config_class)}"
|
||||
)
|
||||
else:
|
||||
# No config class - dict input not supported
|
||||
raise TypeError(
|
||||
f"Dict input not supported for {param_name} (no config class defined). "
|
||||
f"Use a supported type: bool, str, or instance."
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# 6. String handling
|
||||
# =========================================================================
|
||||
@@ -203,7 +221,7 @@ def _resolve_array(
|
||||
return config_class(sources=sources, **config_override)
|
||||
return sources
|
||||
|
||||
# Single or list mode
|
||||
# Single or list mode - SINGLE item only, multiple items raise error
|
||||
if array_mode == ArrayMode.SINGLE_OR_LIST:
|
||||
if len(value) == 1:
|
||||
single_value = value[0]
|
||||
@@ -215,10 +233,17 @@ def _resolve_array(
|
||||
# Check if single item is a preset
|
||||
if isinstance(single_value, str) and presets and single_value in presets:
|
||||
return _apply_preset(single_value, presets, config_class)
|
||||
# Multiple items - treat as sources
|
||||
if config_class:
|
||||
return config_class(sources=list(value))
|
||||
return list(value)
|
||||
# Single non-preset string - try as URL or return as-is
|
||||
if isinstance(single_value, str):
|
||||
if config_class:
|
||||
return config_class()
|
||||
return single_value
|
||||
# Multiple items - ERROR (not allowed for SINGLE_OR_LIST mode)
|
||||
raise TypeError(
|
||||
f"Multiple values not allowed for {param_name}. "
|
||||
f"Use a single value: {param_name}='preset' or {param_name}='url://...' "
|
||||
f"Got {len(value)} items: {value}"
|
||||
)
|
||||
|
||||
# Preset override mode - [preset, {overrides}] or [preset]
|
||||
if array_mode == ArrayMode.PRESET_OVERRIDE:
|
||||
@@ -384,6 +409,81 @@ def _get_config_fields(config_class: Type) -> str:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _get_valid_keys(config_class: Type) -> Optional[Set[str]]:
|
||||
"""
|
||||
Get valid keys from a config class.
|
||||
|
||||
Supports:
|
||||
- Dataclasses (via __dataclass_fields__)
|
||||
- Regular classes (via __init__ signature)
|
||||
- Classes with __annotations__
|
||||
|
||||
Returns:
|
||||
Set of valid key names, or None if cannot determine.
|
||||
"""
|
||||
# 1. Dataclass fields (most common)
|
||||
if hasattr(config_class, '__dataclass_fields__'):
|
||||
return set(config_class.__dataclass_fields__.keys())
|
||||
|
||||
# 2. __init__ signature parameters
|
||||
try:
|
||||
sig = inspect.signature(config_class.__init__)
|
||||
params = set(sig.parameters.keys()) - {'self'}
|
||||
if params:
|
||||
return params
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 3. Class annotations
|
||||
if hasattr(config_class, '__annotations__'):
|
||||
return set(config_class.__annotations__.keys())
|
||||
|
||||
# Cannot determine valid keys
|
||||
return None
|
||||
|
||||
|
||||
def _validate_dict_keys(value: dict, config_class: Type) -> list:
|
||||
"""
|
||||
Validate dict keys against config class fields.
|
||||
Returns list of unknown keys (empty if all valid).
|
||||
O(n) where n = number of keys in dict (typically small).
|
||||
|
||||
Works for both dataclasses and regular classes.
|
||||
"""
|
||||
valid_keys = _get_valid_keys(config_class)
|
||||
if valid_keys is None:
|
||||
return [] # Cannot validate, allow all keys
|
||||
|
||||
provided_keys = set(value.keys())
|
||||
unknown = provided_keys - valid_keys
|
||||
return list(unknown)
|
||||
|
||||
|
||||
def _get_example_dict(config_class: Type) -> str:
|
||||
"""
|
||||
Generate a minimal example dict snippet for error messages.
|
||||
Shows first 2-3 fields with placeholder values.
|
||||
"""
|
||||
if not hasattr(config_class, '__dataclass_fields__'):
|
||||
return "..."
|
||||
|
||||
fields = list(config_class.__dataclass_fields__.items())[:3]
|
||||
examples = []
|
||||
for name, field in fields:
|
||||
# Generate appropriate placeholder based on type hint
|
||||
field_type = field.type if hasattr(field, 'type') else None
|
||||
if field_type is bool or str(field_type) == 'bool':
|
||||
examples.append(f"'{name}': True")
|
||||
elif field_type is int or str(field_type) == 'int':
|
||||
examples.append(f"'{name}': 1")
|
||||
elif field_type is str or 'str' in str(field_type):
|
||||
examples.append(f"'{name}': '...'")
|
||||
else:
|
||||
examples.append(f"'{name}': ...")
|
||||
|
||||
return ", ".join(examples)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Convenience Functions for Specific Parameters
|
||||
# =============================================================================
|
||||
|
||||
@@ -41,6 +41,37 @@ MEMORY_URL_SCHEMES: Dict[str, str] = {
|
||||
# =============================================================================
|
||||
|
||||
OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
|
||||
# Silent preset - DEFAULT for SDK
|
||||
# Zero overhead, no callbacks, no I/O on hot path
|
||||
# Fastest performance for programmatic use
|
||||
"silent": {
|
||||
"verbose": False,
|
||||
"markdown": False,
|
||||
"stream": False,
|
||||
"metrics": False,
|
||||
"reasoning_steps": False,
|
||||
"actions_trace": False,
|
||||
},
|
||||
# Actions preset - opt-in observability
|
||||
# Shows action trace (tool calls, agent lifecycle) + final output
|
||||
# Registers callbacks, outputs to stderr
|
||||
"actions": {
|
||||
"verbose": False,
|
||||
"markdown": False,
|
||||
"stream": False,
|
||||
"metrics": False,
|
||||
"reasoning_steps": False,
|
||||
"actions_trace": True, # Special flag for action trace mode
|
||||
},
|
||||
# Plain preset - final output only, no action trace
|
||||
"plain": {
|
||||
"verbose": False,
|
||||
"markdown": False,
|
||||
"stream": False,
|
||||
"metrics": False,
|
||||
"reasoning_steps": False,
|
||||
"actions_trace": False,
|
||||
},
|
||||
"minimal": {
|
||||
"verbose": False,
|
||||
"markdown": False,
|
||||
@@ -69,15 +100,31 @@ OUTPUT_PRESETS: Dict[str, Dict[str, Any]] = {
|
||||
"metrics": True,
|
||||
"reasoning_steps": True,
|
||||
},
|
||||
"silent": {
|
||||
# Streaming preset - enables streaming by default
|
||||
"stream": {
|
||||
"verbose": True,
|
||||
"markdown": True,
|
||||
"stream": True,
|
||||
"metrics": False,
|
||||
"reasoning_steps": False,
|
||||
},
|
||||
# JSON preset - JSONL output for piping
|
||||
"json": {
|
||||
"verbose": False,
|
||||
"markdown": False,
|
||||
"stream": False,
|
||||
"metrics": False,
|
||||
"reasoning_steps": False,
|
||||
"actions_trace": True,
|
||||
"json_output": True, # Output as JSONL
|
||||
},
|
||||
}
|
||||
|
||||
# Default output mode - can be overridden by PRAISONAI_OUTPUT env var
|
||||
# "silent" = zero overhead, fastest performance (DEFAULT)
|
||||
# "actions" = tool call trace + final output (opt-in observability)
|
||||
DEFAULT_OUTPUT_MODE = "silent"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Execution Presets
|
||||
|
||||
@@ -157,6 +157,16 @@ __all__ = [
|
||||
"FastContextResult",
|
||||
"FileMatch",
|
||||
"LineRange",
|
||||
# Artifacts (Dynamic Context Discovery)
|
||||
"ArtifactRef",
|
||||
"ArtifactMetadata",
|
||||
"GrepMatch",
|
||||
"QueueConfig",
|
||||
"ArtifactStoreProtocol",
|
||||
"HistoryStoreProtocol",
|
||||
"TerminalLoggerProtocol",
|
||||
"compute_checksum",
|
||||
"generate_summary",
|
||||
]
|
||||
|
||||
|
||||
@@ -252,4 +262,11 @@ def __getattr__(name: str):
|
||||
from praisonaiagents.context.fast.result import LineRange
|
||||
return LineRange
|
||||
|
||||
# Artifacts (Dynamic Context Discovery)
|
||||
if name in ("ArtifactRef", "ArtifactMetadata", "GrepMatch", "QueueConfig",
|
||||
"ArtifactStoreProtocol", "HistoryStoreProtocol", "TerminalLoggerProtocol",
|
||||
"compute_checksum", "generate_summary"):
|
||||
from . import artifacts
|
||||
return getattr(artifacts, name)
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,580 @@
|
||||
"""
|
||||
Artifact Types and Protocols for Dynamic Context Discovery.
|
||||
|
||||
This module provides lightweight types and protocols for artifact-based
|
||||
context management. Implementations live in the wrapper (praisonai).
|
||||
|
||||
Zero Performance Impact:
|
||||
- Pure dataclasses and protocols
|
||||
- No heavy imports
|
||||
- No I/O at import time
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.context.artifacts import ArtifactRef, ArtifactStoreProtocol
|
||||
|
||||
# Create a reference to a stored artifact
|
||||
ref = ArtifactRef(
|
||||
path="/path/to/artifact.json",
|
||||
summary="API response with 50,000 records",
|
||||
size_bytes=1_200_000,
|
||||
mime_type="application/json",
|
||||
)
|
||||
|
||||
# Implement the protocol in wrapper
|
||||
class FileSystemArtifactStore:
|
||||
def store(self, content, metadata): ...
|
||||
def load(self, ref): ...
|
||||
"""
|
||||
|
||||
import time
|
||||
import hashlib
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Protocol, Union, runtime_checkable
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArtifactRef:
|
||||
"""
|
||||
Reference to a stored artifact.
|
||||
|
||||
Used to replace large tool outputs in context with a lightweight pointer.
|
||||
The agent can then use tail/grep/load operations to access content on demand.
|
||||
|
||||
Attributes:
|
||||
path: Absolute path to the artifact file
|
||||
summary: Brief description or first ~200 chars of content
|
||||
size_bytes: Original content size in bytes
|
||||
mime_type: Content type (e.g., "application/json", "text/plain")
|
||||
checksum: SHA256 hash for integrity verification
|
||||
created_at: Unix timestamp when artifact was created
|
||||
artifact_id: Unique identifier for the artifact
|
||||
agent_id: ID of the agent that created this artifact
|
||||
run_id: ID of the run/session this artifact belongs to
|
||||
tool_name: Name of the tool that produced this output (if applicable)
|
||||
turn_id: Conversation turn number when artifact was created
|
||||
"""
|
||||
path: str
|
||||
summary: str
|
||||
size_bytes: int
|
||||
mime_type: str = "application/octet-stream"
|
||||
checksum: str = ""
|
||||
created_at: float = field(default_factory=time.time)
|
||||
artifact_id: str = ""
|
||||
agent_id: str = ""
|
||||
run_id: str = ""
|
||||
tool_name: Optional[str] = None
|
||||
turn_id: int = 0
|
||||
|
||||
def to_inline(self) -> str:
|
||||
"""
|
||||
Generate inline representation for context.
|
||||
|
||||
Returns a compact string that can be included in the conversation
|
||||
context, allowing the agent to reference the artifact.
|
||||
"""
|
||||
size_str = self._format_size(self.size_bytes)
|
||||
tool_info = f" from {self.tool_name}" if self.tool_name else ""
|
||||
return f"[Artifact{tool_info}: {self.path} ({size_str}) - {self.summary}]"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
"path": self.path,
|
||||
"summary": self.summary,
|
||||
"size_bytes": self.size_bytes,
|
||||
"mime_type": self.mime_type,
|
||||
"checksum": self.checksum,
|
||||
"created_at": self.created_at,
|
||||
"artifact_id": self.artifact_id,
|
||||
"agent_id": self.agent_id,
|
||||
"run_id": self.run_id,
|
||||
"tool_name": self.tool_name,
|
||||
"turn_id": self.turn_id,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ArtifactRef":
|
||||
"""Create from dictionary."""
|
||||
return cls(
|
||||
path=data.get("path", ""),
|
||||
summary=data.get("summary", ""),
|
||||
size_bytes=data.get("size_bytes", 0),
|
||||
mime_type=data.get("mime_type", "application/octet-stream"),
|
||||
checksum=data.get("checksum", ""),
|
||||
created_at=data.get("created_at", time.time()),
|
||||
artifact_id=data.get("artifact_id", ""),
|
||||
agent_id=data.get("agent_id", ""),
|
||||
run_id=data.get("run_id", ""),
|
||||
tool_name=data.get("tool_name"),
|
||||
turn_id=data.get("turn_id", 0),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size_bytes: int) -> str:
|
||||
"""Format size in human-readable form."""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.1f} KB"
|
||||
elif size_bytes < 1024 * 1024 * 1024:
|
||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArtifactMetadata:
|
||||
"""
|
||||
Metadata for artifact creation and tracking.
|
||||
|
||||
Passed to ArtifactStore.store() to provide context about the artifact.
|
||||
"""
|
||||
agent_id: str = ""
|
||||
run_id: str = ""
|
||||
tool_name: Optional[str] = None
|
||||
turn_id: int = 0
|
||||
content_type: str = "auto" # auto, json, text, binary
|
||||
tags: List[str] = field(default_factory=list)
|
||||
extra: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"agent_id": self.agent_id,
|
||||
"run_id": self.run_id,
|
||||
"tool_name": self.tool_name,
|
||||
"turn_id": self.turn_id,
|
||||
"content_type": self.content_type,
|
||||
"tags": self.tags,
|
||||
"extra": self.extra,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrepMatch:
|
||||
"""A single match from grep operation."""
|
||||
line_number: int
|
||||
line_content: str
|
||||
context_before: List[str] = field(default_factory=list)
|
||||
context_after: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"line_number": self.line_number,
|
||||
"line_content": self.line_content,
|
||||
"context_before": self.context_before,
|
||||
"context_after": self.context_after,
|
||||
}
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ArtifactStoreProtocol(Protocol):
|
||||
"""
|
||||
Protocol for artifact storage backends.
|
||||
|
||||
Implementations should handle:
|
||||
- Storing content to persistent storage
|
||||
- Loading content back
|
||||
- Efficient tail/grep operations for large files
|
||||
- Checksum verification
|
||||
- Secret redaction (if enabled)
|
||||
|
||||
Example implementation in wrapper:
|
||||
class FileSystemArtifactStore:
|
||||
def store(self, content, metadata):
|
||||
# Write to ~/.praison/runs/{run_id}/artifacts/
|
||||
...
|
||||
"""
|
||||
|
||||
def store(
|
||||
self,
|
||||
content: Any,
|
||||
metadata: ArtifactMetadata,
|
||||
) -> ArtifactRef:
|
||||
"""
|
||||
Store content as an artifact.
|
||||
|
||||
Args:
|
||||
content: The content to store (will be serialized)
|
||||
metadata: Metadata about the artifact
|
||||
|
||||
Returns:
|
||||
ArtifactRef pointing to the stored artifact
|
||||
"""
|
||||
...
|
||||
|
||||
def load(self, ref: ArtifactRef) -> Any:
|
||||
"""
|
||||
Load full content from an artifact.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
|
||||
Returns:
|
||||
The deserialized content
|
||||
"""
|
||||
...
|
||||
|
||||
def tail(self, ref: ArtifactRef, lines: int = 50) -> str:
|
||||
"""
|
||||
Get the last N lines of an artifact.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
lines: Number of lines to return
|
||||
|
||||
Returns:
|
||||
String containing the last N lines
|
||||
"""
|
||||
...
|
||||
|
||||
def head(self, ref: ArtifactRef, lines: int = 50) -> str:
|
||||
"""
|
||||
Get the first N lines of an artifact.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
lines: Number of lines to return
|
||||
|
||||
Returns:
|
||||
String containing the first N lines
|
||||
"""
|
||||
...
|
||||
|
||||
def grep(
|
||||
self,
|
||||
ref: ArtifactRef,
|
||||
pattern: str,
|
||||
context_lines: int = 2,
|
||||
max_matches: int = 50,
|
||||
) -> List[GrepMatch]:
|
||||
"""
|
||||
Search for pattern in artifact content.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
pattern: Regex pattern to search for
|
||||
context_lines: Number of context lines before/after match
|
||||
max_matches: Maximum number of matches to return
|
||||
|
||||
Returns:
|
||||
List of GrepMatch objects
|
||||
"""
|
||||
...
|
||||
|
||||
def chunk(
|
||||
self,
|
||||
ref: ArtifactRef,
|
||||
start_line: int = 1,
|
||||
end_line: Optional[int] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Get a chunk of lines from an artifact.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
start_line: Starting line number (1-indexed)
|
||||
end_line: Ending line number (inclusive), None for end of file
|
||||
|
||||
Returns:
|
||||
String containing the requested lines
|
||||
"""
|
||||
...
|
||||
|
||||
def delete(self, ref: ArtifactRef) -> bool:
|
||||
"""
|
||||
Delete an artifact.
|
||||
|
||||
Args:
|
||||
ref: Reference to the artifact
|
||||
|
||||
Returns:
|
||||
True if deleted successfully
|
||||
"""
|
||||
...
|
||||
|
||||
def list_artifacts(
|
||||
self,
|
||||
run_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
tool_name: Optional[str] = None,
|
||||
) -> List[ArtifactRef]:
|
||||
"""
|
||||
List artifacts matching filters.
|
||||
|
||||
Args:
|
||||
run_id: Filter by run ID
|
||||
agent_id: Filter by agent ID
|
||||
tool_name: Filter by tool name
|
||||
|
||||
Returns:
|
||||
List of matching ArtifactRef objects
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class HistoryStoreProtocol(Protocol):
|
||||
"""
|
||||
Protocol for conversation history storage.
|
||||
|
||||
Enables loss recovery during summarization by persisting
|
||||
full conversation history to artifacts.
|
||||
"""
|
||||
|
||||
def append(
|
||||
self,
|
||||
message: Dict[str, Any],
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
) -> None:
|
||||
"""
|
||||
Append a message to history.
|
||||
|
||||
Args:
|
||||
message: The message dict (role, content, etc.)
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
"""
|
||||
...
|
||||
|
||||
def get_ref(self, agent_id: str, run_id: str) -> Optional[ArtifactRef]:
|
||||
"""
|
||||
Get artifact reference for history file.
|
||||
|
||||
Args:
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
|
||||
Returns:
|
||||
ArtifactRef if history exists, None otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
max_results: int = 20,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search history for matching messages.
|
||||
|
||||
Args:
|
||||
query: Search query (supports regex)
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
max_results: Maximum results to return
|
||||
|
||||
Returns:
|
||||
List of matching message dicts
|
||||
"""
|
||||
...
|
||||
|
||||
def get_messages(
|
||||
self,
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
start_turn: int = 0,
|
||||
end_turn: Optional[int] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get messages from history.
|
||||
|
||||
Args:
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
start_turn: Starting turn number
|
||||
end_turn: Ending turn number (None for all)
|
||||
|
||||
Returns:
|
||||
List of message dicts
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TerminalLoggerProtocol(Protocol):
|
||||
"""
|
||||
Protocol for terminal session logging.
|
||||
|
||||
Captures shell command outputs to artifacts for later retrieval.
|
||||
"""
|
||||
|
||||
def log_command(
|
||||
self,
|
||||
command: str,
|
||||
output: str,
|
||||
exit_code: int,
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
) -> ArtifactRef:
|
||||
"""
|
||||
Log a command execution.
|
||||
|
||||
Args:
|
||||
command: The command that was executed
|
||||
output: Combined stdout/stderr output
|
||||
exit_code: Command exit code
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
|
||||
Returns:
|
||||
ArtifactRef to the logged output
|
||||
"""
|
||||
...
|
||||
|
||||
def get_session_ref(self, agent_id: str, run_id: str) -> Optional[ArtifactRef]:
|
||||
"""
|
||||
Get artifact reference for terminal session log.
|
||||
|
||||
Args:
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
|
||||
Returns:
|
||||
ArtifactRef if session exists, None otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
def tail_session(
|
||||
self,
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
lines: int = 100,
|
||||
) -> str:
|
||||
"""
|
||||
Get last N lines from terminal session.
|
||||
|
||||
Args:
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
lines: Number of lines to return
|
||||
|
||||
Returns:
|
||||
String containing the last N lines
|
||||
"""
|
||||
...
|
||||
|
||||
def grep_session(
|
||||
self,
|
||||
pattern: str,
|
||||
agent_id: str,
|
||||
run_id: str,
|
||||
max_matches: int = 50,
|
||||
) -> List[GrepMatch]:
|
||||
"""
|
||||
Search terminal session for pattern.
|
||||
|
||||
Args:
|
||||
pattern: Regex pattern to search for
|
||||
agent_id: ID of the agent
|
||||
run_id: ID of the run/session
|
||||
max_matches: Maximum matches to return
|
||||
|
||||
Returns:
|
||||
List of GrepMatch objects
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueueConfig:
|
||||
"""
|
||||
Configuration for tool output queuing.
|
||||
|
||||
Controls when and how large tool outputs are queued to artifacts
|
||||
instead of being included inline in the context.
|
||||
"""
|
||||
enabled: bool = True
|
||||
inline_max_bytes: int = 32 * 1024 # 32KB default
|
||||
redact_secrets: bool = True
|
||||
summary_max_chars: int = 200
|
||||
checksum_algorithm: str = "sha256"
|
||||
|
||||
# Secret patterns to redact (regex)
|
||||
secret_patterns: List[str] = field(default_factory=lambda: [
|
||||
r'(?i)(api[_-]?key|apikey)["\']?\s*[:=]\s*["\']?[\w\-]+',
|
||||
r'(?i)(secret|password|passwd|pwd)["\']?\s*[:=]\s*["\']?[\w\-]+',
|
||||
r'(?i)(token|bearer)["\']?\s*[:=]\s*["\']?[\w\-]+',
|
||||
r'(?i)(auth|authorization)["\']?\s*[:=]\s*["\']?[\w\-]+',
|
||||
r'sk-[a-zA-Z0-9]{20,}', # OpenAI keys
|
||||
r'ghp_[a-zA-Z0-9]{36}', # GitHub tokens
|
||||
r'gho_[a-zA-Z0-9]{36}', # GitHub OAuth
|
||||
r'xox[baprs]-[a-zA-Z0-9\-]+', # Slack tokens
|
||||
])
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"enabled": self.enabled,
|
||||
"inline_max_bytes": self.inline_max_bytes,
|
||||
"redact_secrets": self.redact_secrets,
|
||||
"summary_max_chars": self.summary_max_chars,
|
||||
"checksum_algorithm": self.checksum_algorithm,
|
||||
"secret_patterns": self.secret_patterns,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "QueueConfig":
|
||||
return cls(
|
||||
enabled=data.get("enabled", True),
|
||||
inline_max_bytes=data.get("inline_max_bytes", 32 * 1024),
|
||||
redact_secrets=data.get("redact_secrets", True),
|
||||
summary_max_chars=data.get("summary_max_chars", 200),
|
||||
checksum_algorithm=data.get("checksum_algorithm", "sha256"),
|
||||
secret_patterns=data.get("secret_patterns", cls().secret_patterns),
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def compute_checksum(content: Union[str, bytes], algorithm: str = "sha256") -> str:
|
||||
"""
|
||||
Compute checksum of content.
|
||||
|
||||
Args:
|
||||
content: String or bytes to hash
|
||||
algorithm: Hash algorithm (sha256, md5, etc.)
|
||||
|
||||
Returns:
|
||||
Hex digest of the hash
|
||||
"""
|
||||
if isinstance(content, str):
|
||||
content = content.encode("utf-8")
|
||||
|
||||
hasher = hashlib.new(algorithm)
|
||||
hasher.update(content)
|
||||
return hasher.hexdigest()
|
||||
|
||||
|
||||
def generate_summary(content: Any, max_chars: int = 200) -> str:
|
||||
"""
|
||||
Generate a brief summary of content.
|
||||
|
||||
Args:
|
||||
content: Content to summarize
|
||||
max_chars: Maximum characters in summary
|
||||
|
||||
Returns:
|
||||
Brief summary string
|
||||
"""
|
||||
if isinstance(content, (dict, list)):
|
||||
# For structured data, describe the structure
|
||||
if isinstance(content, dict):
|
||||
keys = list(content.keys())[:5]
|
||||
key_str = ", ".join(str(k) for k in keys)
|
||||
if len(content) > 5:
|
||||
key_str += f", ... ({len(content)} total keys)"
|
||||
return f"Dict with keys: {key_str}"[:max_chars]
|
||||
else:
|
||||
if len(content) > 0:
|
||||
first_type = type(content[0]).__name__
|
||||
return f"List of {len(content)} {first_type} items"[:max_chars]
|
||||
return "Empty list"
|
||||
|
||||
# For strings, take first N chars
|
||||
text = str(content)
|
||||
if len(text) <= max_chars:
|
||||
return text
|
||||
return text[:max_chars - 3] + "..."
|
||||
@@ -68,8 +68,8 @@ class LLM:
|
||||
MODEL_WINDOWS = {
|
||||
# OpenAI
|
||||
"gpt-4": 6144, # 8,192 actual
|
||||
"gpt-5-nano": 96000, # 128,000 actual
|
||||
"gpt-5-nano": 96000, # 128,000 actual
|
||||
"gpt-4o-mini": 96000, # 128,000 actual
|
||||
"gpt-4o-mini": 96000, # 128,000 actual
|
||||
"gpt-4-turbo": 96000, # 128,000 actual
|
||||
"o1-preview": 96000, # 128,000 actual
|
||||
"o1-mini": 96000, # 128,000 actual
|
||||
@@ -1430,6 +1430,8 @@ Now provide your final answer using this result."""
|
||||
task_id: Optional[str] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
stream: bool = True,
|
||||
stream_callback: Optional[Callable] = None,
|
||||
emit_events: bool = False,
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""Enhanced get_response with all OpenAI-like features"""
|
||||
@@ -1489,6 +1491,19 @@ Now provide your final answer using this result."""
|
||||
# Disable litellm debug messages
|
||||
litellm.set_verbose = False
|
||||
|
||||
# Setup StreamEvent emission if enabled
|
||||
_emit = emit_events and stream_callback is not None
|
||||
if _emit:
|
||||
from ..streaming.events import StreamEvent, StreamEventType
|
||||
_first_token_emitted = False
|
||||
_last_content_time = None
|
||||
_request_start_perf = time.perf_counter()
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.REQUEST_START,
|
||||
timestamp=_request_start_perf,
|
||||
metadata={"model": self.model, "provider": "litellm"}
|
||||
))
|
||||
|
||||
# Format tools if provided
|
||||
formatted_tools = self._format_tools_for_litellm(tools)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class ModelRouter:
|
||||
DEFAULT_MODELS = [
|
||||
# Lightweight/cheap models for simple tasks
|
||||
ModelProfile(
|
||||
name="gpt-5-nano",
|
||||
name="gpt-4o-mini",
|
||||
provider="openai",
|
||||
complexity_range=(TaskComplexity.SIMPLE, TaskComplexity.MODERATE),
|
||||
cost_per_1k_tokens=0.00075, # Average of $0.00015 input, $0.0006 output
|
||||
@@ -80,7 +80,7 @@ class ModelRouter:
|
||||
|
||||
# Mid-tier models for moderate complexity
|
||||
ModelProfile(
|
||||
name="gpt-5-nano",
|
||||
name="gpt-4o-mini",
|
||||
provider="openai",
|
||||
complexity_range=(TaskComplexity.MODERATE, TaskComplexity.COMPLEX),
|
||||
cost_per_1k_tokens=0.0075, # Average of $0.0025 input, $0.01 output
|
||||
@@ -145,7 +145,7 @@ class ModelRouter:
|
||||
preferred_providers: List of preferred providers in order
|
||||
"""
|
||||
self.models = models or self.DEFAULT_MODELS
|
||||
self.default_model = default_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-5-nano')
|
||||
self.default_model = default_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
self.cost_threshold = cost_threshold
|
||||
self.preferred_providers = preferred_providers or []
|
||||
|
||||
|
||||
@@ -582,6 +582,8 @@ class OpenAIClient:
|
||||
console: Optional[Console] = None,
|
||||
display_fn: Optional[Callable] = None,
|
||||
reasoning_steps: bool = False,
|
||||
stream_callback: Optional[Callable] = None,
|
||||
emit_events: bool = False,
|
||||
**kwargs
|
||||
) -> Optional[ChatCompletion]:
|
||||
"""
|
||||
@@ -596,11 +598,18 @@ class OpenAIClient:
|
||||
console: Console for output
|
||||
display_fn: Display function for live updates
|
||||
reasoning_steps: Whether to show reasoning steps
|
||||
stream_callback: Optional callback for StreamEvent emission
|
||||
emit_events: Whether to emit StreamEvents (requires stream_callback)
|
||||
**kwargs: Additional parameters for the API
|
||||
|
||||
Returns:
|
||||
ChatCompletion object or None if error
|
||||
"""
|
||||
# Lazy import StreamEvent types only when needed
|
||||
_emit = emit_events and stream_callback is not None
|
||||
if _emit:
|
||||
from ..streaming.events import StreamEvent, StreamEventType
|
||||
|
||||
try:
|
||||
# Default start time and console if not provided
|
||||
if start_time is None:
|
||||
@@ -608,6 +617,15 @@ class OpenAIClient:
|
||||
if console is None:
|
||||
console = self.console
|
||||
|
||||
# Emit REQUEST_START event
|
||||
request_start_perf = time.perf_counter()
|
||||
if _emit:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.REQUEST_START,
|
||||
timestamp=request_start_perf,
|
||||
metadata={"model": model, "message_count": len(messages)}
|
||||
))
|
||||
|
||||
# Create the response stream
|
||||
response_stream = self.sync_client.chat.completions.create(
|
||||
model=model,
|
||||
@@ -615,29 +633,81 @@ class OpenAIClient:
|
||||
temperature=temperature,
|
||||
tools=tools if tools else None,
|
||||
stream=True,
|
||||
stream_options={"include_usage": True} if _emit else None,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# Emit HEADERS_RECEIVED event (stream object created means headers received)
|
||||
if _emit:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.HEADERS_RECEIVED,
|
||||
timestamp=time.perf_counter()
|
||||
))
|
||||
|
||||
full_response_text = ""
|
||||
reasoning_content = ""
|
||||
chunks = []
|
||||
first_token_emitted = False
|
||||
last_content_time = None
|
||||
ttft_logged = False
|
||||
|
||||
# If display function provided, use Live display
|
||||
if display_fn:
|
||||
with Live(
|
||||
display_fn("", start_time),
|
||||
console=console,
|
||||
refresh_per_second=4,
|
||||
refresh_per_second=10, # Increased for more responsive streaming
|
||||
transient=True,
|
||||
vertical_overflow="ellipsis",
|
||||
auto_refresh=True
|
||||
) as live:
|
||||
for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
if chunk.choices[0].delta.content:
|
||||
full_response_text += chunk.choices[0].delta.content
|
||||
|
||||
# Check for content delta
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
content = chunk.choices[0].delta.content
|
||||
full_response_text += content
|
||||
last_content_time = time.perf_counter()
|
||||
|
||||
# Log TTFT on first token
|
||||
if not ttft_logged:
|
||||
ttft_ms = (last_content_time - request_start_perf) * 1000
|
||||
logging.debug(f"TTFT (Time To First Token): {ttft_ms:.0f}ms")
|
||||
ttft_logged = True
|
||||
|
||||
# Emit FIRST_TOKEN on first content
|
||||
if _emit and not first_token_emitted:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.FIRST_TOKEN,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
first_token_emitted = True
|
||||
elif _emit:
|
||||
# Emit DELTA_TEXT for subsequent tokens
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
|
||||
live.update(display_fn(full_response_text, start_time))
|
||||
|
||||
# Handle tool calls streaming
|
||||
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
|
||||
for tc in chunk.choices[0].delta.tool_calls:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.DELTA_TOOL_CALL,
|
||||
timestamp=time.perf_counter(),
|
||||
tool_call={
|
||||
"index": tc.index,
|
||||
"id": getattr(tc, 'id', None),
|
||||
"name": getattr(tc.function, 'name', None) if tc.function else None,
|
||||
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
|
||||
}
|
||||
))
|
||||
|
||||
# Update live display with reasoning content if enabled
|
||||
if reasoning_steps and hasattr(chunk.choices[0].delta, "reasoning_content"):
|
||||
rc = chunk.choices[0].delta.reasoning_content
|
||||
@@ -648,9 +718,60 @@ class OpenAIClient:
|
||||
# Clear the last generating display with a blank line
|
||||
console.print()
|
||||
else:
|
||||
# Just collect chunks without display
|
||||
# Just collect chunks without display, but still emit events
|
||||
for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
content = chunk.choices[0].delta.content
|
||||
last_content_time = time.perf_counter()
|
||||
|
||||
# Log TTFT on first token
|
||||
if not ttft_logged:
|
||||
ttft_ms = (last_content_time - request_start_perf) * 1000
|
||||
logging.debug(f"TTFT (Time To First Token): {ttft_ms:.0f}ms")
|
||||
ttft_logged = True
|
||||
|
||||
if _emit and not first_token_emitted:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.FIRST_TOKEN,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
first_token_emitted = True
|
||||
else:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
|
||||
# Handle tool calls streaming
|
||||
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
|
||||
for tc in chunk.choices[0].delta.tool_calls:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.DELTA_TOOL_CALL,
|
||||
timestamp=time.perf_counter(),
|
||||
tool_call={
|
||||
"index": tc.index,
|
||||
"id": getattr(tc, 'id', None),
|
||||
"name": getattr(tc.function, 'name', None) if tc.function else None,
|
||||
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
|
||||
}
|
||||
))
|
||||
|
||||
# Emit LAST_TOKEN and STREAM_END events
|
||||
if _emit:
|
||||
if last_content_time:
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.LAST_TOKEN,
|
||||
timestamp=last_content_time
|
||||
))
|
||||
stream_callback(StreamEvent(
|
||||
type=StreamEventType.STREAM_END,
|
||||
timestamp=time.perf_counter(),
|
||||
metadata={"chunk_count": len(chunks)}
|
||||
))
|
||||
|
||||
final_response = process_stream_chunks(chunks)
|
||||
return final_response
|
||||
@@ -669,6 +790,8 @@ class OpenAIClient:
|
||||
console: Optional[Console] = None,
|
||||
display_fn: Optional[Callable] = None,
|
||||
reasoning_steps: bool = False,
|
||||
stream_callback: Optional[Callable] = None,
|
||||
emit_events: bool = False,
|
||||
**kwargs
|
||||
) -> Optional[ChatCompletion]:
|
||||
"""
|
||||
@@ -683,11 +806,18 @@ class OpenAIClient:
|
||||
console: Console for output
|
||||
display_fn: Display function for live updates
|
||||
reasoning_steps: Whether to show reasoning steps
|
||||
stream_callback: Optional callback for StreamEvent emission (can be sync or async)
|
||||
emit_events: Whether to emit StreamEvents (requires stream_callback)
|
||||
**kwargs: Additional parameters for the API
|
||||
|
||||
Returns:
|
||||
ChatCompletion object or None if error
|
||||
"""
|
||||
# Lazy import StreamEvent types only when needed
|
||||
_emit = emit_events and stream_callback is not None
|
||||
if _emit:
|
||||
from ..streaming.events import StreamEvent, StreamEventType
|
||||
|
||||
try:
|
||||
# Default start time and console if not provided
|
||||
if start_time is None:
|
||||
@@ -695,6 +825,19 @@ class OpenAIClient:
|
||||
if console is None:
|
||||
console = self.console
|
||||
|
||||
# Emit REQUEST_START event
|
||||
request_start_perf = time.perf_counter()
|
||||
if _emit:
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.REQUEST_START,
|
||||
timestamp=request_start_perf,
|
||||
metadata={"model": model, "message_count": len(messages)}
|
||||
)
|
||||
if asyncio.iscoroutinefunction(stream_callback):
|
||||
await stream_callback(event)
|
||||
else:
|
||||
stream_callback(event)
|
||||
|
||||
# Create the response stream
|
||||
response_stream = await self.async_client.chat.completions.create(
|
||||
model=model,
|
||||
@@ -702,29 +845,81 @@ class OpenAIClient:
|
||||
temperature=temperature,
|
||||
tools=tools if tools else None,
|
||||
stream=True,
|
||||
stream_options={"include_usage": True} if _emit else None,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# Emit HEADERS_RECEIVED event
|
||||
if _emit:
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.HEADERS_RECEIVED,
|
||||
timestamp=time.perf_counter()
|
||||
)
|
||||
if asyncio.iscoroutinefunction(stream_callback):
|
||||
await stream_callback(event)
|
||||
else:
|
||||
stream_callback(event)
|
||||
|
||||
full_response_text = ""
|
||||
reasoning_content = ""
|
||||
chunks = []
|
||||
first_token_emitted = False
|
||||
last_content_time = None
|
||||
|
||||
async def emit_event(event: 'StreamEvent'):
|
||||
if asyncio.iscoroutinefunction(stream_callback):
|
||||
await stream_callback(event)
|
||||
else:
|
||||
stream_callback(event)
|
||||
|
||||
# If display function provided, use Live display
|
||||
if display_fn:
|
||||
with Live(
|
||||
display_fn("", start_time),
|
||||
console=console,
|
||||
refresh_per_second=4,
|
||||
refresh_per_second=10,
|
||||
transient=True,
|
||||
vertical_overflow="ellipsis",
|
||||
auto_refresh=True
|
||||
) as live:
|
||||
async for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
if chunk.choices[0].delta.content:
|
||||
full_response_text += chunk.choices[0].delta.content
|
||||
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
content = chunk.choices[0].delta.content
|
||||
full_response_text += content
|
||||
last_content_time = time.perf_counter()
|
||||
|
||||
if _emit and not first_token_emitted:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.FIRST_TOKEN,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
first_token_emitted = True
|
||||
elif _emit:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
|
||||
live.update(display_fn(full_response_text, start_time))
|
||||
|
||||
# Handle tool calls streaming
|
||||
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
|
||||
for tc in chunk.choices[0].delta.tool_calls:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.DELTA_TOOL_CALL,
|
||||
timestamp=time.perf_counter(),
|
||||
tool_call={
|
||||
"index": tc.index,
|
||||
"id": getattr(tc, 'id', None),
|
||||
"name": getattr(tc.function, 'name', None) if tc.function else None,
|
||||
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
|
||||
}
|
||||
))
|
||||
|
||||
# Update live display with reasoning content if enabled
|
||||
if reasoning_steps and hasattr(chunk.choices[0].delta, "reasoning_content"):
|
||||
rc = chunk.choices[0].delta.reasoning_content
|
||||
@@ -738,6 +933,50 @@ class OpenAIClient:
|
||||
# Just collect chunks without display
|
||||
async for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
|
||||
if _emit and chunk.choices and chunk.choices[0].delta.content:
|
||||
content = chunk.choices[0].delta.content
|
||||
last_content_time = time.perf_counter()
|
||||
|
||||
if not first_token_emitted:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.FIRST_TOKEN,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
first_token_emitted = True
|
||||
else:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
timestamp=last_content_time,
|
||||
content=content
|
||||
))
|
||||
|
||||
if _emit and chunk.choices and hasattr(chunk.choices[0].delta, 'tool_calls') and chunk.choices[0].delta.tool_calls:
|
||||
for tc in chunk.choices[0].delta.tool_calls:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.DELTA_TOOL_CALL,
|
||||
timestamp=time.perf_counter(),
|
||||
tool_call={
|
||||
"index": tc.index,
|
||||
"id": getattr(tc, 'id', None),
|
||||
"name": getattr(tc.function, 'name', None) if tc.function else None,
|
||||
"arguments": getattr(tc.function, 'arguments', None) if tc.function else None
|
||||
}
|
||||
))
|
||||
|
||||
# Emit LAST_TOKEN and STREAM_END events
|
||||
if _emit:
|
||||
if last_content_time:
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.LAST_TOKEN,
|
||||
timestamp=last_content_time
|
||||
))
|
||||
await emit_event(StreamEvent(
|
||||
type=StreamEventType.STREAM_END,
|
||||
timestamp=time.perf_counter(),
|
||||
metadata={"chunk_count": len(chunks)}
|
||||
))
|
||||
|
||||
final_response = process_stream_chunks(chunks)
|
||||
return final_response
|
||||
@@ -749,7 +988,7 @@ class OpenAIClient:
|
||||
def create_completion(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
stream: bool = False,
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
@@ -794,7 +1033,7 @@ class OpenAIClient:
|
||||
async def acreate_completion(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
stream: bool = False,
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
@@ -839,7 +1078,7 @@ class OpenAIClient:
|
||||
def chat_completion_with_tools(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
@@ -904,7 +1143,9 @@ class OpenAIClient:
|
||||
if display_fn and console:
|
||||
# When verbose (display_fn provided), use streaming for better UX
|
||||
try:
|
||||
with Live(display_fn("", start_time), console=console, refresh_per_second=4, transient=True) as live:
|
||||
request_start_perf = time.perf_counter()
|
||||
ttft_logged = False
|
||||
with Live(display_fn("", start_time), console=console, refresh_per_second=10, transient=True) as live:
|
||||
# Use streaming when display_fn is provided for progressive display
|
||||
response_stream = self.create_completion(
|
||||
messages=messages,
|
||||
@@ -922,6 +1163,11 @@ class OpenAIClient:
|
||||
for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
if chunk.choices[0].delta.content:
|
||||
# Log TTFT on first token
|
||||
if not ttft_logged:
|
||||
ttft_ms = (time.perf_counter() - request_start_perf) * 1000
|
||||
logging.debug(f"TTFT (Time To First Token): {ttft_ms:.0f}ms")
|
||||
ttft_logged = True
|
||||
full_response_text += chunk.choices[0].delta.content
|
||||
live.update(display_fn(full_response_text, start_time))
|
||||
|
||||
@@ -1020,7 +1266,7 @@ class OpenAIClient:
|
||||
async def achat_completion_with_tools(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
@@ -1204,7 +1450,7 @@ class OpenAIClient:
|
||||
def chat_completion_with_tools_stream(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
tools: Optional[List[Any]] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
@@ -1356,7 +1602,7 @@ class OpenAIClient:
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response_format: BaseModel,
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
@@ -1390,7 +1636,7 @@ class OpenAIClient:
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response_format: BaseModel,
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
temperature: float = 1.0,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
|
||||
@@ -162,7 +162,7 @@ class MCP:
|
||||
# Method 1: Using command and args separately
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant...",
|
||||
llm="gpt-5-nano",
|
||||
llm="gpt-4o-mini",
|
||||
tools=MCP(
|
||||
command="/path/to/python",
|
||||
args=["/path/to/app.py"]
|
||||
@@ -172,14 +172,14 @@ class MCP:
|
||||
# Method 2: Using a single command string
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant...",
|
||||
llm="gpt-5-nano",
|
||||
llm="gpt-4o-mini",
|
||||
tools=MCP("/path/to/python /path/to/app.py")
|
||||
)
|
||||
|
||||
# Method 3: Using an SSE endpoint
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant...",
|
||||
llm="gpt-5-nano",
|
||||
llm="gpt-4o-mini",
|
||||
tools=MCP("http://localhost:8080/sse")
|
||||
)
|
||||
|
||||
@@ -548,7 +548,7 @@ class MCP:
|
||||
"""Convert the MCP tool to an OpenAI-compatible tool definition.
|
||||
|
||||
This method is specifically invoked by the Agent class when using
|
||||
provider/model format (e.g., "openai/gpt-5-nano").
|
||||
provider/model format (e.g., "openai/gpt-4o-mini").
|
||||
|
||||
Returns:
|
||||
dict or list: OpenAI-compatible tool definition(s)
|
||||
|
||||
@@ -110,6 +110,31 @@ def __getattr__(name):
|
||||
if name == "create_hooks_manager":
|
||||
from .hooks import create_hooks_manager
|
||||
return create_hooks_manager
|
||||
# Learn module
|
||||
if name == "LearnManager":
|
||||
from .learn import LearnManager
|
||||
return LearnManager
|
||||
if name == "PersonaStore":
|
||||
from .learn import PersonaStore
|
||||
return PersonaStore
|
||||
if name == "InsightStore":
|
||||
from .learn import InsightStore
|
||||
return InsightStore
|
||||
if name == "ThreadStore":
|
||||
from .learn import ThreadStore
|
||||
return ThreadStore
|
||||
if name == "PatternStore":
|
||||
from .learn import PatternStore
|
||||
return PatternStore
|
||||
if name == "DecisionStore":
|
||||
from .learn import DecisionStore
|
||||
return DecisionStore
|
||||
if name == "FeedbackStore":
|
||||
from .learn import FeedbackStore
|
||||
return FeedbackStore
|
||||
if name == "ImprovementStore":
|
||||
from .learn import ImprovementStore
|
||||
return ImprovementStore
|
||||
# DocsManager and MCPConfigManager are already imported at module level
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -153,4 +178,13 @@ __all__ = [
|
||||
# MCP config management
|
||||
"MCPConfigManager",
|
||||
"MCPConfig",
|
||||
# Learn module
|
||||
"LearnManager",
|
||||
"PersonaStore",
|
||||
"InsightStore",
|
||||
"ThreadStore",
|
||||
"PatternStore",
|
||||
"DecisionStore",
|
||||
"FeedbackStore",
|
||||
"ImprovementStore",
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Learn Module - Continuous Learning within Memory System.
|
||||
|
||||
Provides learning capabilities for agents to capture patterns, preferences,
|
||||
and insights from interactions to improve future responses.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import Agent, MemoryConfig, LearnConfig
|
||||
|
||||
# Simple enable
|
||||
agent = Agent(memory=MemoryConfig(learn=True))
|
||||
|
||||
# With specific capabilities
|
||||
agent = Agent(memory=MemoryConfig(
|
||||
learn=LearnConfig(
|
||||
persona=True,
|
||||
insights=True,
|
||||
patterns=True,
|
||||
)
|
||||
))
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"LearnManager",
|
||||
"PersonaStore",
|
||||
"InsightStore",
|
||||
"ThreadStore",
|
||||
"PatternStore",
|
||||
"DecisionStore",
|
||||
"FeedbackStore",
|
||||
"ImprovementStore",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy loading for learn module components."""
|
||||
if name == "LearnManager":
|
||||
from .manager import LearnManager
|
||||
return LearnManager
|
||||
if name == "PersonaStore":
|
||||
from .stores import PersonaStore
|
||||
return PersonaStore
|
||||
if name == "InsightStore":
|
||||
from .stores import InsightStore
|
||||
return InsightStore
|
||||
if name == "ThreadStore":
|
||||
from .stores import ThreadStore
|
||||
return ThreadStore
|
||||
if name == "PatternStore":
|
||||
from .stores import PatternStore
|
||||
return PatternStore
|
||||
if name == "DecisionStore":
|
||||
from .stores import DecisionStore
|
||||
return DecisionStore
|
||||
if name == "FeedbackStore":
|
||||
from .stores import FeedbackStore
|
||||
return FeedbackStore
|
||||
if name == "ImprovementStore":
|
||||
from .stores import ImprovementStore
|
||||
return ImprovementStore
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
LearnManager - Central manager for continuous learning capabilities.
|
||||
|
||||
Coordinates all learning stores and provides a unified interface for
|
||||
capturing and retrieving learned information.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ...config.feature_configs import LearnConfig, LearnScope
|
||||
from .stores import (
|
||||
PersonaStore,
|
||||
InsightStore,
|
||||
ThreadStore,
|
||||
PatternStore,
|
||||
DecisionStore,
|
||||
FeedbackStore,
|
||||
ImprovementStore,
|
||||
LearnEntry,
|
||||
)
|
||||
|
||||
|
||||
class LearnManager:
|
||||
"""
|
||||
Central manager for continuous learning within the memory system.
|
||||
|
||||
Coordinates all learning stores based on LearnConfig settings.
|
||||
|
||||
Usage:
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config, user_id="alice")
|
||||
|
||||
# Capture learning
|
||||
manager.capture_persona("User prefers concise responses")
|
||||
manager.capture_insight("User works in data science")
|
||||
|
||||
# Retrieve learnings
|
||||
context = manager.get_learning_context()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[LearnConfig] = None,
|
||||
user_id: Optional[str] = None,
|
||||
store_path: Optional[str] = None,
|
||||
):
|
||||
self.config = config or LearnConfig()
|
||||
self.user_id = user_id or "default"
|
||||
self.store_path = store_path or self.config.store_path
|
||||
|
||||
scope = self.config.scope
|
||||
if isinstance(scope, LearnScope):
|
||||
scope = scope.value
|
||||
|
||||
self._stores: Dict[str, Any] = {}
|
||||
|
||||
if self.config.persona:
|
||||
self._stores["persona"] = PersonaStore(
|
||||
store_path=self._get_store_path("persona"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.insights:
|
||||
self._stores["insights"] = InsightStore(
|
||||
store_path=self._get_store_path("insights"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.thread:
|
||||
self._stores["threads"] = ThreadStore(
|
||||
store_path=self._get_store_path("threads"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.patterns:
|
||||
self._stores["patterns"] = PatternStore(
|
||||
store_path=self._get_store_path("patterns"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.decisions:
|
||||
self._stores["decisions"] = DecisionStore(
|
||||
store_path=self._get_store_path("decisions"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.feedback:
|
||||
self._stores["feedback"] = FeedbackStore(
|
||||
store_path=self._get_store_path("feedback"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
if self.config.improvements:
|
||||
self._stores["improvements"] = ImprovementStore(
|
||||
store_path=self._get_store_path("improvements"),
|
||||
user_id=self.user_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
def _get_store_path(self, store_name: str) -> Optional[str]:
|
||||
"""Get path for a specific store."""
|
||||
if self.store_path:
|
||||
return f"{self.store_path}/{store_name}.json"
|
||||
return None
|
||||
|
||||
def capture_persona(self, content: str, category: str = "general") -> Optional[LearnEntry]:
|
||||
"""Capture user preference or profile information."""
|
||||
if "persona" in self._stores:
|
||||
return self._stores["persona"].add_preference(content, category)
|
||||
return None
|
||||
|
||||
def capture_insight(self, content: str, source: str = "interaction") -> Optional[LearnEntry]:
|
||||
"""Capture an observation or learning."""
|
||||
if "insights" in self._stores:
|
||||
return self._stores["insights"].add_insight(content, source)
|
||||
return None
|
||||
|
||||
def capture_thread(self, summary: str, thread_id: str) -> Optional[LearnEntry]:
|
||||
"""Capture thread/session context."""
|
||||
if "threads" in self._stores:
|
||||
return self._stores["threads"].add_thread_summary(summary, thread_id)
|
||||
return None
|
||||
|
||||
def capture_pattern(self, pattern: str, pattern_type: str = "general") -> Optional[LearnEntry]:
|
||||
"""Capture a reusable knowledge pattern."""
|
||||
if "patterns" in self._stores:
|
||||
return self._stores["patterns"].add_pattern(pattern, pattern_type)
|
||||
return None
|
||||
|
||||
def capture_decision(
|
||||
self,
|
||||
decision: str,
|
||||
reasoning: str = "",
|
||||
outcome: str = "",
|
||||
) -> Optional[LearnEntry]:
|
||||
"""Capture a decision record."""
|
||||
if "decisions" in self._stores:
|
||||
return self._stores["decisions"].add_decision(decision, reasoning, outcome)
|
||||
return None
|
||||
|
||||
def capture_feedback(
|
||||
self,
|
||||
feedback: str,
|
||||
rating: Optional[int] = None,
|
||||
source: str = "user",
|
||||
) -> Optional[LearnEntry]:
|
||||
"""Capture feedback."""
|
||||
if "feedback" in self._stores:
|
||||
return self._stores["feedback"].add_feedback(feedback, rating, source)
|
||||
return None
|
||||
|
||||
def capture_improvement(
|
||||
self,
|
||||
proposal: str,
|
||||
area: str = "general",
|
||||
priority: str = "medium",
|
||||
) -> Optional[LearnEntry]:
|
||||
"""Capture an improvement proposal."""
|
||||
if "improvements" in self._stores:
|
||||
return self._stores["improvements"].add_improvement(proposal, area, priority)
|
||||
return None
|
||||
|
||||
def get_learning_context(self, limit_per_store: int = 5) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Get learning context from all enabled stores.
|
||||
|
||||
Returns a dictionary with entries from each store, suitable for
|
||||
injection into agent context.
|
||||
"""
|
||||
context = {}
|
||||
for store_name, store in self._stores.items():
|
||||
entries = store.list_all(limit=limit_per_store)
|
||||
context[store_name] = [e.to_dict() for e in entries]
|
||||
return context
|
||||
|
||||
def search(self, query: str, limit: int = 10) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Search across all enabled stores."""
|
||||
results = {}
|
||||
for store_name, store in self._stores.items():
|
||||
entries = store.search(query, limit=limit)
|
||||
if entries:
|
||||
results[store_name] = [e.to_dict() for e in entries]
|
||||
return results
|
||||
|
||||
def get_persona_context(self, limit: int = 10) -> List[str]:
|
||||
"""Get persona entries as context strings."""
|
||||
if "persona" not in self._stores:
|
||||
return []
|
||||
entries = self._stores["persona"].list_all(limit=limit)
|
||||
return [e.content for e in entries]
|
||||
|
||||
def get_insights_context(self, limit: int = 10) -> List[str]:
|
||||
"""Get insight entries as context strings."""
|
||||
if "insights" not in self._stores:
|
||||
return []
|
||||
entries = self._stores["insights"].list_all(limit=limit)
|
||||
return [e.content for e in entries]
|
||||
|
||||
def clear_all(self) -> Dict[str, int]:
|
||||
"""Clear all stores."""
|
||||
cleared = {}
|
||||
for store_name, store in self._stores.items():
|
||||
cleared[store_name] = store.clear()
|
||||
return cleared
|
||||
|
||||
def get_stats(self) -> Dict[str, int]:
|
||||
"""Get entry counts for all stores."""
|
||||
return {
|
||||
store_name: len(store._entries)
|
||||
for store_name, store in self._stores.items()
|
||||
}
|
||||
|
||||
def to_system_prompt_context(self) -> str:
|
||||
"""
|
||||
Generate context suitable for injection into system prompt.
|
||||
|
||||
Returns a formatted string with relevant learnings.
|
||||
"""
|
||||
parts = []
|
||||
|
||||
if "persona" in self._stores:
|
||||
personas = self.get_persona_context(limit=5)
|
||||
if personas:
|
||||
parts.append("User Preferences:")
|
||||
for p in personas:
|
||||
parts.append(f"- {p}")
|
||||
|
||||
if "insights" in self._stores:
|
||||
insights = self.get_insights_context(limit=5)
|
||||
if insights:
|
||||
parts.append("\nLearned Insights:")
|
||||
for i in insights:
|
||||
parts.append(f"- {i}")
|
||||
|
||||
return "\n".join(parts) if parts else ""
|
||||
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Learning Stores - Storage backends for different learning capabilities.
|
||||
|
||||
Each store handles a specific type of learning data:
|
||||
- PersonaStore: User preferences and profile
|
||||
- InsightStore: Observations and learnings
|
||||
- ThreadStore: Session/conversation context
|
||||
- PatternStore: Reusable knowledge patterns
|
||||
- DecisionStore: Decision logging
|
||||
- FeedbackStore: Outcome signals
|
||||
- ImprovementStore: Self-improvement proposals
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class LearnEntry:
|
||||
"""Base entry for all learning stores."""
|
||||
id: str
|
||||
content: str
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
||||
updated_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"content": self.content,
|
||||
"metadata": self.metadata,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "LearnEntry":
|
||||
return cls(
|
||||
id=data["id"],
|
||||
content=data["content"],
|
||||
metadata=data.get("metadata", {}),
|
||||
created_at=data.get("created_at", datetime.utcnow().isoformat()),
|
||||
updated_at=data.get("updated_at", datetime.utcnow().isoformat()),
|
||||
)
|
||||
|
||||
|
||||
class BaseStore(ABC):
|
||||
"""Abstract base class for learning stores."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
store_path: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
scope: str = "private",
|
||||
):
|
||||
self.user_id = user_id or "default"
|
||||
self.scope = scope
|
||||
self.store_path = store_path or self._default_path()
|
||||
self._entries: Dict[str, LearnEntry] = {}
|
||||
self._load()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def store_name(self) -> str:
|
||||
"""Name of the store (used for file naming)."""
|
||||
pass
|
||||
|
||||
def _default_path(self) -> str:
|
||||
"""Default storage path."""
|
||||
base = Path.home() / ".praison" / "learn" / self.scope / self.user_id
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
return str(base / f"{self.store_name}.json")
|
||||
|
||||
def _load(self) -> None:
|
||||
"""Load entries from storage."""
|
||||
if os.path.exists(self.store_path):
|
||||
try:
|
||||
with open(self.store_path, "r") as f:
|
||||
data = json.load(f)
|
||||
self._entries = {
|
||||
k: LearnEntry.from_dict(v) for k, v in data.items()
|
||||
}
|
||||
except (json.JSONDecodeError, IOError):
|
||||
self._entries = {}
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Save entries to storage."""
|
||||
os.makedirs(os.path.dirname(self.store_path), exist_ok=True)
|
||||
with open(self.store_path, "w") as f:
|
||||
json.dump(
|
||||
{k: v.to_dict() for k, v in self._entries.items()},
|
||||
f,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
def add(self, content: str, metadata: Optional[Dict[str, Any]] = None) -> LearnEntry:
|
||||
"""Add a new entry."""
|
||||
entry_id = f"{self.store_name}_{len(self._entries)}_{datetime.utcnow().timestamp()}"
|
||||
entry = LearnEntry(
|
||||
id=entry_id,
|
||||
content=content,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
self._entries[entry_id] = entry
|
||||
self._save()
|
||||
return entry
|
||||
|
||||
def get(self, entry_id: str) -> Optional[LearnEntry]:
|
||||
"""Get entry by ID."""
|
||||
return self._entries.get(entry_id)
|
||||
|
||||
def search(self, query: str, limit: int = 10) -> List[LearnEntry]:
|
||||
"""Simple text search (can be overridden for semantic search)."""
|
||||
query_lower = query.lower()
|
||||
results = [
|
||||
entry for entry in self._entries.values()
|
||||
if query_lower in entry.content.lower()
|
||||
]
|
||||
return results[:limit]
|
||||
|
||||
def list_all(self, limit: int = 100) -> List[LearnEntry]:
|
||||
"""List all entries."""
|
||||
entries = list(self._entries.values())
|
||||
entries.sort(key=lambda x: x.updated_at, reverse=True)
|
||||
return entries[:limit]
|
||||
|
||||
def update(self, entry_id: str, content: str, metadata: Optional[Dict[str, Any]] = None) -> Optional[LearnEntry]:
|
||||
"""Update an existing entry."""
|
||||
if entry_id not in self._entries:
|
||||
return None
|
||||
entry = self._entries[entry_id]
|
||||
entry.content = content
|
||||
if metadata:
|
||||
entry.metadata.update(metadata)
|
||||
entry.updated_at = datetime.utcnow().isoformat()
|
||||
self._save()
|
||||
return entry
|
||||
|
||||
def delete(self, entry_id: str) -> bool:
|
||||
"""Delete an entry."""
|
||||
if entry_id in self._entries:
|
||||
del self._entries[entry_id]
|
||||
self._save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear(self) -> int:
|
||||
"""Clear all entries."""
|
||||
count = len(self._entries)
|
||||
self._entries = {}
|
||||
self._save()
|
||||
return count
|
||||
|
||||
|
||||
class PersonaStore(BaseStore):
|
||||
"""Store for user preferences and profile information."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "persona"
|
||||
|
||||
def add_preference(self, preference: str, category: str = "general") -> LearnEntry:
|
||||
"""Add a user preference."""
|
||||
return self.add(preference, {"category": category, "type": "preference"})
|
||||
|
||||
def add_profile(self, profile_data: str, aspect: str = "general") -> LearnEntry:
|
||||
"""Add profile information."""
|
||||
return self.add(profile_data, {"aspect": aspect, "type": "profile"})
|
||||
|
||||
|
||||
class InsightStore(BaseStore):
|
||||
"""Store for observations and learnings."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "insights"
|
||||
|
||||
def add_insight(self, insight: str, source: str = "interaction") -> LearnEntry:
|
||||
"""Add an insight."""
|
||||
return self.add(insight, {"source": source, "type": "insight"})
|
||||
|
||||
def add_observation(self, observation: str, context: str = "") -> LearnEntry:
|
||||
"""Add an observation."""
|
||||
return self.add(observation, {"context": context, "type": "observation"})
|
||||
|
||||
|
||||
class ThreadStore(BaseStore):
|
||||
"""Store for session/conversation context."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "threads"
|
||||
|
||||
def add_thread_summary(self, summary: str, thread_id: str) -> LearnEntry:
|
||||
"""Add a thread summary."""
|
||||
return self.add(summary, {"thread_id": thread_id, "type": "summary"})
|
||||
|
||||
def add_context(self, context: str, thread_id: str) -> LearnEntry:
|
||||
"""Add thread context."""
|
||||
return self.add(context, {"thread_id": thread_id, "type": "context"})
|
||||
|
||||
|
||||
class PatternStore(BaseStore):
|
||||
"""Store for reusable knowledge patterns."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "patterns"
|
||||
|
||||
def add_pattern(self, pattern: str, pattern_type: str = "general") -> LearnEntry:
|
||||
"""Add a knowledge pattern."""
|
||||
return self.add(pattern, {"pattern_type": pattern_type, "type": "pattern"})
|
||||
|
||||
|
||||
class DecisionStore(BaseStore):
|
||||
"""Store for decision logging."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "decisions"
|
||||
|
||||
def add_decision(
|
||||
self,
|
||||
decision: str,
|
||||
reasoning: str = "",
|
||||
outcome: str = "",
|
||||
) -> LearnEntry:
|
||||
"""Add a decision record."""
|
||||
return self.add(decision, {
|
||||
"reasoning": reasoning,
|
||||
"outcome": outcome,
|
||||
"type": "decision",
|
||||
})
|
||||
|
||||
|
||||
class FeedbackStore(BaseStore):
|
||||
"""Store for outcome signals and feedback."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "feedback"
|
||||
|
||||
def add_feedback(
|
||||
self,
|
||||
feedback: str,
|
||||
rating: Optional[int] = None,
|
||||
source: str = "user",
|
||||
) -> LearnEntry:
|
||||
"""Add feedback."""
|
||||
metadata = {"source": source, "type": "feedback"}
|
||||
if rating is not None:
|
||||
metadata["rating"] = rating
|
||||
return self.add(feedback, metadata)
|
||||
|
||||
|
||||
class ImprovementStore(BaseStore):
|
||||
"""Store for self-improvement proposals."""
|
||||
|
||||
@property
|
||||
def store_name(self) -> str:
|
||||
return "improvements"
|
||||
|
||||
def add_improvement(
|
||||
self,
|
||||
proposal: str,
|
||||
area: str = "general",
|
||||
priority: str = "medium",
|
||||
) -> LearnEntry:
|
||||
"""Add an improvement proposal."""
|
||||
return self.add(proposal, {
|
||||
"area": area,
|
||||
"priority": priority,
|
||||
"type": "improvement",
|
||||
})
|
||||
@@ -109,7 +109,7 @@ class Memory:
|
||||
},
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {"model": "gpt-5-nano", "api_key": "..."}
|
||||
"config": {"model": "gpt-4o-mini", "api_key": "..."}
|
||||
},
|
||||
"embedder": {
|
||||
"provider": "openai",
|
||||
@@ -146,6 +146,8 @@ class Memory:
|
||||
self.use_rag = (self.provider.lower() == "rag") and CHROMADB_AVAILABLE and self.cfg.get("use_embedding", False)
|
||||
self.use_mongodb = (self.provider.lower() == "mongodb") and PYMONGO_AVAILABLE
|
||||
self.graph_enabled = False # Initialize graph support flag
|
||||
self._learn_manager = None # Lazy-loaded LearnManager
|
||||
self._learn_config = self.cfg.get("learn", None) # Learn configuration
|
||||
|
||||
# Extract embedding model from config
|
||||
self.embedder_config = self.cfg.get("embedder", {})
|
||||
@@ -1442,7 +1444,7 @@ class Memory:
|
||||
import litellm
|
||||
|
||||
# Convert model name if it's in litellm format
|
||||
model_name = llm or "gpt-5-nano"
|
||||
model_name = llm or "gpt-4o-mini"
|
||||
|
||||
response = litellm.completion(
|
||||
model=model_name,
|
||||
@@ -1459,7 +1461,7 @@ class Memory:
|
||||
client = OpenAI()
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=llm or "gpt-5-nano",
|
||||
model=llm or "gpt-4o-mini",
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": custom_prompt or default_prompt
|
||||
@@ -1599,3 +1601,50 @@ class Memory:
|
||||
except Exception as e:
|
||||
self._log_verbose(f"Error getting all memories: {e}", logging.ERROR)
|
||||
return []
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Learn Integration
|
||||
# -------------------------------------------------------------------------
|
||||
@property
|
||||
def learn(self):
|
||||
"""
|
||||
Get the LearnManager for continuous learning capabilities.
|
||||
|
||||
Returns None if learn is not enabled in config.
|
||||
|
||||
Usage:
|
||||
memory = Memory({"learn": True})
|
||||
memory.learn.capture_persona("User prefers concise responses")
|
||||
memory.learn.capture_insight("User works in data science")
|
||||
"""
|
||||
if self._learn_manager is not None:
|
||||
return self._learn_manager
|
||||
|
||||
if self._learn_config is None or self._learn_config is False:
|
||||
return None
|
||||
|
||||
from .learn import LearnManager
|
||||
from ..config.feature_configs import LearnConfig
|
||||
|
||||
if self._learn_config is True:
|
||||
config = LearnConfig()
|
||||
elif isinstance(self._learn_config, dict):
|
||||
config = LearnConfig(**self._learn_config)
|
||||
elif isinstance(self._learn_config, LearnConfig):
|
||||
config = self._learn_config
|
||||
else:
|
||||
return None
|
||||
|
||||
user_id = self.cfg.get("user_id", "default")
|
||||
self._learn_manager = LearnManager(config=config, user_id=user_id)
|
||||
return self._learn_manager
|
||||
|
||||
def get_learn_context(self) -> str:
|
||||
"""
|
||||
Get learning context suitable for injection into system prompt.
|
||||
|
||||
Returns empty string if learn is not enabled.
|
||||
"""
|
||||
if self.learn is None:
|
||||
return ""
|
||||
return self.learn.to_system_prompt_context()
|
||||
@@ -722,9 +722,8 @@ Workflow Finished: {self.workflow_finished} # ADDED: Workflow Finished Status
|
||||
goal="Manage the entire flow of tasks and delegate them to the right agent",
|
||||
backstory="Expert project manager to coordinate tasks among agents",
|
||||
llm=self.manager_llm,
|
||||
verbose=self.verbose,
|
||||
markdown=True,
|
||||
self_reflect=False
|
||||
output={"verbose": self.verbose, "markdown": True},
|
||||
reflection=False
|
||||
)
|
||||
|
||||
class ManagerInstructions(BaseModel):
|
||||
@@ -1384,9 +1383,8 @@ Workflow Finished: {self.workflow_finished} # ADDED: Workflow Finished Status
|
||||
goal="Manage the entire flow of tasks and delegate them to the right agent",
|
||||
backstory="Expert project manager to coordinate tasks among agents",
|
||||
llm=self.manager_llm,
|
||||
verbose=self.verbose,
|
||||
markdown=True,
|
||||
self_reflect=False
|
||||
output={"verbose": self.verbose, "markdown": True},
|
||||
reflection=False
|
||||
)
|
||||
|
||||
class ManagerInstructions(BaseModel):
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
"""
|
||||
PraisonAI Agents Core Profiling Module
|
||||
|
||||
Lightweight, protocol-based profiling for the core SDK.
|
||||
Zero overhead when disabled. Opt-in only.
|
||||
|
||||
Features:
|
||||
- Pre-LLM latency breakdown
|
||||
- LLM client initialization timing
|
||||
- Tool resolution timing
|
||||
- Message serialization timing
|
||||
- Streaming TTFT tracking
|
||||
- Import timing
|
||||
- Async-safe
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.profiling import Profiler, profile_block
|
||||
|
||||
# Enable profiling
|
||||
Profiler.enable()
|
||||
|
||||
# Profile a block
|
||||
with profile_block("my_operation"):
|
||||
do_something()
|
||||
|
||||
# Get report
|
||||
Profiler.report()
|
||||
"""
|
||||
|
||||
import time
|
||||
import os
|
||||
import threading
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import Dict, List, Optional, Any, Callable
|
||||
from contextlib import contextmanager
|
||||
import functools
|
||||
import json
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Data Classes
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class TimingRecord:
|
||||
"""Record of a single timing measurement."""
|
||||
name: str
|
||||
duration_ms: float
|
||||
category: str = "block"
|
||||
parent: str = ""
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamingRecord:
|
||||
"""Record of streaming operation."""
|
||||
name: str
|
||||
ttft_ms: float # Time to first token
|
||||
total_ms: float
|
||||
chunk_count: int = 0
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PreLLMBreakdown:
|
||||
"""Breakdown of pre-LLM latency phases."""
|
||||
agent_init_ms: float = 0.0
|
||||
llm_client_init_ms: float = 0.0
|
||||
tool_resolution_ms: float = 0.0
|
||||
message_build_ms: float = 0.0
|
||||
context_management_ms: float = 0.0
|
||||
knowledge_retrieval_ms: float = 0.0
|
||||
guardrail_check_ms: float = 0.0
|
||||
total_pre_llm_ms: float = 0.0
|
||||
|
||||
def to_dict(self) -> Dict[str, float]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Streaming Tracker
|
||||
# ============================================================================
|
||||
|
||||
class StreamingTracker:
|
||||
"""Track streaming operations for TTFT metrics."""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self._start_time: Optional[float] = None
|
||||
self._first_token_time: Optional[float] = None
|
||||
self._end_time: Optional[float] = None
|
||||
self._chunk_count: int = 0
|
||||
|
||||
def start(self) -> None:
|
||||
self._start_time = time.perf_counter()
|
||||
|
||||
def first_token(self) -> None:
|
||||
if self._first_token_time is None:
|
||||
self._first_token_time = time.perf_counter()
|
||||
|
||||
def chunk(self) -> None:
|
||||
self._chunk_count += 1
|
||||
|
||||
def end(self) -> None:
|
||||
self._end_time = time.perf_counter()
|
||||
if self._start_time is not None:
|
||||
ttft_ms = 0.0
|
||||
if self._first_token_time is not None:
|
||||
ttft_ms = (self._first_token_time - self._start_time) * 1000
|
||||
total_ms = (self._end_time - self._start_time) * 1000
|
||||
Profiler.record_streaming(
|
||||
name=self.name,
|
||||
ttft_ms=ttft_ms,
|
||||
total_ms=total_ms,
|
||||
chunk_count=self._chunk_count
|
||||
)
|
||||
|
||||
@property
|
||||
def ttft_ms(self) -> float:
|
||||
if self._start_time and self._first_token_time:
|
||||
return (self._first_token_time - self._start_time) * 1000
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def elapsed_ms(self) -> float:
|
||||
if self._start_time:
|
||||
end = self._end_time or time.perf_counter()
|
||||
return (end - self._start_time) * 1000
|
||||
return 0.0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Core Profiler
|
||||
# ============================================================================
|
||||
|
||||
class Profiler:
|
||||
"""
|
||||
Lightweight profiler for PraisonAI Agents core SDK.
|
||||
|
||||
Thread-safe singleton with zero overhead when disabled.
|
||||
"""
|
||||
|
||||
_lock = threading.Lock()
|
||||
_enabled: bool = False
|
||||
_timings: List[TimingRecord] = []
|
||||
_streaming: List[StreamingRecord] = []
|
||||
_pre_llm_breakdown: Optional[PreLLMBreakdown] = None
|
||||
_current_phases: Dict[str, float] = {} # Track in-progress phases
|
||||
_phase_stack: List[str] = [] # Track nested phases
|
||||
|
||||
@classmethod
|
||||
def enable(cls) -> None:
|
||||
"""Enable profiling."""
|
||||
cls._enabled = True
|
||||
|
||||
@classmethod
|
||||
def disable(cls) -> None:
|
||||
"""Disable profiling."""
|
||||
cls._enabled = False
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls) -> bool:
|
||||
"""Check if profiling is enabled."""
|
||||
return cls._enabled or os.environ.get('PRAISONAI_PROFILE', '').lower() in ('1', 'true', 'yes')
|
||||
|
||||
@classmethod
|
||||
def clear(cls) -> None:
|
||||
"""Clear all profiling data."""
|
||||
with cls._lock:
|
||||
cls._timings.clear()
|
||||
cls._streaming.clear()
|
||||
cls._pre_llm_breakdown = None
|
||||
cls._current_phases.clear()
|
||||
cls._phase_stack.clear()
|
||||
|
||||
@classmethod
|
||||
def record_timing(cls, name: str, duration_ms: float, category: str = "block",
|
||||
parent: str = "", metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""Record a timing measurement."""
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
with cls._lock:
|
||||
cls._timings.append(TimingRecord(
|
||||
name=name,
|
||||
duration_ms=duration_ms,
|
||||
category=category,
|
||||
parent=parent,
|
||||
metadata=metadata or {}
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def record_streaming(cls, name: str, ttft_ms: float, total_ms: float,
|
||||
chunk_count: int = 0) -> None:
|
||||
"""Record streaming metrics."""
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
with cls._lock:
|
||||
cls._streaming.append(StreamingRecord(
|
||||
name=name,
|
||||
ttft_ms=ttft_ms,
|
||||
total_ms=total_ms,
|
||||
chunk_count=chunk_count
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def start_phase(cls, phase_name: str) -> None:
|
||||
"""Start timing a phase (for pre-LLM breakdown)."""
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
with cls._lock:
|
||||
cls._current_phases[phase_name] = time.perf_counter()
|
||||
cls._phase_stack.append(phase_name)
|
||||
|
||||
@classmethod
|
||||
def end_phase(cls, phase_name: str) -> float:
|
||||
"""End timing a phase and return duration in ms."""
|
||||
if not cls.is_enabled():
|
||||
return 0.0
|
||||
with cls._lock:
|
||||
start = cls._current_phases.pop(phase_name, None)
|
||||
if phase_name in cls._phase_stack:
|
||||
cls._phase_stack.remove(phase_name)
|
||||
if start is None:
|
||||
return 0.0
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
# Record as timing
|
||||
parent = cls._phase_stack[-1] if cls._phase_stack else ""
|
||||
cls._timings.append(TimingRecord(
|
||||
name=phase_name,
|
||||
duration_ms=duration_ms,
|
||||
category="phase",
|
||||
parent=parent
|
||||
))
|
||||
return duration_ms
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def phase(cls, name: str, category: str = "phase"):
|
||||
"""Context manager for timing a phase."""
|
||||
if not cls.is_enabled():
|
||||
yield
|
||||
return
|
||||
|
||||
start = time.perf_counter()
|
||||
parent = ""
|
||||
with cls._lock:
|
||||
parent = cls._phase_stack[-1] if cls._phase_stack else ""
|
||||
cls._phase_stack.append(name)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
with cls._lock:
|
||||
if name in cls._phase_stack:
|
||||
cls._phase_stack.remove(name)
|
||||
cls._timings.append(TimingRecord(
|
||||
name=name,
|
||||
duration_ms=duration_ms,
|
||||
category=category,
|
||||
parent=parent
|
||||
))
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def streaming(cls, name: str):
|
||||
"""Context manager for streaming profiling."""
|
||||
tracker = StreamingTracker(name)
|
||||
tracker.start()
|
||||
try:
|
||||
yield tracker
|
||||
finally:
|
||||
tracker.end()
|
||||
|
||||
@classmethod
|
||||
def set_pre_llm_breakdown(cls, breakdown: PreLLMBreakdown) -> None:
|
||||
"""Set the pre-LLM breakdown data."""
|
||||
if not cls.is_enabled():
|
||||
return
|
||||
with cls._lock:
|
||||
cls._pre_llm_breakdown = breakdown
|
||||
|
||||
@classmethod
|
||||
def get_timings(cls, category: Optional[str] = None) -> List[TimingRecord]:
|
||||
"""Get timing records."""
|
||||
with cls._lock:
|
||||
if category:
|
||||
return [t for t in cls._timings if t.category == category]
|
||||
return cls._timings.copy()
|
||||
|
||||
@classmethod
|
||||
def get_streaming_records(cls) -> List[StreamingRecord]:
|
||||
"""Get streaming records."""
|
||||
with cls._lock:
|
||||
return cls._streaming.copy()
|
||||
|
||||
@classmethod
|
||||
def get_pre_llm_breakdown(cls) -> Optional[PreLLMBreakdown]:
|
||||
"""Get pre-LLM breakdown."""
|
||||
with cls._lock:
|
||||
return cls._pre_llm_breakdown
|
||||
|
||||
@classmethod
|
||||
def get_summary(cls) -> Dict[str, Any]:
|
||||
"""Get profiling summary."""
|
||||
with cls._lock:
|
||||
total_time = sum(t.duration_ms for t in cls._timings)
|
||||
|
||||
# Group by category
|
||||
by_category: Dict[str, float] = {}
|
||||
for t in cls._timings:
|
||||
by_category[t.category] = by_category.get(t.category, 0) + t.duration_ms
|
||||
|
||||
# Top slowest
|
||||
slowest = sorted(cls._timings, key=lambda x: x.duration_ms, reverse=True)[:15]
|
||||
|
||||
# Pre-LLM breakdown
|
||||
pre_llm = cls._pre_llm_breakdown.to_dict() if cls._pre_llm_breakdown else {}
|
||||
|
||||
# Streaming stats
|
||||
streaming_stats = []
|
||||
for s in cls._streaming:
|
||||
streaming_stats.append({
|
||||
'name': s.name,
|
||||
'ttft_ms': s.ttft_ms,
|
||||
'total_ms': s.total_ms,
|
||||
'chunk_count': s.chunk_count
|
||||
})
|
||||
|
||||
return {
|
||||
'total_time_ms': total_time,
|
||||
'timing_count': len(cls._timings),
|
||||
'by_category': by_category,
|
||||
'slowest_operations': [(s.name, s.duration_ms, s.category) for s in slowest],
|
||||
'pre_llm_breakdown': pre_llm,
|
||||
'streaming': streaming_stats
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def report(cls, output: str = "console") -> str:
|
||||
"""Generate and output profiling report."""
|
||||
summary = cls.get_summary()
|
||||
|
||||
lines = [
|
||||
"=" * 70,
|
||||
"PraisonAI Agents Core Profiling Report",
|
||||
"=" * 70,
|
||||
"",
|
||||
f"Total Time: {summary['total_time_ms']:.2f}ms",
|
||||
f"Timing Records: {summary['timing_count']}",
|
||||
"",
|
||||
]
|
||||
|
||||
# Pre-LLM Breakdown (key for debugging the 7s delay)
|
||||
if summary['pre_llm_breakdown']:
|
||||
lines.append("## Pre-LLM Latency Breakdown")
|
||||
lines.append("-" * 50)
|
||||
for phase, ms in summary['pre_llm_breakdown'].items():
|
||||
if ms > 0:
|
||||
lines.append(f" {phase:<30}: {ms:>10.2f} ms")
|
||||
lines.append("")
|
||||
|
||||
# By Category
|
||||
lines.append("## By Category")
|
||||
lines.append("-" * 50)
|
||||
for cat, time_ms in summary['by_category'].items():
|
||||
lines.append(f" {cat}: {time_ms:.2f}ms")
|
||||
lines.append("")
|
||||
|
||||
# Slowest Operations
|
||||
lines.append("## Slowest Operations")
|
||||
lines.append("-" * 50)
|
||||
for name, time_ms, cat in summary['slowest_operations']:
|
||||
lines.append(f" {name:<40}: {time_ms:>10.2f}ms [{cat}]")
|
||||
lines.append("")
|
||||
|
||||
# Streaming Stats
|
||||
if summary['streaming']:
|
||||
lines.append("## Streaming Metrics")
|
||||
lines.append("-" * 50)
|
||||
for s in summary['streaming']:
|
||||
lines.append(f" {s['name']}: TTFT={s['ttft_ms']:.2f}ms, Total={s['total_ms']:.2f}ms, Chunks={s['chunk_count']}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("=" * 70)
|
||||
|
||||
report_text = "\n".join(lines)
|
||||
|
||||
if output == "console":
|
||||
print(report_text)
|
||||
|
||||
return report_text
|
||||
|
||||
@classmethod
|
||||
def export_json(cls) -> str:
|
||||
"""Export profiling data as JSON."""
|
||||
summary = cls.get_summary()
|
||||
with cls._lock:
|
||||
data = {
|
||||
'summary': summary,
|
||||
'timings': [asdict(t) for t in cls._timings],
|
||||
'streaming': [asdict(s) for s in cls._streaming],
|
||||
}
|
||||
return json.dumps(data, indent=2, default=str)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Decorators
|
||||
# ============================================================================
|
||||
|
||||
def profile_func(func: Optional[Callable] = None, *, category: str = "function"):
|
||||
"""Decorator to profile a function."""
|
||||
def decorator(fn: Callable) -> Callable:
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not Profiler.is_enabled():
|
||||
return fn(*args, **kwargs)
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
finally:
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
Profiler.record_timing(fn.__name__, duration_ms, category)
|
||||
return wrapper
|
||||
|
||||
if func is not None:
|
||||
return decorator(func)
|
||||
return decorator
|
||||
|
||||
|
||||
def profile_async(func: Optional[Callable] = None, *, category: str = "async"):
|
||||
"""Decorator to profile an async function."""
|
||||
def decorator(fn: Callable) -> Callable:
|
||||
@functools.wraps(fn)
|
||||
async def wrapper(*args, **kwargs):
|
||||
if not Profiler.is_enabled():
|
||||
return await fn(*args, **kwargs)
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
return await fn(*args, **kwargs)
|
||||
finally:
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
Profiler.record_timing(fn.__name__, duration_ms, category)
|
||||
return wrapper
|
||||
|
||||
if func is not None:
|
||||
return decorator(func)
|
||||
return decorator
|
||||
|
||||
|
||||
@contextmanager
|
||||
def profile_block(name: str, category: str = "block"):
|
||||
"""Context manager for profiling a block of code."""
|
||||
if not Profiler.is_enabled():
|
||||
yield
|
||||
return
|
||||
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
Profiler.record_timing(name, duration_ms, category)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Exports
|
||||
# ============================================================================
|
||||
|
||||
__all__ = [
|
||||
'Profiler',
|
||||
'StreamingTracker',
|
||||
'TimingRecord',
|
||||
'StreamingRecord',
|
||||
'PreLLMBreakdown',
|
||||
'profile_func',
|
||||
'profile_async',
|
||||
'profile_block',
|
||||
]
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
PraisonAI Streaming Module.
|
||||
|
||||
Provides the StreamEvent protocol for pass-through streaming from LLM providers.
|
||||
|
||||
This module is lazily loaded to ensure zero performance impact when streaming
|
||||
features are not used.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.streaming import (
|
||||
StreamEvent,
|
||||
StreamEventType,
|
||||
StreamMetrics,
|
||||
StreamEventEmitter,
|
||||
create_text_printer_callback,
|
||||
)
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"StreamEvent",
|
||||
"StreamEventType",
|
||||
"StreamMetrics",
|
||||
"StreamEventEmitter",
|
||||
"StreamCallback",
|
||||
"AsyncStreamCallback",
|
||||
"create_text_printer_callback",
|
||||
"create_metrics_callback",
|
||||
"StreamLogger",
|
||||
"create_logging_callback",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy loading of streaming components."""
|
||||
if name in __all__:
|
||||
# Events module exports
|
||||
if name in (
|
||||
"StreamEvent", "StreamEventType", "StreamMetrics",
|
||||
"StreamEventEmitter", "StreamCallback", "AsyncStreamCallback",
|
||||
"create_text_printer_callback", "create_metrics_callback"
|
||||
):
|
||||
from .events import (
|
||||
StreamEvent,
|
||||
StreamEventType,
|
||||
StreamMetrics,
|
||||
StreamEventEmitter,
|
||||
StreamCallback,
|
||||
AsyncStreamCallback,
|
||||
create_text_printer_callback,
|
||||
create_metrics_callback,
|
||||
)
|
||||
|
||||
_events_exports = {
|
||||
"StreamEvent": StreamEvent,
|
||||
"StreamEventType": StreamEventType,
|
||||
"StreamMetrics": StreamMetrics,
|
||||
"StreamEventEmitter": StreamEventEmitter,
|
||||
"StreamCallback": StreamCallback,
|
||||
"AsyncStreamCallback": AsyncStreamCallback,
|
||||
"create_text_printer_callback": create_text_printer_callback,
|
||||
"create_metrics_callback": create_metrics_callback,
|
||||
}
|
||||
return _events_exports.get(name)
|
||||
|
||||
# Logging module exports
|
||||
if name in ("StreamLogger", "create_logging_callback"):
|
||||
from .logging import StreamLogger, create_logging_callback
|
||||
|
||||
_logging_exports = {
|
||||
"StreamLogger": StreamLogger,
|
||||
"create_logging_callback": create_logging_callback,
|
||||
}
|
||||
return _logging_exports.get(name)
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,300 @@
|
||||
"""
|
||||
StreamEvent protocol for PraisonAI streaming.
|
||||
|
||||
This module defines the minimal streaming event protocol that both OpenAI SDK
|
||||
and LiteLLM streaming paths map their native chunks/events into.
|
||||
|
||||
Design principles:
|
||||
- Zero overhead when not used (lazy imports, no global state)
|
||||
- Multi-agent safe (per-agent callbacks, no shared mutable state)
|
||||
- Async-safe (separate sync/async callback interfaces)
|
||||
- Provider-agnostic (same events for OpenAI, LiteLLM, etc.)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Protocol
|
||||
import time
|
||||
|
||||
|
||||
class StreamEventType(Enum):
|
||||
"""Types of streaming events emitted during LLM response streaming."""
|
||||
|
||||
REQUEST_START = "request_start" # Before API call is made
|
||||
HEADERS_RECEIVED = "headers_received" # When HTTP 200 headers arrive
|
||||
FIRST_TOKEN = "first_token" # First content delta received (TTFT marker)
|
||||
DELTA_TEXT = "delta_text" # Text content delta
|
||||
DELTA_TOOL_CALL = "delta_tool_call" # Tool call delta (function name, args chunk)
|
||||
TOOL_CALL_END = "tool_call_end" # Tool call complete
|
||||
LAST_TOKEN = "last_token" # Final content delta
|
||||
STREAM_END = "stream_end" # Stream completed successfully
|
||||
ERROR = "error" # Error during streaming
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamEvent:
|
||||
"""
|
||||
A single streaming event emitted during LLM response streaming.
|
||||
|
||||
Attributes:
|
||||
type: The type of event (see StreamEventType)
|
||||
timestamp: High-precision timestamp (time.perf_counter())
|
||||
content: Text content for DELTA_TEXT events
|
||||
tool_call: Tool call data for DELTA_TOOL_CALL events
|
||||
metadata: Additional event-specific metadata
|
||||
error: Error message for ERROR events
|
||||
"""
|
||||
type: StreamEventType
|
||||
timestamp: float = field(default_factory=time.perf_counter)
|
||||
content: Optional[str] = None
|
||||
tool_call: Optional[Dict[str, Any]] = None
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
parts = [f"StreamEvent({self.type.value}"]
|
||||
if self.content:
|
||||
preview = self.content[:20] + "..." if len(self.content) > 20 else self.content
|
||||
parts.append(f", content={preview!r}")
|
||||
if self.tool_call:
|
||||
parts.append(f", tool_call={self.tool_call.get('name', 'unknown')}")
|
||||
if self.error:
|
||||
parts.append(f", error={self.error!r}")
|
||||
parts.append(")")
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
class StreamCallback(Protocol):
|
||||
"""Protocol for synchronous stream event callbacks."""
|
||||
|
||||
def __call__(self, event: StreamEvent) -> None:
|
||||
"""Called when a stream event is emitted."""
|
||||
...
|
||||
|
||||
|
||||
class AsyncStreamCallback(Protocol):
|
||||
"""Protocol for asynchronous stream event callbacks."""
|
||||
|
||||
async def __call__(self, event: StreamEvent) -> None:
|
||||
"""Called when a stream event is emitted (async)."""
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamMetrics:
|
||||
"""
|
||||
Timing metrics for a streaming response.
|
||||
|
||||
Tracks key timestamps to measure:
|
||||
- TTFT (Time To First Token): How long until first token arrives
|
||||
- Stream Duration: How long the token streaming takes
|
||||
- Total Time: End-to-end request time
|
||||
|
||||
All times are in seconds, measured with time.perf_counter() for precision.
|
||||
"""
|
||||
request_start: float = 0.0
|
||||
headers_received: float = 0.0
|
||||
first_token: float = 0.0
|
||||
last_token: float = 0.0
|
||||
stream_end: float = 0.0
|
||||
token_count: int = 0
|
||||
|
||||
@property
|
||||
def ttft(self) -> float:
|
||||
"""Time To First Token in seconds (first_token - request_start)."""
|
||||
if self.first_token and self.request_start:
|
||||
return self.first_token - self.request_start
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def stream_duration(self) -> float:
|
||||
"""Duration of token streaming in seconds (last_token - first_token)."""
|
||||
if self.last_token and self.first_token:
|
||||
return self.last_token - self.first_token
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def total_time(self) -> float:
|
||||
"""Total request time in seconds (stream_end - request_start)."""
|
||||
if self.stream_end and self.request_start:
|
||||
return self.stream_end - self.request_start
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def tokens_per_second(self) -> float:
|
||||
"""Token generation rate (tokens / stream_duration)."""
|
||||
if self.stream_duration > 0 and self.token_count > 0:
|
||||
return self.token_count / self.stream_duration
|
||||
return 0.0
|
||||
|
||||
def update_from_event(self, event: StreamEvent) -> None:
|
||||
"""Update metrics from a StreamEvent."""
|
||||
if event.type == StreamEventType.REQUEST_START:
|
||||
self.request_start = event.timestamp
|
||||
elif event.type == StreamEventType.HEADERS_RECEIVED:
|
||||
self.headers_received = event.timestamp
|
||||
elif event.type == StreamEventType.FIRST_TOKEN:
|
||||
self.first_token = event.timestamp
|
||||
self.token_count = 1
|
||||
elif event.type == StreamEventType.DELTA_TEXT:
|
||||
self.token_count += 1
|
||||
elif event.type == StreamEventType.LAST_TOKEN:
|
||||
self.last_token = event.timestamp
|
||||
elif event.type == StreamEventType.STREAM_END:
|
||||
self.stream_end = event.timestamp
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert metrics to dictionary for logging/display."""
|
||||
return {
|
||||
"ttft_ms": round(self.ttft * 1000, 1),
|
||||
"stream_duration_ms": round(self.stream_duration * 1000, 1),
|
||||
"total_time_ms": round(self.total_time * 1000, 1),
|
||||
"token_count": self.token_count,
|
||||
"tokens_per_second": round(self.tokens_per_second, 1),
|
||||
}
|
||||
|
||||
def format_summary(self) -> str:
|
||||
"""Format metrics as a human-readable summary string."""
|
||||
return (
|
||||
f"TTFT: {self.ttft*1000:.0f}ms | "
|
||||
f"Stream: {self.stream_duration*1000:.0f}ms | "
|
||||
f"Total: {self.total_time*1000:.0f}ms | "
|
||||
f"Tokens: {self.token_count} ({self.tokens_per_second:.1f}/s)"
|
||||
)
|
||||
|
||||
|
||||
class StreamEventEmitter:
|
||||
"""
|
||||
Manages stream event callbacks for an agent or streaming session.
|
||||
|
||||
Thread-safe and multi-agent safe - each agent has its own emitter instance.
|
||||
Supports both sync and async callbacks.
|
||||
|
||||
Usage:
|
||||
emitter = StreamEventEmitter()
|
||||
emitter.add_callback(my_callback)
|
||||
|
||||
# During streaming:
|
||||
emitter.emit(StreamEvent(type=StreamEventType.DELTA_TEXT, content="Hello"))
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._callbacks: List[StreamCallback] = []
|
||||
self._async_callbacks: List[AsyncStreamCallback] = []
|
||||
self._metrics: Optional[StreamMetrics] = None
|
||||
self._collect_metrics: bool = False
|
||||
|
||||
def add_callback(self, callback: StreamCallback) -> None:
|
||||
"""Add a synchronous callback to receive stream events."""
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def remove_callback(self, callback: StreamCallback) -> None:
|
||||
"""Remove a synchronous callback."""
|
||||
if callback in self._callbacks:
|
||||
self._callbacks.remove(callback)
|
||||
|
||||
def add_async_callback(self, callback: AsyncStreamCallback) -> None:
|
||||
"""Add an asynchronous callback to receive stream events."""
|
||||
self._async_callbacks.append(callback)
|
||||
|
||||
def remove_async_callback(self, callback: AsyncStreamCallback) -> None:
|
||||
"""Remove an asynchronous callback."""
|
||||
if callback in self._async_callbacks:
|
||||
self._async_callbacks.remove(callback)
|
||||
|
||||
def enable_metrics(self) -> None:
|
||||
"""Enable metrics collection for this streaming session."""
|
||||
self._collect_metrics = True
|
||||
self._metrics = StreamMetrics()
|
||||
|
||||
def get_metrics(self) -> Optional[StreamMetrics]:
|
||||
"""Get collected metrics (None if metrics not enabled)."""
|
||||
return self._metrics
|
||||
|
||||
def reset_metrics(self) -> None:
|
||||
"""Reset metrics for a new streaming session."""
|
||||
if self._collect_metrics:
|
||||
self._metrics = StreamMetrics()
|
||||
|
||||
def emit(self, event: StreamEvent) -> None:
|
||||
"""
|
||||
Emit a stream event to all registered callbacks.
|
||||
|
||||
This is the synchronous emission path. For async streaming,
|
||||
use emit_async().
|
||||
"""
|
||||
# Update metrics if enabled
|
||||
if self._collect_metrics and self._metrics:
|
||||
self._metrics.update_from_event(event)
|
||||
|
||||
# Call all sync callbacks
|
||||
for callback in self._callbacks:
|
||||
try:
|
||||
callback(event)
|
||||
except Exception:
|
||||
# Don't let callback errors break streaming
|
||||
pass
|
||||
|
||||
async def emit_async(self, event: StreamEvent) -> None:
|
||||
"""
|
||||
Emit a stream event to all registered callbacks (async version).
|
||||
|
||||
Calls both sync and async callbacks.
|
||||
"""
|
||||
# Update metrics if enabled
|
||||
if self._collect_metrics and self._metrics:
|
||||
self._metrics.update_from_event(event)
|
||||
|
||||
# Call sync callbacks
|
||||
for callback in self._callbacks:
|
||||
try:
|
||||
callback(event)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Call async callbacks
|
||||
for callback in self._async_callbacks:
|
||||
try:
|
||||
await callback(event)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@property
|
||||
def has_callbacks(self) -> bool:
|
||||
"""Check if any callbacks are registered."""
|
||||
return bool(self._callbacks or self._async_callbacks)
|
||||
|
||||
|
||||
def create_text_printer_callback(flush: bool = True) -> StreamCallback:
|
||||
"""
|
||||
Create a simple callback that prints text deltas to stdout.
|
||||
|
||||
Args:
|
||||
flush: Whether to flush stdout after each print (default True)
|
||||
|
||||
Returns:
|
||||
A StreamCallback that prints DELTA_TEXT content
|
||||
"""
|
||||
def printer(event: StreamEvent) -> None:
|
||||
if event.type == StreamEventType.DELTA_TEXT and event.content:
|
||||
print(event.content, end="", flush=flush)
|
||||
elif event.type == StreamEventType.STREAM_END:
|
||||
print() # Newline at end
|
||||
|
||||
return printer
|
||||
|
||||
|
||||
def create_metrics_callback(metrics: StreamMetrics) -> StreamCallback:
|
||||
"""
|
||||
Create a callback that updates a StreamMetrics instance.
|
||||
|
||||
Args:
|
||||
metrics: The StreamMetrics instance to update
|
||||
|
||||
Returns:
|
||||
A StreamCallback that updates the metrics
|
||||
"""
|
||||
def updater(event: StreamEvent) -> None:
|
||||
metrics.update_from_event(event)
|
||||
|
||||
return updater
|
||||
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Streaming logging utilities for PraisonAI.
|
||||
|
||||
Provides guarded logging for streaming events with clear timestamps
|
||||
for request_start, headers_received, first_token, last_token, and stream_end.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.streaming.logging import StreamLogger
|
||||
|
||||
logger = StreamLogger(verbose=True, metrics=True)
|
||||
logger.log_request_start(model="gpt-4o-mini")
|
||||
# ... streaming ...
|
||||
logger.log_first_token()
|
||||
# ... more tokens ...
|
||||
logger.log_stream_end()
|
||||
print(logger.get_metrics_summary())
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from .events import StreamMetrics, StreamEvent, StreamEventType
|
||||
|
||||
|
||||
class StreamLogger:
|
||||
"""
|
||||
Guarded logging for streaming events with timing metrics.
|
||||
|
||||
Only logs when verbose=True or metrics=True to ensure zero overhead
|
||||
when logging is disabled.
|
||||
|
||||
Attributes:
|
||||
verbose: Whether to log detailed streaming events
|
||||
metrics: Whether to collect and display timing metrics
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
verbose: bool = False,
|
||||
metrics: bool = False,
|
||||
logger: Optional[logging.Logger] = None
|
||||
):
|
||||
self.verbose = verbose
|
||||
self.metrics = metrics
|
||||
self._logger = logger or logging.getLogger(__name__)
|
||||
self._metrics = StreamMetrics() if metrics else None
|
||||
self._enabled = verbose or metrics
|
||||
|
||||
def log_request_start(
|
||||
self,
|
||||
model: Optional[str] = None,
|
||||
message_count: Optional[int] = None
|
||||
) -> None:
|
||||
"""Log when the streaming request is initiated."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
timestamp = time.perf_counter()
|
||||
if self._metrics:
|
||||
self._metrics.request_start = timestamp
|
||||
|
||||
if self.verbose:
|
||||
extra = f" model={model}" if model else ""
|
||||
extra += f" messages={message_count}" if message_count else ""
|
||||
self._logger.info(f"[STREAM] REQUEST_START{extra}")
|
||||
|
||||
def log_headers_received(self) -> None:
|
||||
"""Log when HTTP response headers are received."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
timestamp = time.perf_counter()
|
||||
if self._metrics:
|
||||
self._metrics.headers_received = timestamp
|
||||
|
||||
if self.verbose:
|
||||
self._logger.info("[STREAM] HEADERS_RECEIVED")
|
||||
|
||||
def log_first_token(self, content: Optional[str] = None) -> None:
|
||||
"""Log when the first content token is received (TTFT marker)."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
timestamp = time.perf_counter()
|
||||
if self._metrics:
|
||||
self._metrics.first_token = timestamp
|
||||
self._metrics.token_count = 1
|
||||
|
||||
if self.verbose:
|
||||
preview = content[:20] + "..." if content and len(content) > 20 else content
|
||||
self._logger.info(f"[STREAM] FIRST_TOKEN content={preview!r}")
|
||||
|
||||
def log_delta_text(self, content: Optional[str] = None) -> None:
|
||||
"""Log a text delta (only when verbose, not for every token)."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if self._metrics:
|
||||
self._metrics.token_count += 1
|
||||
|
||||
# Don't log every delta to avoid noise - just track metrics
|
||||
|
||||
def log_last_token(self) -> None:
|
||||
"""Log when the last content token is received."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
timestamp = time.perf_counter()
|
||||
if self._metrics:
|
||||
self._metrics.last_token = timestamp
|
||||
|
||||
if self.verbose:
|
||||
self._logger.info("[STREAM] LAST_TOKEN")
|
||||
|
||||
def log_stream_end(self, chunk_count: Optional[int] = None) -> None:
|
||||
"""Log when the stream ends."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
timestamp = time.perf_counter()
|
||||
if self._metrics:
|
||||
self._metrics.stream_end = timestamp
|
||||
|
||||
if self.verbose:
|
||||
extra = f" chunks={chunk_count}" if chunk_count else ""
|
||||
self._logger.info(f"[STREAM] STREAM_END{extra}")
|
||||
|
||||
def log_error(self, error: str) -> None:
|
||||
"""Log a streaming error."""
|
||||
if self.verbose:
|
||||
self._logger.error(f"[STREAM] ERROR: {error}")
|
||||
|
||||
def get_metrics(self) -> Optional[StreamMetrics]:
|
||||
"""Get the collected metrics (None if metrics not enabled)."""
|
||||
return self._metrics
|
||||
|
||||
def get_metrics_summary(self) -> str:
|
||||
"""Get a formatted summary of streaming metrics."""
|
||||
if not self._metrics:
|
||||
return "Metrics not enabled"
|
||||
return self._metrics.format_summary()
|
||||
|
||||
def get_metrics_dict(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get metrics as a dictionary."""
|
||||
if not self._metrics:
|
||||
return None
|
||||
return self._metrics.to_dict()
|
||||
|
||||
def update_from_event(self, event: StreamEvent) -> None:
|
||||
"""Update logger state from a StreamEvent."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if self._metrics:
|
||||
self._metrics.update_from_event(event)
|
||||
|
||||
if self.verbose:
|
||||
if event.type == StreamEventType.REQUEST_START:
|
||||
self.log_request_start()
|
||||
elif event.type == StreamEventType.HEADERS_RECEIVED:
|
||||
self.log_headers_received()
|
||||
elif event.type == StreamEventType.FIRST_TOKEN:
|
||||
self.log_first_token(event.content)
|
||||
elif event.type == StreamEventType.LAST_TOKEN:
|
||||
self.log_last_token()
|
||||
elif event.type == StreamEventType.STREAM_END:
|
||||
self.log_stream_end()
|
||||
elif event.type == StreamEventType.ERROR:
|
||||
self.log_error(event.error or "Unknown error")
|
||||
|
||||
|
||||
def create_logging_callback(
|
||||
verbose: bool = False,
|
||||
metrics: bool = False,
|
||||
logger: Optional[logging.Logger] = None
|
||||
) -> tuple:
|
||||
"""
|
||||
Create a StreamLogger and a callback function for use with streaming.
|
||||
|
||||
Args:
|
||||
verbose: Whether to log detailed events
|
||||
metrics: Whether to collect timing metrics
|
||||
logger: Optional custom logger
|
||||
|
||||
Returns:
|
||||
Tuple of (StreamLogger, callback_function)
|
||||
|
||||
Usage:
|
||||
stream_logger, callback = create_logging_callback(verbose=True, metrics=True)
|
||||
|
||||
# Pass callback to streaming method
|
||||
response = client.process_stream_response(
|
||||
...,
|
||||
stream_callback=callback,
|
||||
emit_events=True
|
||||
)
|
||||
|
||||
# After streaming, get metrics
|
||||
print(stream_logger.get_metrics_summary())
|
||||
"""
|
||||
stream_logger = StreamLogger(verbose=verbose, metrics=metrics, logger=logger)
|
||||
|
||||
def callback(event: StreamEvent) -> None:
|
||||
stream_logger.update_from_event(event)
|
||||
|
||||
return stream_logger, callback
|
||||
@@ -326,7 +326,7 @@ class Task:
|
||||
if hasattr(self.agent.llm_instance, 'model'):
|
||||
llm_model = self.agent.llm_instance.model
|
||||
else:
|
||||
llm_model = "gpt-5-nano" # Default fallback
|
||||
llm_model = "gpt-4o-mini" # Default fallback
|
||||
elif hasattr(self.agent, 'llm') and self.agent.llm:
|
||||
# For standard model strings
|
||||
llm_model = self.agent.llm
|
||||
|
||||
@@ -27,7 +27,7 @@ class GenerateCOT:
|
||||
def __init__(
|
||||
self,
|
||||
qa_pairs: Optional[Dict[str, str]] = None,
|
||||
model: str = "gpt-5-nano",
|
||||
model: str = "gpt-4o-mini",
|
||||
api_key: Optional[str] = None,
|
||||
max_attempts: int = 3,
|
||||
verbose: bool = True,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Trace Module for PraisonAI Agents.
|
||||
|
||||
Provides lightweight action tracing for the `output="actions"` mode.
|
||||
Shows agent lifecycle events, tool calls, and final output without
|
||||
verbose internal details.
|
||||
|
||||
Zero Performance Impact:
|
||||
- All imports are lazy loaded via __getattr__
|
||||
- NoOpSink is the default (zero overhead when not used)
|
||||
- Disabled emitter has near-zero overhead
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Simple usage - shows action trace
|
||||
agent = Agent(instructions="...", output="actions")
|
||||
agent.start("Do something")
|
||||
|
||||
# Advanced - capture to file
|
||||
from praisonaiagents.trace import ActionTraceConfig
|
||||
|
||||
agent = Agent(
|
||||
instructions="...",
|
||||
output="actions",
|
||||
trace=ActionTraceConfig(
|
||||
sink_type="jsonl",
|
||||
file_path="trace.jsonl",
|
||||
)
|
||||
)
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
# Core types
|
||||
"ActionEvent",
|
||||
"ActionEventType",
|
||||
"ActionTraceConfig",
|
||||
# Sinks
|
||||
"TraceSink",
|
||||
"NoOpSink",
|
||||
"ListSink",
|
||||
# Emitter
|
||||
"TraceEmitter",
|
||||
# Redaction
|
||||
"redact_dict",
|
||||
"REDACT_KEYS",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load module components to avoid import overhead."""
|
||||
if name in ("ActionEvent", "ActionEventType", "ActionTraceConfig"):
|
||||
from .protocol import ActionEvent, ActionEventType, ActionTraceConfig
|
||||
return locals()[name]
|
||||
|
||||
if name in ("TraceSink", "NoOpSink", "ListSink"):
|
||||
from .protocol import TraceSink, NoOpSink, ListSink
|
||||
return locals()[name]
|
||||
|
||||
if name == "TraceEmitter":
|
||||
from .protocol import TraceEmitter
|
||||
return TraceEmitter
|
||||
|
||||
if name in ("redact_dict", "REDACT_KEYS"):
|
||||
from .redact import redact_dict, REDACT_KEYS
|
||||
return locals()[name]
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,398 @@
|
||||
"""
|
||||
Action Trace Protocol for PraisonAI Agents.
|
||||
|
||||
Lightweight protocol for tracing agent actions. Uses only stdlib
|
||||
to ensure zero dependencies and minimal overhead.
|
||||
|
||||
Schema Version: 1.0
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
||||
|
||||
|
||||
# Schema version for forward compatibility
|
||||
SCHEMA_VERSION = "1.0"
|
||||
|
||||
|
||||
class ActionEventType(str, Enum):
|
||||
"""Types of action events."""
|
||||
AGENT_START = "agent_start"
|
||||
AGENT_END = "agent_end"
|
||||
TOOL_START = "tool_start"
|
||||
TOOL_END = "tool_end"
|
||||
ERROR = "error"
|
||||
OUTPUT = "output"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionEvent:
|
||||
"""
|
||||
Lightweight action trace event.
|
||||
|
||||
Represents a single action in the agent execution trace.
|
||||
Designed for minimal memory footprint and fast serialization.
|
||||
"""
|
||||
event_type: str
|
||||
timestamp: float
|
||||
schema_version: str = SCHEMA_VERSION
|
||||
agent_id: Optional[str] = None
|
||||
agent_name: Optional[str] = None
|
||||
tool_name: Optional[str] = None
|
||||
tool_args: Optional[Dict[str, Any]] = None
|
||||
tool_result_summary: Optional[str] = None
|
||||
duration_ms: Optional[float] = None
|
||||
status: Optional[str] = None # ok, error
|
||||
error_message: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary, excluding None values."""
|
||||
result = {
|
||||
"schema_version": self.schema_version,
|
||||
"event_type": self.event_type,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
# Add optional fields only if set
|
||||
if self.agent_id is not None:
|
||||
result["agent_id"] = self.agent_id
|
||||
if self.agent_name is not None:
|
||||
result["agent_name"] = self.agent_name
|
||||
if self.tool_name is not None:
|
||||
result["tool_name"] = self.tool_name
|
||||
if self.tool_args is not None:
|
||||
result["tool_args"] = self.tool_args
|
||||
if self.tool_result_summary is not None:
|
||||
result["tool_result_summary"] = self.tool_result_summary
|
||||
if self.duration_ms is not None:
|
||||
result["duration_ms"] = self.duration_ms
|
||||
if self.status is not None:
|
||||
result["status"] = self.status
|
||||
if self.error_message is not None:
|
||||
result["error_message"] = self.error_message
|
||||
if self.metadata:
|
||||
result["metadata"] = self.metadata
|
||||
|
||||
return result
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert to JSON string."""
|
||||
return json.dumps(self.to_dict(), default=str)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TraceSink(Protocol):
|
||||
"""
|
||||
Protocol for trace event sinks.
|
||||
|
||||
Implementations receive action events and handle them
|
||||
(e.g., print to stdout, write to file, send to server).
|
||||
"""
|
||||
|
||||
def emit(self, event: ActionEvent) -> None:
|
||||
"""Emit an action event."""
|
||||
...
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush any buffered events."""
|
||||
...
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the sink and release resources."""
|
||||
...
|
||||
|
||||
|
||||
class NoOpSink:
|
||||
"""
|
||||
No-operation sink that discards all events.
|
||||
|
||||
Used as the default sink when tracing is disabled.
|
||||
Has near-zero overhead.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def emit(self, event: ActionEvent) -> None:
|
||||
"""Discard the event."""
|
||||
pass
|
||||
|
||||
def flush(self) -> None:
|
||||
"""No-op."""
|
||||
pass
|
||||
|
||||
def close(self) -> None:
|
||||
"""No-op."""
|
||||
pass
|
||||
|
||||
|
||||
class ListSink:
|
||||
"""
|
||||
Sink that collects events in a list.
|
||||
|
||||
Useful for testing and programmatic access to trace events.
|
||||
"""
|
||||
|
||||
__slots__ = ("_events",)
|
||||
|
||||
def __init__(self):
|
||||
self._events: List[ActionEvent] = []
|
||||
|
||||
def emit(self, event: ActionEvent) -> None:
|
||||
"""Add event to the list."""
|
||||
self._events.append(event)
|
||||
|
||||
def flush(self) -> None:
|
||||
"""No-op for list sink."""
|
||||
pass
|
||||
|
||||
def close(self) -> None:
|
||||
"""No-op for list sink."""
|
||||
pass
|
||||
|
||||
def get_events(self) -> List[ActionEvent]:
|
||||
"""Get all collected events."""
|
||||
return self._events.copy()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all collected events."""
|
||||
self._events.clear()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionTraceConfig:
|
||||
"""
|
||||
Configuration for action tracing.
|
||||
|
||||
Usage:
|
||||
# Default (disabled)
|
||||
config = ActionTraceConfig()
|
||||
|
||||
# Enable with stdout
|
||||
config = ActionTraceConfig(enabled=True, sink_type="stdout")
|
||||
|
||||
# Enable with JSONL file
|
||||
config = ActionTraceConfig(
|
||||
enabled=True,
|
||||
sink_type="jsonl",
|
||||
file_path="trace.jsonl",
|
||||
)
|
||||
"""
|
||||
enabled: bool = True
|
||||
redact: bool = True
|
||||
compact: bool = False
|
||||
sink_type: str = "noop" # noop, stdout, jsonl, file
|
||||
file_path: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"enabled": self.enabled,
|
||||
"redact": self.redact,
|
||||
"compact": self.compact,
|
||||
"sink_type": self.sink_type,
|
||||
"file_path": self.file_path,
|
||||
}
|
||||
|
||||
|
||||
class TraceEmitter:
|
||||
"""
|
||||
Emitter for action trace events.
|
||||
|
||||
Provides a high-level API for emitting trace events.
|
||||
Handles redaction and sink management.
|
||||
|
||||
Usage:
|
||||
sink = ListSink()
|
||||
emitter = TraceEmitter(sink=sink)
|
||||
|
||||
emitter.agent_start("researcher")
|
||||
emitter.tool_start("search", {"query": "AI"})
|
||||
emitter.tool_end("search", duration_ms=100, status="ok")
|
||||
emitter.agent_end("researcher", duration_ms=500)
|
||||
"""
|
||||
|
||||
__slots__ = ("_sink", "_enabled", "_redact", "_agent_starts")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sink: Optional[TraceSink] = None,
|
||||
enabled: bool = True,
|
||||
redact: bool = True,
|
||||
):
|
||||
self._sink = sink or NoOpSink()
|
||||
self._enabled = enabled
|
||||
self._redact = redact
|
||||
self._agent_starts: Dict[str, float] = {} # Track agent start times
|
||||
|
||||
def _emit(self, event: ActionEvent) -> None:
|
||||
"""Internal emit with enabled check."""
|
||||
if not self._enabled:
|
||||
return
|
||||
self._sink.emit(event)
|
||||
|
||||
def _redact_args(self, args: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
"""Redact sensitive data from args if enabled."""
|
||||
if args is None or not self._redact:
|
||||
return args
|
||||
|
||||
from .redact import redact_dict
|
||||
return redact_dict(args)
|
||||
|
||||
def agent_start(
|
||||
self,
|
||||
agent_name: str,
|
||||
agent_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit agent start event."""
|
||||
ts = time.time()
|
||||
self._agent_starts[agent_name] = ts
|
||||
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.AGENT_START.value,
|
||||
timestamp=ts,
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def agent_end(
|
||||
self,
|
||||
agent_name: str,
|
||||
agent_id: Optional[str] = None,
|
||||
duration_ms: Optional[float] = None,
|
||||
status: str = "ok",
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit agent end event."""
|
||||
ts = time.time()
|
||||
|
||||
# Calculate duration if not provided
|
||||
if duration_ms is None and agent_name in self._agent_starts:
|
||||
start_ts = self._agent_starts.pop(agent_name, None)
|
||||
if start_ts:
|
||||
duration_ms = (ts - start_ts) * 1000
|
||||
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.AGENT_END.value,
|
||||
timestamp=ts,
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
duration_ms=duration_ms,
|
||||
status=status,
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def tool_start(
|
||||
self,
|
||||
tool_name: str,
|
||||
tool_args: Optional[Dict[str, Any]] = None,
|
||||
agent_name: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit tool start event."""
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.TOOL_START.value,
|
||||
timestamp=time.time(),
|
||||
tool_name=tool_name,
|
||||
tool_args=self._redact_args(tool_args),
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def tool_end(
|
||||
self,
|
||||
tool_name: str,
|
||||
duration_ms: float,
|
||||
status: str = "ok",
|
||||
result_summary: Optional[str] = None,
|
||||
agent_name: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
error_message: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit tool end event."""
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.TOOL_END.value,
|
||||
timestamp=time.time(),
|
||||
tool_name=tool_name,
|
||||
duration_ms=duration_ms,
|
||||
status=status,
|
||||
tool_result_summary=result_summary,
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
error_message=error_message,
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def error(
|
||||
self,
|
||||
message: str,
|
||||
tool_name: Optional[str] = None,
|
||||
agent_name: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit error event."""
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.ERROR.value,
|
||||
timestamp=time.time(),
|
||||
error_message=message,
|
||||
tool_name=tool_name,
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
status="error",
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def output(
|
||||
self,
|
||||
content: str,
|
||||
agent_name: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit final output event."""
|
||||
# Truncate long output for summary
|
||||
summary = content[:500] + "..." if len(content) > 500 else content
|
||||
|
||||
self._emit(ActionEvent(
|
||||
event_type=ActionEventType.OUTPUT.value,
|
||||
timestamp=time.time(),
|
||||
tool_result_summary=summary,
|
||||
agent_name=agent_name,
|
||||
agent_id=agent_id,
|
||||
metadata=metadata or {},
|
||||
))
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush the sink."""
|
||||
self._sink.flush()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the sink."""
|
||||
self._sink.close()
|
||||
|
||||
|
||||
# Global default emitter (NoOp by default)
|
||||
_default_emitter: Optional[TraceEmitter] = None
|
||||
|
||||
|
||||
def get_default_emitter() -> TraceEmitter:
|
||||
"""Get the default trace emitter."""
|
||||
global _default_emitter
|
||||
if _default_emitter is None:
|
||||
_default_emitter = TraceEmitter(sink=NoOpSink(), enabled=False)
|
||||
return _default_emitter
|
||||
|
||||
|
||||
def set_default_emitter(emitter: TraceEmitter) -> None:
|
||||
"""Set the default trace emitter."""
|
||||
global _default_emitter
|
||||
_default_emitter = emitter
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Redaction utilities for action trace.
|
||||
|
||||
Provides deterministic redaction of sensitive data in tool arguments
|
||||
and results. Uses only stdlib for zero dependencies.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Set
|
||||
import re
|
||||
|
||||
# Keys that should always be redacted (case-insensitive matching)
|
||||
REDACT_KEYS: Set[str] = {
|
||||
# API keys and tokens
|
||||
"api_key",
|
||||
"apikey",
|
||||
"api-key",
|
||||
"secret",
|
||||
"secret_key",
|
||||
"secretkey",
|
||||
"private_key",
|
||||
"privatekey",
|
||||
# Authentication
|
||||
"password",
|
||||
"passwd",
|
||||
"pwd",
|
||||
"token",
|
||||
"bearer",
|
||||
"authorization",
|
||||
"auth",
|
||||
"auth_token",
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
# Session and cookies
|
||||
"cookie",
|
||||
"session",
|
||||
"session_id",
|
||||
"sessionid",
|
||||
"credential",
|
||||
"credentials",
|
||||
# Database
|
||||
"connection_string",
|
||||
"connectionstring",
|
||||
"database_url",
|
||||
"db_url",
|
||||
"dburl",
|
||||
"db_password",
|
||||
"db_pass",
|
||||
# Cloud provider specific
|
||||
"aws_secret",
|
||||
"aws_secret_access_key",
|
||||
"azure_key",
|
||||
"gcp_key",
|
||||
"openai_api_key",
|
||||
"anthropic_api_key",
|
||||
}
|
||||
|
||||
# Compiled regex for case-insensitive matching
|
||||
_REDACT_PATTERN = re.compile(
|
||||
r"(" + "|".join(re.escape(k) for k in REDACT_KEYS) + r")",
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
REDACTED_VALUE = "[REDACTED]"
|
||||
|
||||
|
||||
def _should_redact(key: str) -> bool:
|
||||
"""Check if a key should be redacted."""
|
||||
# Normalize key for comparison
|
||||
normalized = key.lower().replace("-", "_").replace(" ", "_")
|
||||
|
||||
# Check exact match first
|
||||
if normalized in REDACT_KEYS:
|
||||
return True
|
||||
|
||||
# Check if any redact key is contained in the normalized key
|
||||
for redact_key in REDACT_KEYS:
|
||||
if redact_key in normalized:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def redact_dict(data: Dict[str, Any], enabled: bool = True) -> Dict[str, Any]:
|
||||
"""
|
||||
Redact sensitive values from a dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary to redact
|
||||
enabled: If False, returns data unchanged
|
||||
|
||||
Returns:
|
||||
New dictionary with sensitive values redacted
|
||||
"""
|
||||
if not enabled:
|
||||
return data
|
||||
|
||||
if not data:
|
||||
return {}
|
||||
|
||||
return _redact_value(data)
|
||||
|
||||
|
||||
def _redact_value(value: Any) -> Any:
|
||||
"""Recursively redact sensitive values."""
|
||||
if isinstance(value, dict):
|
||||
return {k: _redact_key_value(k, v) for k, v in value.items()}
|
||||
elif isinstance(value, list):
|
||||
return [_redact_value(item) for item in value]
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def _redact_key_value(key: str, value: Any) -> Any:
|
||||
"""Redact a value if its key is sensitive."""
|
||||
if _should_redact(key):
|
||||
return REDACTED_VALUE
|
||||
|
||||
# Recursively process nested structures
|
||||
return _redact_value(value)
|
||||
|
||||
|
||||
def redact_string(text: str, enabled: bool = True) -> str:
|
||||
"""
|
||||
Redact potential secrets from a string.
|
||||
|
||||
This is a best-effort function that looks for common patterns
|
||||
like "api_key=xxx" or "password: xxx" in strings.
|
||||
|
||||
Args:
|
||||
text: String to redact
|
||||
enabled: If False, returns text unchanged
|
||||
|
||||
Returns:
|
||||
String with potential secrets redacted
|
||||
"""
|
||||
if not enabled or not text:
|
||||
return text
|
||||
|
||||
# Pattern for key=value or key: value
|
||||
patterns = [
|
||||
(r'(["\']?)(' + '|'.join(re.escape(k) for k in REDACT_KEYS) + r')(["\']?\s*[:=]\s*["\']?)([^"\'\s,}\]]+)',
|
||||
r'\1\2\3[REDACTED]'),
|
||||
]
|
||||
|
||||
result = text
|
||||
for pattern, replacement in patterns:
|
||||
result = re.sub(pattern, replacement, result, flags=re.IGNORECASE)
|
||||
|
||||
return result
|
||||
@@ -12,7 +12,9 @@ Provides dataclasses for consolidated workflow feature configuration:
|
||||
- WorkflowStepRoutingConfig: Step routing/branching settings
|
||||
|
||||
All configs follow the workflow-centric pattern with precedence:
|
||||
Instance > Config > String > Bool > Default
|
||||
Instance > Config > Dict > Array > String > Bool > Default
|
||||
|
||||
Uses the CANONICAL resolver from param_resolver.py for DRY resolution.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import Workflow, WorkflowOutputConfig
|
||||
@@ -263,7 +265,7 @@ class WorkflowStepRoutingConfig:
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Precedence Resolution Helper
|
||||
# Precedence Resolution Helper - Uses CANONICAL resolver
|
||||
# =============================================================================
|
||||
|
||||
def resolve_param(
|
||||
@@ -275,245 +277,173 @@ def resolve_param(
|
||||
array_mode: Optional[str] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Resolve a consolidated parameter following precedence rules:
|
||||
Instance > Config > Array > String > Bool > Default
|
||||
Resolve a consolidated parameter following precedence rules.
|
||||
|
||||
Args:
|
||||
value: The parameter value (can be Instance, Config, Array, String, Bool, or None)
|
||||
config_class: The expected config dataclass type
|
||||
presets: Dict mapping preset strings to config instances or dicts
|
||||
default: Default value if None/unset
|
||||
instance_check: Optional function to check if value is an instance
|
||||
array_mode: How to handle arrays ("preset_override", "step_names", "passthrough")
|
||||
|
||||
Returns:
|
||||
Resolved config object or value
|
||||
|
||||
Example:
|
||||
# Resolve output param
|
||||
output_config = resolve_param(
|
||||
value=output,
|
||||
config_class=WorkflowOutputConfig,
|
||||
presets={
|
||||
"silent": WorkflowOutputConfig(verbose=False, stream=False),
|
||||
"verbose": WorkflowOutputConfig(verbose=True, stream=True),
|
||||
},
|
||||
default=WorkflowOutputConfig(),
|
||||
)
|
||||
|
||||
# With array override
|
||||
output_config = resolve_param(
|
||||
value=["verbose", {"stream": False}],
|
||||
config_class=WorkflowOutputConfig,
|
||||
presets=presets,
|
||||
array_mode="preset_override",
|
||||
)
|
||||
DEPRECATED: This is a wrapper around the canonical resolver for backward compatibility.
|
||||
New code should use `from ..config.param_resolver import resolve` directly.
|
||||
|
||||
Precedence: Instance > Config > Dict > Array > String > Bool > Default
|
||||
"""
|
||||
# None/unset -> Default
|
||||
if value is None:
|
||||
return default
|
||||
# Import canonical resolver
|
||||
from ..config.param_resolver import resolve as canonical_resolve, ArrayMode
|
||||
|
||||
# Instance check (e.g., pre-built manager object)
|
||||
if instance_check and instance_check(value):
|
||||
return value
|
||||
# Map string array_mode to ArrayMode enum
|
||||
array_mode_map = {
|
||||
"preset_override": ArrayMode.PRESET_OVERRIDE,
|
||||
"step_names": ArrayMode.STEP_NAMES,
|
||||
"passthrough": ArrayMode.PASSTHROUGH,
|
||||
}
|
||||
canonical_array_mode = array_mode_map.get(array_mode) if array_mode else None
|
||||
|
||||
# Config dataclass
|
||||
if isinstance(value, config_class):
|
||||
return value
|
||||
# Convert presets from config instances to dicts for canonical resolver
|
||||
presets_dict = None
|
||||
if presets:
|
||||
presets_dict = {}
|
||||
for key, val in presets.items():
|
||||
if hasattr(val, 'to_dict'):
|
||||
presets_dict[key] = val.to_dict()
|
||||
elif hasattr(val, '__dataclass_fields__'):
|
||||
from dataclasses import asdict
|
||||
presets_dict[key] = asdict(val)
|
||||
else:
|
||||
presets_dict[key] = val
|
||||
|
||||
# Dict -> convert to config
|
||||
if isinstance(value, dict):
|
||||
try:
|
||||
return config_class(**value)
|
||||
except TypeError:
|
||||
return default
|
||||
|
||||
# Array handling (NEW - before string to respect precedence)
|
||||
if isinstance(value, (list, tuple)):
|
||||
if not value:
|
||||
return None # Empty array = disabled
|
||||
|
||||
# Passthrough mode - return list as-is
|
||||
if array_mode == "passthrough":
|
||||
return list(value)
|
||||
|
||||
# Step names mode - for context/routing
|
||||
if array_mode == "step_names":
|
||||
# All strings = step names
|
||||
if all(isinstance(item, str) for item in value):
|
||||
return value # Return list directly for step names
|
||||
|
||||
# Preset override mode - [preset, {overrides}]
|
||||
if array_mode == "preset_override" and presets:
|
||||
first = value[0]
|
||||
if isinstance(first, str) and first in presets:
|
||||
# Get base config from preset
|
||||
preset_value = presets[first]
|
||||
if isinstance(preset_value, dict):
|
||||
base_config = config_class(**preset_value)
|
||||
elif isinstance(preset_value, config_class):
|
||||
base_config = preset_value
|
||||
else:
|
||||
base_config = config_class()
|
||||
|
||||
# Apply overrides if present
|
||||
if len(value) >= 2 and isinstance(value[-1], dict):
|
||||
overrides = value[-1]
|
||||
# Merge base with overrides
|
||||
from dataclasses import asdict
|
||||
base_dict = asdict(base_config)
|
||||
base_dict.update(overrides)
|
||||
return config_class(**base_dict)
|
||||
|
||||
return base_config
|
||||
|
||||
# Default: treat first item as value
|
||||
if len(value) == 1:
|
||||
return resolve_param(value[0], config_class, presets, default, instance_check)
|
||||
|
||||
return default
|
||||
|
||||
# String -> preset lookup
|
||||
if isinstance(value, str) and presets:
|
||||
preset_value = presets.get(value)
|
||||
if preset_value is not None:
|
||||
if isinstance(preset_value, dict):
|
||||
return config_class(**preset_value)
|
||||
return preset_value
|
||||
return default
|
||||
|
||||
# Bool -> True enables defaults, False disables
|
||||
if isinstance(value, bool):
|
||||
if value:
|
||||
return config_class() if config_class else True
|
||||
return None # Disabled
|
||||
|
||||
# Fallback to default
|
||||
return default
|
||||
return canonical_resolve(
|
||||
value=value,
|
||||
param_name="workflow_param",
|
||||
config_class=config_class,
|
||||
presets=presets_dict,
|
||||
default=default,
|
||||
instance_check=instance_check,
|
||||
array_mode=canonical_array_mode,
|
||||
)
|
||||
|
||||
|
||||
def resolve_output_config(value: Any) -> WorkflowOutputConfig:
|
||||
"""Resolve output parameter to WorkflowOutputConfig.
|
||||
"""Resolve output parameter to WorkflowOutputConfig using canonical resolver.
|
||||
|
||||
Supports: None, str preset, list [preset, overrides], Config, dict
|
||||
"""
|
||||
presets = {
|
||||
"silent": WorkflowOutputConfig(verbose=False, stream=False),
|
||||
"minimal": WorkflowOutputConfig(verbose=False, stream=True),
|
||||
"normal": WorkflowOutputConfig(verbose=False, stream=True),
|
||||
"verbose": WorkflowOutputConfig(verbose=True, stream=True),
|
||||
"debug": WorkflowOutputConfig(verbose=True, stream=True),
|
||||
}
|
||||
result = resolve_param(
|
||||
value, WorkflowOutputConfig, presets, WorkflowOutputConfig(),
|
||||
array_mode="preset_override"
|
||||
from ..config.param_resolver import resolve, ArrayMode
|
||||
from ..config.presets import WORKFLOW_OUTPUT_PRESETS
|
||||
|
||||
result = resolve(
|
||||
value=value,
|
||||
param_name="output",
|
||||
config_class=WorkflowOutputConfig,
|
||||
presets=WORKFLOW_OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
default=WorkflowOutputConfig(),
|
||||
)
|
||||
return result if result else WorkflowOutputConfig(verbose=False, stream=False)
|
||||
|
||||
|
||||
def resolve_planning_config(value: Any) -> Optional[WorkflowPlanningConfig]:
|
||||
"""Resolve planning parameter to WorkflowPlanningConfig or None."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return WorkflowPlanningConfig(enabled=True)
|
||||
if isinstance(value, WorkflowPlanningConfig):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return WorkflowPlanningConfig(**value)
|
||||
if isinstance(value, str):
|
||||
# String is treated as LLM model name
|
||||
return WorkflowPlanningConfig(enabled=True, llm=value)
|
||||
return None
|
||||
"""Resolve planning parameter to WorkflowPlanningConfig using canonical resolver."""
|
||||
from ..config.param_resolver import resolve, ArrayMode
|
||||
|
||||
result = resolve(
|
||||
value=value,
|
||||
param_name="planning",
|
||||
config_class=WorkflowPlanningConfig,
|
||||
string_mode="llm_model",
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
default=None,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def resolve_memory_config(value: Any) -> Optional[WorkflowMemoryConfig]:
|
||||
"""Resolve memory parameter to WorkflowMemoryConfig or None."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return WorkflowMemoryConfig()
|
||||
if isinstance(value, WorkflowMemoryConfig):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return WorkflowMemoryConfig(**value)
|
||||
# Instance check - if it has search/add methods, it's a memory instance
|
||||
if hasattr(value, 'search') and hasattr(value, 'add'):
|
||||
return value # Return instance directly
|
||||
return None
|
||||
"""Resolve memory parameter to WorkflowMemoryConfig using canonical resolver."""
|
||||
from ..config.param_resolver import resolve, ArrayMode
|
||||
from ..config.presets import MEMORY_PRESETS, MEMORY_URL_SCHEMES
|
||||
|
||||
result = resolve(
|
||||
value=value,
|
||||
param_name="memory",
|
||||
config_class=WorkflowMemoryConfig,
|
||||
presets=MEMORY_PRESETS,
|
||||
url_schemes=MEMORY_URL_SCHEMES,
|
||||
instance_check=lambda v: hasattr(v, 'search') and hasattr(v, 'add'),
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST,
|
||||
default=None,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def resolve_hooks_config(value: Any) -> Optional[WorkflowHooksConfig]:
|
||||
"""Resolve hooks parameter to WorkflowHooksConfig or None."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, WorkflowHooksConfig):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return WorkflowHooksConfig(**value)
|
||||
return None
|
||||
"""Resolve hooks parameter to WorkflowHooksConfig using canonical resolver."""
|
||||
from ..config.param_resolver import resolve, ArrayMode
|
||||
|
||||
result = resolve(
|
||||
value=value,
|
||||
param_name="hooks",
|
||||
config_class=WorkflowHooksConfig,
|
||||
array_mode=ArrayMode.PASSTHROUGH,
|
||||
default=None,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def resolve_step_context_config(value: Any) -> Optional[WorkflowStepContextConfig]:
|
||||
"""Resolve step context parameter."""
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if value is True:
|
||||
return WorkflowStepContextConfig()
|
||||
if isinstance(value, WorkflowStepContextConfig):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return WorkflowStepContextConfig(**value)
|
||||
if isinstance(value, list):
|
||||
# List of step names
|
||||
return WorkflowStepContextConfig(from_steps=value)
|
||||
return None
|
||||
"""Resolve step context parameter using canonical resolver."""
|
||||
from ..config.param_resolver import resolve, ArrayMode
|
||||
|
||||
result = resolve(
|
||||
value=value,
|
||||
param_name="context",
|
||||
config_class=WorkflowStepContextConfig,
|
||||
array_mode=ArrayMode.STEP_NAMES,
|
||||
default=None,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def resolve_step_output_config(value: Any) -> Optional[WorkflowStepOutputConfig]:
|
||||
"""Resolve step output parameter."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, WorkflowStepOutputConfig):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return WorkflowStepOutputConfig(**value)
|
||||
if isinstance(value, str):
|
||||
# String is treated as output file
|
||||
"""Resolve step output parameter using canonical resolver."""
|
||||
from ..config.param_resolver import resolve
|
||||
|
||||
result = resolve(
|
||||
value=value,
|
||||
param_name="output",
|
||||
config_class=WorkflowStepOutputConfig,
|
||||
string_mode="path_as_source", # String treated as output file
|
||||
default=None,
|
||||
)
|
||||
# Handle string -> file conversion for backward compatibility
|
||||
if isinstance(value, str) and result is None:
|
||||
return WorkflowStepOutputConfig(file=value)
|
||||
return None
|
||||
return result
|
||||
|
||||
|
||||
def resolve_step_execution_config(value: Any) -> WorkflowStepExecutionConfig:
|
||||
"""Resolve step execution parameter."""
|
||||
presets = {
|
||||
"fast": WorkflowStepExecutionConfig(max_retries=1, quality_check=False),
|
||||
"balanced": WorkflowStepExecutionConfig(max_retries=3, quality_check=True),
|
||||
"thorough": WorkflowStepExecutionConfig(max_retries=5, quality_check=True),
|
||||
}
|
||||
if value is None:
|
||||
return WorkflowStepExecutionConfig()
|
||||
if isinstance(value, WorkflowStepExecutionConfig):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return WorkflowStepExecutionConfig(**value)
|
||||
if isinstance(value, str) and value in presets:
|
||||
return presets[value]
|
||||
return WorkflowStepExecutionConfig()
|
||||
"""Resolve step execution parameter using canonical resolver."""
|
||||
from ..config.param_resolver import resolve, ArrayMode
|
||||
from ..config.presets import WORKFLOW_STEP_EXECUTION_PRESETS
|
||||
|
||||
result = resolve(
|
||||
value=value,
|
||||
param_name="execution",
|
||||
config_class=WorkflowStepExecutionConfig,
|
||||
presets=WORKFLOW_STEP_EXECUTION_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
default=WorkflowStepExecutionConfig(),
|
||||
)
|
||||
return result if result else WorkflowStepExecutionConfig()
|
||||
|
||||
|
||||
def resolve_step_routing_config(value: Any) -> Optional[WorkflowStepRoutingConfig]:
|
||||
"""Resolve step routing parameter."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, WorkflowStepRoutingConfig):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return WorkflowStepRoutingConfig(**value)
|
||||
if isinstance(value, list):
|
||||
# List of next step names
|
||||
return WorkflowStepRoutingConfig(next_steps=value)
|
||||
return None
|
||||
"""Resolve step routing parameter using canonical resolver."""
|
||||
from ..config.param_resolver import resolve, ArrayMode
|
||||
|
||||
result = resolve(
|
||||
value=value,
|
||||
param_name="routing",
|
||||
config_class=WorkflowStepRoutingConfig,
|
||||
array_mode=ArrayMode.STEP_NAMES,
|
||||
default=None,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -231,20 +231,33 @@ class WorkflowStep:
|
||||
loop_over: Optional[str] = None # Variable name to iterate over (e.g., "items")
|
||||
loop_var: str = "item" # Variable name for current item in loop
|
||||
|
||||
# Guardrail (like process="workflow")
|
||||
guardrail: Optional[Callable[['StepResult'], Tuple[bool, Any]]] = None # Returns (is_valid, feedback)
|
||||
# Guardrails (standardized plural naming)
|
||||
# Accepts: callable, str (LLM prompt), GuardrailConfig, or list [preset, overrides]
|
||||
guardrails: Optional[Any] = None # Union[Callable, str, GuardrailConfig] - validation
|
||||
|
||||
# Backward compatibility alias (deprecated - use guardrails instead)
|
||||
guardrail: Optional[Callable[['StepResult'], Tuple[bool, Any]]] = None # DEPRECATED: use guardrails
|
||||
|
||||
# Image handling (migrated from Task)
|
||||
images: Optional[List[str]] = None # Image paths/URLs for vision tasks
|
||||
|
||||
# ============================================================
|
||||
# CONSOLIDATED FEATURE PARAMS (workflow-centric API)
|
||||
# Precedence: Instance > Config > String > Bool > Default
|
||||
# Precedence: Instance > Config > Array > Dict > String > Bool > Default
|
||||
# ============================================================
|
||||
context: Optional[Any] = None # Union[bool, List[str], WorkflowStepContextConfig] - context handling
|
||||
output: Optional[Any] = None # Union[str, WorkflowStepOutputConfig] - output handling
|
||||
execution: Optional[Any] = None # Union[str, WorkflowStepExecutionConfig] - execution control
|
||||
routing: Optional[Any] = None # Union[List[str], WorkflowStepRoutingConfig] - branching
|
||||
# NEW: Agent-like consolidated params for feature parity
|
||||
autonomy: Optional[Any] = None # Union[bool, AutonomyConfig] - agent autonomy
|
||||
knowledge: Optional[Any] = None # Union[bool, List[str], KnowledgeConfig] - RAG
|
||||
web: Optional[Any] = None # Union[bool, WebConfig] - web search/fetch
|
||||
reflection: Optional[Any] = None # Union[bool, ReflectionConfig] - self-reflection
|
||||
memory: Optional[Any] = None # Union[bool, MemoryConfig] - step-level memory
|
||||
planning: Optional[Any] = None # Union[bool, PlanningConfig] - step-level planning
|
||||
hooks: Optional[Any] = None # Union[List, HooksConfig] - step lifecycle hooks
|
||||
caching: Optional[Any] = None # Union[bool, CachingConfig] - step-level caching
|
||||
|
||||
# Status tracking
|
||||
status: str = "pending" # pending, running, completed, failed, skipped
|
||||
@@ -264,6 +277,15 @@ class WorkflowStep:
|
||||
_on_error: str = field(default="stop", repr=False)
|
||||
_next_steps: Optional[List[str]] = field(default=None, repr=False)
|
||||
_branch_condition: Optional[Dict[str, List[str]]] = field(default=None, repr=False)
|
||||
# NEW: Resolved configs for agent-like params
|
||||
_autonomy_config: Optional[Any] = field(default=None, repr=False)
|
||||
_knowledge_config: Optional[Any] = field(default=None, repr=False)
|
||||
_web_config: Optional[Any] = field(default=None, repr=False)
|
||||
_reflection_config: Optional[Any] = field(default=None, repr=False)
|
||||
_memory_config: Optional[Any] = field(default=None, repr=False)
|
||||
_planning_config: Optional[Any] = field(default=None, repr=False)
|
||||
_hooks_config: Optional[Any] = field(default=None, repr=False)
|
||||
_caching_config: Optional[Any] = field(default=None, repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve consolidated params to internal values."""
|
||||
@@ -272,6 +294,18 @@ class WorkflowStep:
|
||||
resolve_step_execution_config, resolve_step_routing_config,
|
||||
)
|
||||
|
||||
# Handle guardrails/guardrail compatibility
|
||||
# guardrails (plural) takes precedence; guardrail (singular) is deprecated
|
||||
if self.guardrails is None and self.guardrail is not None:
|
||||
# Use deprecated guardrail value, map to guardrails
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"WorkflowStep 'guardrail' parameter is deprecated. Use 'guardrails' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
object.__setattr__(self, 'guardrails', self.guardrail)
|
||||
|
||||
# Resolve context param
|
||||
ctx_cfg = resolve_step_context_config(self.context)
|
||||
if ctx_cfg:
|
||||
@@ -300,6 +334,106 @@ class WorkflowStep:
|
||||
if route_cfg:
|
||||
self._next_steps = route_cfg.next_steps
|
||||
self._branch_condition = route_cfg.branches
|
||||
|
||||
# Resolve NEW agent-like params using canonical resolver
|
||||
from ..config.param_resolver import resolve, ArrayMode
|
||||
from ..config.presets import (
|
||||
WEB_PRESETS, REFLECTION_PRESETS, AUTONOMY_PRESETS, KNOWLEDGE_PRESETS,
|
||||
)
|
||||
try:
|
||||
from ..config.feature_configs import (
|
||||
AutonomyConfig, KnowledgeConfig, WebConfig, ReflectionConfig,
|
||||
)
|
||||
except ImportError:
|
||||
AutonomyConfig = KnowledgeConfig = WebConfig = ReflectionConfig = None
|
||||
|
||||
# Resolve autonomy
|
||||
if AutonomyConfig and self.autonomy is not None:
|
||||
self._autonomy_config = resolve(
|
||||
value=self.autonomy, param_name="autonomy",
|
||||
config_class=AutonomyConfig, presets=AUTONOMY_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Resolve knowledge
|
||||
if KnowledgeConfig and self.knowledge is not None:
|
||||
self._knowledge_config = resolve(
|
||||
value=self.knowledge, param_name="knowledge",
|
||||
config_class=KnowledgeConfig, presets=KNOWLEDGE_PRESETS,
|
||||
array_mode=ArrayMode.SOURCES, default=None,
|
||||
)
|
||||
|
||||
# Resolve web
|
||||
if WebConfig and self.web is not None:
|
||||
self._web_config = resolve(
|
||||
value=self.web, param_name="web",
|
||||
config_class=WebConfig, presets=WEB_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Resolve reflection
|
||||
if ReflectionConfig and self.reflection is not None:
|
||||
self._reflection_config = resolve(
|
||||
value=self.reflection, param_name="reflection",
|
||||
config_class=ReflectionConfig, presets=REFLECTION_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Resolve memory (NEW for feature parity)
|
||||
try:
|
||||
from ..config.feature_configs import MemoryConfig
|
||||
from ..config.presets import MEMORY_PRESETS
|
||||
except ImportError:
|
||||
MemoryConfig = None
|
||||
MEMORY_PRESETS = {}
|
||||
if MemoryConfig and self.memory is not None:
|
||||
self._memory_config = resolve(
|
||||
value=self.memory, param_name="memory",
|
||||
config_class=MemoryConfig, presets=MEMORY_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Resolve planning (NEW for feature parity)
|
||||
try:
|
||||
from ..config.feature_configs import PlanningConfig
|
||||
from ..config.presets import PLANNING_PRESETS
|
||||
except ImportError:
|
||||
PlanningConfig = None
|
||||
PLANNING_PRESETS = {}
|
||||
if PlanningConfig and self.planning is not None:
|
||||
self._planning_config = resolve(
|
||||
value=self.planning, param_name="planning",
|
||||
config_class=PlanningConfig, presets=PLANNING_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Resolve hooks (NEW for feature parity)
|
||||
try:
|
||||
from ..config.feature_configs import HooksConfig
|
||||
except ImportError:
|
||||
HooksConfig = None
|
||||
if HooksConfig and self.hooks is not None:
|
||||
# Hooks don't have presets, just config class
|
||||
if isinstance(self.hooks, list):
|
||||
self._hooks_config = HooksConfig(callbacks=self.hooks)
|
||||
elif hasattr(self.hooks, 'callbacks'):
|
||||
self._hooks_config = self.hooks
|
||||
else:
|
||||
self._hooks_config = self.hooks
|
||||
|
||||
# Resolve caching (NEW for feature parity)
|
||||
try:
|
||||
from ..config.feature_configs import CachingConfig
|
||||
from ..config.presets import CACHING_PRESETS
|
||||
except ImportError:
|
||||
CachingConfig = None
|
||||
CACHING_PRESETS = {}
|
||||
if CachingConfig and self.caching is not None:
|
||||
self._caching_config = resolve(
|
||||
value=self.caching, param_name="caching",
|
||||
config_class=CachingConfig, presets=CACHING_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Property accessors for backward compatibility
|
||||
@property
|
||||
@@ -423,13 +557,22 @@ class Workflow:
|
||||
|
||||
# ============================================================
|
||||
# CONSOLIDATED FEATURE PARAMS (workflow-centric API)
|
||||
# Precedence: Instance > Config > String > Bool > Default
|
||||
# Precedence: Instance > Config > Array > Dict > String > Bool > Default
|
||||
# Workflow is "Agents in an organised way" - supports same params as Agents
|
||||
# ============================================================
|
||||
output: Optional[Any] = None # Union[str, WorkflowOutputConfig] - verbose/stream
|
||||
planning: Optional[Any] = False # Union[bool, str, WorkflowPlanningConfig] - planning mode
|
||||
memory: Optional[Any] = None # Union[bool, WorkflowMemoryConfig, instance] - memory
|
||||
hooks: Optional[Any] = None # WorkflowHooksConfig - lifecycle callbacks
|
||||
context: Optional[Any] = False # Union[bool, ManagerConfig, ContextManager] - context management
|
||||
# NEW: Agent-like consolidated params for feature parity
|
||||
autonomy: Optional[Any] = None # Union[bool, AutonomyConfig] - agent autonomy
|
||||
knowledge: Optional[Any] = None # Union[bool, List[str], KnowledgeConfig] - RAG
|
||||
guardrails: Optional[Any] = None # Union[bool, Callable, GuardrailConfig] - validation
|
||||
web: Optional[Any] = None # Union[bool, WebConfig] - web search/fetch
|
||||
reflection: Optional[Any] = None # Union[bool, ReflectionConfig] - self-reflection
|
||||
execution: Optional[Any] = None # Union[str, ExecutionConfig] - execution control
|
||||
caching: Optional[Any] = None # Union[bool, CachingConfig] - caching
|
||||
|
||||
# Status tracking
|
||||
status: str = "not_started" # not_started, running, completed, failed
|
||||
@@ -445,6 +588,12 @@ class Workflow:
|
||||
_hooks_config: Optional[Any] = field(default=None, repr=False)
|
||||
_context_manager: Optional[Any] = field(default=None, repr=False)
|
||||
_context_manager_initialized: bool = field(default=False, repr=False)
|
||||
# NEW: Resolved configs for agent-like params (for propagation to steps/agents)
|
||||
_autonomy_config: Optional[Any] = field(default=None, repr=False)
|
||||
_knowledge_config: Optional[Any] = field(default=None, repr=False)
|
||||
_guardrails_config: Optional[Any] = field(default=None, repr=False)
|
||||
_web_config: Optional[Any] = field(default=None, repr=False)
|
||||
_reflection_config: Optional[Any] = field(default=None, repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Resolve consolidated params to internal values."""
|
||||
@@ -474,6 +623,62 @@ class Workflow:
|
||||
|
||||
# Resolve hooks param
|
||||
self._hooks_config = resolve_hooks_config(self.hooks)
|
||||
|
||||
# Resolve NEW agent-like params using canonical resolver
|
||||
# These are stored for propagation to steps/agents
|
||||
from ..config.param_resolver import resolve, ArrayMode
|
||||
from ..config.presets import (
|
||||
WEB_PRESETS, REFLECTION_PRESETS, GUARDRAIL_PRESETS,
|
||||
AUTONOMY_PRESETS, KNOWLEDGE_PRESETS,
|
||||
)
|
||||
try:
|
||||
from ..config.feature_configs import (
|
||||
AutonomyConfig, KnowledgeConfig, GuardrailConfig,
|
||||
WebConfig, ReflectionConfig,
|
||||
)
|
||||
except ImportError:
|
||||
AutonomyConfig = KnowledgeConfig = GuardrailConfig = None
|
||||
WebConfig = ReflectionConfig = None
|
||||
|
||||
# Resolve autonomy
|
||||
if AutonomyConfig and self.autonomy is not None:
|
||||
self._autonomy_config = resolve(
|
||||
value=self.autonomy, param_name="autonomy",
|
||||
config_class=AutonomyConfig, presets=AUTONOMY_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Resolve knowledge
|
||||
if KnowledgeConfig and self.knowledge is not None:
|
||||
self._knowledge_config = resolve(
|
||||
value=self.knowledge, param_name="knowledge",
|
||||
config_class=KnowledgeConfig, presets=KNOWLEDGE_PRESETS,
|
||||
array_mode=ArrayMode.SOURCES, default=None,
|
||||
)
|
||||
|
||||
# Resolve guardrails
|
||||
if GuardrailConfig and self.guardrails is not None:
|
||||
self._guardrails_config = resolve(
|
||||
value=self.guardrails, param_name="guardrails",
|
||||
config_class=GuardrailConfig, presets=GUARDRAIL_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Resolve web
|
||||
if WebConfig and self.web is not None:
|
||||
self._web_config = resolve(
|
||||
value=self.web, param_name="web",
|
||||
config_class=WebConfig, presets=WEB_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Resolve reflection
|
||||
if ReflectionConfig and self.reflection is not None:
|
||||
self._reflection_config = resolve(
|
||||
value=self.reflection, param_name="reflection",
|
||||
config_class=ReflectionConfig, presets=REFLECTION_PRESETS,
|
||||
default=None,
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Detailed profiler to identify the exact source of streaming delay.
|
||||
Traces every step from agent.start() to HTTP request.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import functools
|
||||
|
||||
os.environ['LOGLEVEL'] = 'DEBUG'
|
||||
|
||||
# Microsecond precision timestamps
|
||||
def ts():
|
||||
return time.perf_counter()
|
||||
|
||||
start_time = ts()
|
||||
|
||||
def log(msg):
|
||||
elapsed = (ts() - start_time) * 1000
|
||||
print(f"[{elapsed:8.2f}ms] {msg}")
|
||||
|
||||
log("Script starting...")
|
||||
|
||||
# ============================================================
|
||||
# PATCH 1: OpenAI client initialization
|
||||
# ============================================================
|
||||
import openai
|
||||
|
||||
original_openai_init = openai.OpenAI.__init__
|
||||
|
||||
@functools.wraps(original_openai_init)
|
||||
def traced_openai_init(self, *args, **kwargs):
|
||||
t0 = ts()
|
||||
result = original_openai_init(self, *args, **kwargs)
|
||||
log(f"OpenAI.__init__() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
openai.OpenAI.__init__ = traced_openai_init
|
||||
|
||||
# ============================================================
|
||||
# PATCH 2: OpenAI completions.create
|
||||
# ============================================================
|
||||
original_create = openai.resources.chat.completions.Completions.create
|
||||
|
||||
@functools.wraps(original_create)
|
||||
def traced_create(self, *args, **kwargs):
|
||||
log(f">>> OpenAI.create() CALLED - stream={kwargs.get('stream', False)}")
|
||||
t0 = ts()
|
||||
result = original_create(self, *args, **kwargs)
|
||||
log(f"<<< OpenAI.create() RETURNED in {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
openai.resources.chat.completions.Completions.create = traced_create
|
||||
|
||||
# ============================================================
|
||||
# PATCH 3: httpx.Client.send (actual HTTP request)
|
||||
# ============================================================
|
||||
import httpx
|
||||
|
||||
original_send = httpx.Client.send
|
||||
|
||||
@functools.wraps(original_send)
|
||||
def traced_send(self, request, *args, **kwargs):
|
||||
log(f">>> HTTPX.send() {request.method} {request.url}")
|
||||
t0 = ts()
|
||||
result = original_send(self, request, *args, **kwargs)
|
||||
log(f"<<< HTTPX.send() returned in {(ts()-t0)*1000:.2f}ms - status={result.status_code}")
|
||||
return result
|
||||
|
||||
httpx.Client.send = traced_send
|
||||
|
||||
# ============================================================
|
||||
# PATCH 4: Rich Live display
|
||||
# ============================================================
|
||||
from rich.live import Live
|
||||
|
||||
original_live_init = Live.__init__
|
||||
original_live_enter = Live.__enter__
|
||||
original_live_exit = Live.__exit__
|
||||
|
||||
@functools.wraps(original_live_init)
|
||||
def traced_live_init(self, *args, **kwargs):
|
||||
log(f">>> Live.__init__() called")
|
||||
t0 = ts()
|
||||
result = original_live_init(self, *args, **kwargs)
|
||||
log(f"<<< Live.__init__() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_live_enter)
|
||||
def traced_live_enter(self):
|
||||
log(f">>> Live.__enter__() called")
|
||||
t0 = ts()
|
||||
result = original_live_enter(self)
|
||||
log(f"<<< Live.__enter__() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_live_exit)
|
||||
def traced_live_exit(self, *args):
|
||||
log(f">>> Live.__exit__() called")
|
||||
t0 = ts()
|
||||
result = original_live_exit(self, *args)
|
||||
log(f"<<< Live.__exit__() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
Live.__init__ = traced_live_init
|
||||
Live.__enter__ = traced_live_enter
|
||||
Live.__exit__ = traced_live_exit
|
||||
|
||||
log("Patches applied, importing praisonaiagents...")
|
||||
|
||||
# ============================================================
|
||||
# Import and trace Agent
|
||||
# ============================================================
|
||||
t0 = ts()
|
||||
from praisonaiagents import Agent
|
||||
log(f"Agent import took {(ts()-t0)*1000:.2f}ms")
|
||||
|
||||
# ============================================================
|
||||
# PATCH 5: Agent methods
|
||||
# ============================================================
|
||||
from praisonaiagents.agent.agent import Agent as AgentClass
|
||||
|
||||
original_chat_completion = AgentClass._chat_completion
|
||||
original_format_tools = AgentClass._format_tools_for_completion
|
||||
|
||||
@functools.wraps(original_chat_completion)
|
||||
def traced_chat_completion(self, messages, temperature=1.0, tools=None, stream=True,
|
||||
reasoning_steps=False, task_name=None, task_description=None, task_id=None):
|
||||
log(f">>> Agent._chat_completion() stream={stream}, _using_custom_llm={self._using_custom_llm}")
|
||||
t0 = ts()
|
||||
result = original_chat_completion(self, messages, temperature, tools, stream,
|
||||
reasoning_steps, task_name, task_description, task_id)
|
||||
log(f"<<< Agent._chat_completion() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_format_tools)
|
||||
def traced_format_tools(self, tools):
|
||||
log(f">>> Agent._format_tools_for_completion() tools={tools}")
|
||||
t0 = ts()
|
||||
result = original_format_tools(self, tools)
|
||||
log(f"<<< Agent._format_tools_for_completion() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
AgentClass._chat_completion = traced_chat_completion
|
||||
AgentClass._format_tools_for_completion = traced_format_tools
|
||||
|
||||
# ============================================================
|
||||
# PATCH 6: OpenAIClient methods
|
||||
# ============================================================
|
||||
from praisonaiagents.llm.openai_client import OpenAIClient
|
||||
|
||||
original_chat_with_tools = OpenAIClient.chat_completion_with_tools
|
||||
original_process_stream = OpenAIClient.process_stream_response
|
||||
original_create_completion = OpenAIClient.create_completion
|
||||
original_format_tools_client = OpenAIClient.format_tools
|
||||
|
||||
@functools.wraps(original_chat_with_tools)
|
||||
def traced_chat_with_tools(self, *args, **kwargs):
|
||||
log(f">>> OpenAIClient.chat_completion_with_tools() stream={kwargs.get('stream', True)}")
|
||||
t0 = ts()
|
||||
result = original_chat_with_tools(self, *args, **kwargs)
|
||||
log(f"<<< OpenAIClient.chat_completion_with_tools() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_process_stream)
|
||||
def traced_process_stream(self, *args, **kwargs):
|
||||
log(f">>> OpenAIClient.process_stream_response()")
|
||||
t0 = ts()
|
||||
result = original_process_stream(self, *args, **kwargs)
|
||||
log(f"<<< OpenAIClient.process_stream_response() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_create_completion)
|
||||
def traced_create_completion(self, *args, **kwargs):
|
||||
log(f">>> OpenAIClient.create_completion() stream={kwargs.get('stream', False)}")
|
||||
t0 = ts()
|
||||
result = original_create_completion(self, *args, **kwargs)
|
||||
log(f"<<< OpenAIClient.create_completion() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
@functools.wraps(original_format_tools_client)
|
||||
def traced_format_tools_client(self, tools):
|
||||
log(f">>> OpenAIClient.format_tools() tools count={len(tools) if tools else 0}")
|
||||
t0 = ts()
|
||||
result = original_format_tools_client(self, tools)
|
||||
log(f"<<< OpenAIClient.format_tools() took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
OpenAIClient.chat_completion_with_tools = traced_chat_with_tools
|
||||
OpenAIClient.process_stream_response = traced_process_stream
|
||||
OpenAIClient.create_completion = traced_create_completion
|
||||
OpenAIClient.format_tools = traced_format_tools_client
|
||||
|
||||
# ============================================================
|
||||
# PATCH 7: sync_client property (lazy init)
|
||||
# ============================================================
|
||||
original_sync_client_getter = OpenAIClient.sync_client.fget
|
||||
|
||||
def traced_sync_client_getter(self):
|
||||
log(f">>> OpenAIClient.sync_client property accessed")
|
||||
t0 = ts()
|
||||
result = original_sync_client_getter(self)
|
||||
log(f"<<< OpenAIClient.sync_client took {(ts()-t0)*1000:.2f}ms")
|
||||
return result
|
||||
|
||||
OpenAIClient.sync_client = property(traced_sync_client_getter)
|
||||
|
||||
log("All patches applied. Creating agent...")
|
||||
|
||||
# ============================================================
|
||||
# RUN THE TEST
|
||||
# ============================================================
|
||||
t0 = ts()
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant"
|
||||
)
|
||||
log(f"Agent created in {(ts()-t0)*1000:.2f}ms")
|
||||
log(f"Agent.stream={agent.stream}, Agent.verbose={agent.verbose}")
|
||||
|
||||
log("=" * 60)
|
||||
log("Starting agent.start()...")
|
||||
log("=" * 60)
|
||||
|
||||
t0 = ts()
|
||||
result = agent.start("Say hello in one word")
|
||||
log(f"agent.start() completed in {(ts()-t0)*1000:.2f}ms")
|
||||
|
||||
log("=" * 60)
|
||||
log("DONE")
|
||||
log("=" * 60)
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.10.6"
|
||||
version = "0.11.8"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
Streaming Example with TTFT Metrics
|
||||
|
||||
This example demonstrates true pass-through streaming with timing metrics.
|
||||
When verbose output is enabled, tokens are displayed as they arrive from the provider.
|
||||
|
||||
Key timing metrics:
|
||||
- TTFT (Time To First Token): Time from request to first token received
|
||||
- Stream Duration: Time from first to last token
|
||||
- Total Time: End-to-end request time
|
||||
"""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config import OutputConfig
|
||||
|
||||
# Create agent with verbose output (shows streaming progress)
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gpt-4o-mini",
|
||||
output=OutputConfig(verbose=True) # Show generation progress
|
||||
)
|
||||
|
||||
# Run the agent - verbose mode shows streaming progress via Rich Live display
|
||||
result = agent.start("Write a short paragraph about the history of computing")
|
||||
print(f"\n--- Response ---\n{result}")
|
||||
@@ -1,11 +0,0 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Use chat method with stream=True for streaming responses
|
||||
for chunk in agent.chat("Write a short paragraph about the history of computing", stream=True):
|
||||
print(chunk, end="", flush=True)
|
||||
print() # Final newline
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Micro-benchmark for output mode overhead comparison.
|
||||
|
||||
Measures:
|
||||
1. Import-time cost (cold import)
|
||||
2. Agent initialization overhead per output mode
|
||||
3. Hot-path overhead (simulated callback execution)
|
||||
|
||||
Usage:
|
||||
python tests/benchmarks/output_mode_benchmark.py
|
||||
|
||||
All stdlib - no external dependencies required.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from statistics import mean, stdev
|
||||
|
||||
|
||||
def measure_import_time(mode: str, iterations: int = 5) -> dict:
|
||||
"""Measure cold import time for a specific output mode."""
|
||||
times = []
|
||||
|
||||
# Script that imports and creates an agent with specific output mode
|
||||
script = f'''
|
||||
import time
|
||||
start = time.perf_counter()
|
||||
import os
|
||||
os.environ['PRAISONAI_OUTPUT'] = '{mode}'
|
||||
from praisonaiagents import Agent
|
||||
agent = Agent(name="BenchAgent", instructions="Test", output="{mode}")
|
||||
end = time.perf_counter()
|
||||
print(f"{{(end - start) * 1000:.3f}}")
|
||||
'''
|
||||
|
||||
for _ in range(iterations):
|
||||
result = subprocess.run(
|
||||
[sys.executable, '-c', script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**dict(__import__('os').environ), 'PYTHONDONTWRITEBYTECODE': '1'}
|
||||
)
|
||||
if result.returncode == 0:
|
||||
try:
|
||||
times.append(float(result.stdout.strip()))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not times:
|
||||
return {"mode": mode, "mean_ms": -1, "stdev_ms": -1, "samples": 0}
|
||||
|
||||
return {
|
||||
"mode": mode,
|
||||
"mean_ms": mean(times),
|
||||
"stdev_ms": stdev(times) if len(times) > 1 else 0,
|
||||
"samples": len(times),
|
||||
}
|
||||
|
||||
|
||||
def measure_callback_overhead(iterations: int = 1000) -> dict:
|
||||
"""Measure hot-path overhead of callback execution vs no callbacks."""
|
||||
results = {}
|
||||
|
||||
# Silent mode - no callbacks
|
||||
silent_times = []
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
# Simulate what happens in silent mode: nothing
|
||||
pass
|
||||
end = time.perf_counter()
|
||||
silent_times.append((end - start) * 1_000_000) # microseconds
|
||||
|
||||
results["silent"] = {
|
||||
"mean_us": mean(silent_times),
|
||||
"stdev_us": stdev(silent_times) if len(silent_times) > 1 else 0,
|
||||
}
|
||||
|
||||
# Actions mode - simulate callback overhead
|
||||
actions_times = []
|
||||
|
||||
# Simulate the work done by actions mode per tool call
|
||||
def simulate_actions_callback():
|
||||
import time as t
|
||||
# Simulate: time.time(), dict creation, string formatting, stderr write
|
||||
_ = t.time()
|
||||
event = {
|
||||
"type": "tool_call",
|
||||
"tool": "test_tool",
|
||||
"args": {"arg1": "value1", "arg2": "value2"},
|
||||
"timestamp": t.time(),
|
||||
}
|
||||
formatted = f"[tool_call] {event['tool']}({event['args']})"
|
||||
# Don't actually write to avoid polluting output
|
||||
_ = formatted.encode()
|
||||
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter()
|
||||
simulate_actions_callback()
|
||||
end = time.perf_counter()
|
||||
actions_times.append((end - start) * 1_000_000) # microseconds
|
||||
|
||||
results["actions"] = {
|
||||
"mean_us": mean(actions_times),
|
||||
"stdev_us": stdev(actions_times) if len(actions_times) > 1 else 0,
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def measure_agent_init_overhead() -> dict:
|
||||
"""Measure Agent.__init__ overhead for different output modes."""
|
||||
import os
|
||||
|
||||
# Clear any env var
|
||||
os.environ.pop('PRAISONAI_OUTPUT', None)
|
||||
|
||||
results = {}
|
||||
iterations = 10
|
||||
|
||||
for mode in ["silent", "actions", "verbose"]:
|
||||
times = []
|
||||
for _ in range(iterations):
|
||||
# Import fresh each time to avoid caching effects
|
||||
from praisonaiagents import Agent
|
||||
|
||||
start = time.perf_counter()
|
||||
agent = Agent(
|
||||
name="BenchAgent",
|
||||
instructions="Test agent",
|
||||
output=mode,
|
||||
)
|
||||
end = time.perf_counter()
|
||||
times.append((end - start) * 1000) # milliseconds
|
||||
del agent
|
||||
|
||||
results[mode] = {
|
||||
"mean_ms": mean(times),
|
||||
"stdev_ms": stdev(times) if len(times) > 1 else 0,
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("OUTPUT MODE MICRO-BENCHMARK")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# 1. Import time comparison
|
||||
print("1. IMPORT + INIT TIME (cold start, subprocess)")
|
||||
print("-" * 40)
|
||||
|
||||
modes = ["silent", "actions", "verbose"]
|
||||
import_results = []
|
||||
|
||||
for mode in modes:
|
||||
print(f" Measuring {mode}...", end=" ", flush=True)
|
||||
result = measure_import_time(mode, iterations=3)
|
||||
import_results.append(result)
|
||||
print(f"{result['mean_ms']:.1f}ms (±{result['stdev_ms']:.1f})")
|
||||
|
||||
print()
|
||||
|
||||
# 2. Agent init overhead (warm)
|
||||
print("2. AGENT INIT OVERHEAD (warm, same process)")
|
||||
print("-" * 40)
|
||||
|
||||
init_results = measure_agent_init_overhead()
|
||||
for mode, data in init_results.items():
|
||||
print(f" {mode:12s}: {data['mean_ms']:.2f}ms (±{data['stdev_ms']:.2f})")
|
||||
|
||||
print()
|
||||
|
||||
# 3. Hot-path callback overhead
|
||||
print("3. HOT-PATH CALLBACK OVERHEAD (per call)")
|
||||
print("-" * 40)
|
||||
|
||||
callback_results = measure_callback_overhead(iterations=10000)
|
||||
for mode, data in callback_results.items():
|
||||
print(f" {mode:12s}: {data['mean_us']:.3f}µs (±{data['stdev_us']:.3f})")
|
||||
|
||||
print()
|
||||
|
||||
# Summary
|
||||
print("=" * 60)
|
||||
print("SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
silent_init = init_results.get("silent", {}).get("mean_ms", 0)
|
||||
actions_init = init_results.get("actions", {}).get("mean_ms", 0)
|
||||
|
||||
silent_callback = callback_results.get("silent", {}).get("mean_us", 0)
|
||||
actions_callback = callback_results.get("actions", {}).get("mean_us", 0)
|
||||
|
||||
print(f" Init overhead (actions vs silent): +{actions_init - silent_init:.2f}ms")
|
||||
print(f" Per-call overhead (actions vs silent): +{actions_callback - silent_callback:.3f}µs")
|
||||
print()
|
||||
print(" DEFAULT MODE: silent (zero overhead)")
|
||||
print(" OPT-IN: output='actions' for observability")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,333 @@
|
||||
"""
|
||||
Integration tests for Dict support in consolidated param resolution.
|
||||
|
||||
Tests that Agent, Agents, and Workflow classes correctly handle dict params
|
||||
through the unified canonical resolver.
|
||||
|
||||
Note: Agent tests are skipped as they require LLM setup. The core dict
|
||||
resolution is tested in unit tests. These tests focus on Workflow/WorkflowStep
|
||||
which don't require LLM setup.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AGENT DICT PARAM TESTS - Using resolver directly to avoid LLM setup
|
||||
# =============================================================================
|
||||
|
||||
class TestAgentDictParamsResolver:
|
||||
"""Test Agent dict param resolution using the resolver directly."""
|
||||
|
||||
def test_agent_output_dict_resolution(self):
|
||||
"""Output dict resolves correctly through canonical resolver."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
result = resolve(
|
||||
value={"verbose": True, "markdown": False},
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
assert isinstance(result, OutputConfig)
|
||||
assert result.verbose is True
|
||||
assert result.markdown is False
|
||||
|
||||
def test_agent_execution_dict_resolution(self):
|
||||
"""Execution dict resolves correctly through canonical resolver."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.feature_configs import ExecutionConfig
|
||||
from praisonaiagents.config.presets import EXECUTION_PRESETS
|
||||
|
||||
result = resolve(
|
||||
value={"max_iter": 50, "max_retry_limit": 10},
|
||||
param_name="execution",
|
||||
config_class=ExecutionConfig,
|
||||
presets=EXECUTION_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
assert isinstance(result, ExecutionConfig)
|
||||
assert result.max_iter == 50
|
||||
|
||||
def test_agent_dict_with_unknown_key_raises_error(self):
|
||||
"""Dict with unknown key raises TypeError."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
resolve(
|
||||
value={"verbose": True, "invalid_key": "value"},
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
assert "Unknown keys" in str(exc_info.value)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AGENTS (MULTI-AGENT) DICT PARAM TESTS - Using resolver directly
|
||||
# =============================================================================
|
||||
|
||||
class TestAgentsDictParamsResolver:
|
||||
"""Test Agents dict param resolution using the resolver directly."""
|
||||
|
||||
def test_agents_output_dict_resolution(self):
|
||||
"""Multi-agent output dict resolves correctly."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.feature_configs import MultiAgentOutputConfig
|
||||
from praisonaiagents.config.presets import MULTI_AGENT_OUTPUT_PRESETS
|
||||
|
||||
result = resolve(
|
||||
value={"verbose": 2, "stream": False},
|
||||
param_name="output",
|
||||
config_class=MultiAgentOutputConfig,
|
||||
presets=MULTI_AGENT_OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
assert isinstance(result, MultiAgentOutputConfig)
|
||||
assert result.verbose == 2
|
||||
assert result.stream is False
|
||||
|
||||
def test_agents_execution_dict_resolution(self):
|
||||
"""Multi-agent execution dict resolves correctly."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.feature_configs import MultiAgentExecutionConfig
|
||||
from praisonaiagents.config.presets import MULTI_AGENT_EXECUTION_PRESETS
|
||||
|
||||
result = resolve(
|
||||
value={"max_iter": 25, "max_retries": 8},
|
||||
param_name="execution",
|
||||
config_class=MultiAgentExecutionConfig,
|
||||
presets=MULTI_AGENT_EXECUTION_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
assert isinstance(result, MultiAgentExecutionConfig)
|
||||
assert result.max_iter == 25
|
||||
assert result.max_retries == 8
|
||||
|
||||
def test_agents_hooks_dict_resolution(self):
|
||||
"""Multi-agent hooks dict resolves correctly."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.feature_configs import MultiAgentHooksConfig
|
||||
|
||||
def my_checker(result):
|
||||
return True
|
||||
|
||||
result = resolve(
|
||||
value={"completion_checker": my_checker},
|
||||
param_name="hooks",
|
||||
config_class=MultiAgentHooksConfig,
|
||||
array_mode=ArrayMode.PASSTHROUGH,
|
||||
)
|
||||
assert isinstance(result, MultiAgentHooksConfig)
|
||||
assert result.completion_checker is my_checker
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# WORKFLOW DICT PARAM TESTS
|
||||
# =============================================================================
|
||||
|
||||
class TestWorkflowDictParams:
|
||||
"""Test Workflow and WorkflowStep with dict params."""
|
||||
|
||||
def test_workflow_output_dict(self):
|
||||
"""Workflow accepts output as dict."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(
|
||||
name="test",
|
||||
steps=[],
|
||||
output={"verbose": True, "stream": False},
|
||||
)
|
||||
# Output config should be resolved
|
||||
assert workflow.output is not None
|
||||
|
||||
def test_workflow_planning_dict(self):
|
||||
"""Workflow accepts planning as dict."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(
|
||||
name="test",
|
||||
steps=[],
|
||||
planning={"enabled": True, "llm": "gpt-4o"},
|
||||
)
|
||||
# Planning should be configured
|
||||
assert workflow.planning is not None
|
||||
|
||||
def test_workflow_memory_dict(self):
|
||||
"""Workflow accepts memory as dict."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(
|
||||
name="test",
|
||||
steps=[],
|
||||
memory={"backend": "sqlite", "user_id": "test_user"},
|
||||
)
|
||||
# Memory should be configured
|
||||
assert workflow.memory is not None
|
||||
|
||||
def test_workflowstep_context_dict(self):
|
||||
"""WorkflowStep accepts context as dict."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(
|
||||
name="step1",
|
||||
context={"from_steps": ["step0"], "retain_full": True},
|
||||
)
|
||||
assert step.context_from == ["step0"]
|
||||
assert step.retain_full_context is True
|
||||
|
||||
def test_workflowstep_output_dict(self):
|
||||
"""WorkflowStep accepts output as dict."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(
|
||||
name="step1",
|
||||
output={"file": "output.txt", "variable": "result"},
|
||||
)
|
||||
assert step.output_file == "output.txt"
|
||||
assert step.output_variable == "result"
|
||||
|
||||
def test_workflowstep_execution_dict(self):
|
||||
"""WorkflowStep accepts execution as dict."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(
|
||||
name="step1",
|
||||
execution={"max_retries": 5, "quality_check": True},
|
||||
)
|
||||
assert step.max_retries == 5
|
||||
assert step.quality_check is True
|
||||
|
||||
def test_workflowstep_routing_dict(self):
|
||||
"""WorkflowStep accepts routing as dict."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(
|
||||
name="step1",
|
||||
routing={"next_steps": ["step2", "step3"]},
|
||||
)
|
||||
assert step.next_steps == ["step2", "step3"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# BACKWARD COMPATIBILITY TESTS - Using resolver directly
|
||||
# =============================================================================
|
||||
|
||||
class TestBackwardCompatibility:
|
||||
"""Test that existing usage patterns still work via resolver."""
|
||||
|
||||
def test_bool_true_creates_default_config(self):
|
||||
"""Bool True creates default config instance."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
from praisonaiagents.config.feature_configs import PlanningConfig
|
||||
|
||||
result = resolve(
|
||||
value=True,
|
||||
param_name="planning",
|
||||
config_class=PlanningConfig,
|
||||
)
|
||||
assert isinstance(result, PlanningConfig)
|
||||
|
||||
def test_bool_false_returns_none(self):
|
||||
"""Bool False returns None (disabled)."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
from praisonaiagents.config.feature_configs import ReflectionConfig
|
||||
|
||||
result = resolve(
|
||||
value=False,
|
||||
param_name="reflection",
|
||||
config_class=ReflectionConfig,
|
||||
)
|
||||
assert result is None
|
||||
|
||||
def test_string_preset_resolves(self):
|
||||
"""String preset resolves to config."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
result = resolve(
|
||||
value="verbose",
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
assert isinstance(result, OutputConfig)
|
||||
assert result.verbose is True
|
||||
|
||||
def test_array_preset_override_works(self):
|
||||
"""[preset, overrides] pattern works."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
result = resolve(
|
||||
value=["verbose", {"stream": False}],
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
assert isinstance(result, OutputConfig)
|
||||
assert result.verbose is True
|
||||
assert result.stream is False
|
||||
|
||||
def test_workflow_bool_params_still_work(self):
|
||||
"""Workflow with bool params still works."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(
|
||||
name="test",
|
||||
steps=[],
|
||||
planning=True,
|
||||
)
|
||||
# Should not raise - workflow created successfully
|
||||
assert workflow is not None
|
||||
|
||||
def test_workflowstep_list_context_still_works(self):
|
||||
"""WorkflowStep with list context still works."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(
|
||||
name="step1",
|
||||
context=["step0", "step_prev"],
|
||||
)
|
||||
assert step.context_from == ["step0", "step_prev"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# BASE_URL / API_KEY SEPARATION TESTS
|
||||
# =============================================================================
|
||||
|
||||
class TestBaseUrlApiKeySeparation:
|
||||
"""Test that base_url and api_key remain separate parameters."""
|
||||
|
||||
def test_agent_base_url_separate(self):
|
||||
"""Agent base_url is a separate parameter, not consolidated."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
import inspect
|
||||
|
||||
sig = inspect.signature(Agent.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
|
||||
# base_url should be a separate parameter
|
||||
assert "base_url" in params
|
||||
# It should NOT be part of any consolidated config
|
||||
|
||||
def test_agent_api_key_separate(self):
|
||||
"""Agent api_key is a separate parameter, not consolidated."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
import inspect
|
||||
|
||||
sig = inspect.signature(Agent.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
|
||||
# api_key should be a separate parameter
|
||||
assert "api_key" in params
|
||||
@@ -179,25 +179,27 @@ class TestAgentWebFetchParameter:
|
||||
"""Test the web_fetch parameter in Agent class."""
|
||||
|
||||
def test_agent_init_with_web_fetch_true(self):
|
||||
"""Test Agent initialization with web_fetch=True."""
|
||||
"""Test Agent initialization with web=WebConfig(fetch=True)."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=True
|
||||
web=WebConfig(fetch=True)
|
||||
)
|
||||
assert agent.web_fetch is True
|
||||
|
||||
def test_agent_init_with_web_fetch_dict(self):
|
||||
"""Test Agent initialization with web_fetch as dict."""
|
||||
"""Test Agent initialization with web=WebConfig(fetch=True)."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
web_fetch_config = {"max_uses": 10, "allowed_domains": ["example.com"]}
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
# Now using consolidated web= param with WebConfig
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=web_fetch_config
|
||||
web=WebConfig(fetch=True)
|
||||
)
|
||||
assert agent.web_fetch == web_fetch_config
|
||||
assert agent.web_fetch is True
|
||||
|
||||
def test_agent_init_without_web_fetch(self):
|
||||
"""Test Agent initialization without web_fetch."""
|
||||
@@ -208,10 +210,11 @@ class TestAgentWebFetchParameter:
|
||||
def test_agent_passes_web_fetch_to_llm(self):
|
||||
"""Test that Agent passes web_fetch to LLM instance."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=True
|
||||
web=WebConfig(fetch=True)
|
||||
)
|
||||
assert agent.llm_instance.web_fetch is True
|
||||
|
||||
@@ -270,12 +273,13 @@ class TestWebFetchIntegration:
|
||||
def test_web_fetch_with_anthropic(self):
|
||||
"""Test actual web fetch with Anthropic API."""
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
|
||||
agent = Agent(
|
||||
name="Web Fetch Agent",
|
||||
instructions="You are a helpful assistant that can fetch web content.",
|
||||
llm="anthropic/claude-3-5-sonnet-latest",
|
||||
web_fetch=True
|
||||
web=WebConfig(fetch=True)
|
||||
)
|
||||
|
||||
# This would make an actual API call
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
Tests for Agent execution API semantics: start(), run(), stream()
|
||||
|
||||
TDD tests for the new execution API:
|
||||
- start() = interactive, streams by default in TTY
|
||||
- run() = production, silent by default
|
||||
- stream() = app-friendly iterator
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestAgentStartMethod:
|
||||
"""Tests for Agent.start() - interactive, streaming by default in TTY"""
|
||||
|
||||
def test_start_streams_by_default_in_tty(self):
|
||||
"""start() should stream by default when stdout is a TTY"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Create agent without stream attribute set (None)
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
# Ensure stream attribute is None so TTY detection kicks in
|
||||
agent.stream = None
|
||||
|
||||
# Mock TTY detection
|
||||
with patch.object(sys.stdout, 'isatty', return_value=True):
|
||||
# Mock chat to avoid actual LLM call
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello", " World"])
|
||||
with patch.object(agent, 'chat') as mock_chat:
|
||||
agent.start("Hello")
|
||||
# Should call _start_stream when TTY
|
||||
mock_stream.assert_called_once()
|
||||
mock_chat.assert_not_called()
|
||||
|
||||
def test_start_does_not_stream_when_not_tty(self):
|
||||
"""start() should not stream when stdout is not a TTY"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
# Mock non-TTY
|
||||
with patch.object(sys.stdout, 'isatty', return_value=False):
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
with patch.object(agent, 'chat', return_value="Hello World") as mock_chat:
|
||||
result = agent.start("Hello")
|
||||
# Should call chat, not _start_stream
|
||||
mock_chat.assert_called_once()
|
||||
mock_stream.assert_not_called()
|
||||
|
||||
def test_start_respects_explicit_stream_true(self):
|
||||
"""start(stream=True) should always stream"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
# Even when not TTY, explicit stream=True should stream
|
||||
with patch.object(sys.stdout, 'isatty', return_value=False):
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello"])
|
||||
result = agent.start("Hello", stream=True)
|
||||
mock_stream.assert_called_once()
|
||||
|
||||
def test_start_respects_explicit_stream_false(self):
|
||||
"""start(stream=False) should never stream"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
# Even when TTY, explicit stream=False should not stream
|
||||
with patch.object(sys.stdout, 'isatty', return_value=True):
|
||||
with patch.object(agent, 'chat', return_value="Hello") as mock_chat:
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
result = agent.start("Hello", stream=False)
|
||||
mock_chat.assert_called_once()
|
||||
mock_stream.assert_not_called()
|
||||
|
||||
def test_start_respects_agent_stream_attribute(self):
|
||||
"""start() should respect agent's stream attribute over TTY detection"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Agent with stream=True should always stream
|
||||
agent = Agent(name="Test", instructions="You are helpful", output="stream")
|
||||
|
||||
with patch.object(sys.stdout, 'isatty', return_value=False):
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello"])
|
||||
# Agent's stream attribute should take precedence
|
||||
if agent.stream:
|
||||
result = agent.start("Hello")
|
||||
mock_stream.assert_called()
|
||||
|
||||
|
||||
class TestAgentRunMethod:
|
||||
"""Tests for Agent.run() - production, silent by default"""
|
||||
|
||||
def test_run_does_not_stream_by_default(self):
|
||||
"""run() should not stream by default"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, 'chat', return_value="Hello World") as mock_chat:
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
result = agent.run("Hello")
|
||||
# Should call chat with stream=False
|
||||
mock_chat.assert_called_once()
|
||||
call_kwargs = mock_chat.call_args[1]
|
||||
assert call_kwargs.get('stream') == False
|
||||
mock_stream.assert_not_called()
|
||||
|
||||
def test_run_returns_result_directly(self):
|
||||
"""run() should return the result directly, not a generator"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, 'chat', return_value="Hello World") as mock_chat:
|
||||
result = agent.run("Hello")
|
||||
assert result == "Hello World"
|
||||
assert not hasattr(result, '__iter__') or isinstance(result, str)
|
||||
|
||||
def test_run_ignores_tty_detection(self):
|
||||
"""run() should ignore TTY detection and never stream by default"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
# Even in TTY, run() should not stream
|
||||
with patch.object(sys.stdout, 'isatty', return_value=True):
|
||||
with patch.object(agent, 'chat', return_value="Hello") as mock_chat:
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
result = agent.run("Hello")
|
||||
mock_chat.assert_called_once()
|
||||
mock_stream.assert_not_called()
|
||||
|
||||
def test_run_can_be_forced_to_stream(self):
|
||||
"""run(stream=True) should stream when explicitly requested"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, 'chat', return_value="Hello") as mock_chat:
|
||||
result = agent.run("Hello", stream=True)
|
||||
# When stream=True is passed, it should be passed to chat
|
||||
call_kwargs = mock_chat.call_args[1]
|
||||
assert call_kwargs.get('stream') == True
|
||||
|
||||
|
||||
class TestAgentIterStreamMethod:
|
||||
"""Tests for Agent.iter_stream() - app-friendly iterator"""
|
||||
|
||||
def test_iter_stream_returns_iterator(self):
|
||||
"""iter_stream() should return an iterator"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello", " ", "World"])
|
||||
result = agent.iter_stream("Hello")
|
||||
# Should be an iterator/generator
|
||||
assert hasattr(result, '__iter__')
|
||||
assert hasattr(result, '__next__')
|
||||
|
||||
def test_iter_stream_yields_chunks(self):
|
||||
"""iter_stream() should yield response chunks"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello", " ", "World"])
|
||||
chunks = list(agent.iter_stream("Hello"))
|
||||
assert chunks == ["Hello", " ", "World"]
|
||||
|
||||
def test_iter_stream_always_streams(self):
|
||||
"""iter_stream() should always stream regardless of TTY"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
# Even when not TTY
|
||||
with patch.object(sys.stdout, 'isatty', return_value=False):
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello"])
|
||||
result = agent.iter_stream("Hello")
|
||||
# Consume the generator
|
||||
list(result)
|
||||
mock_stream.assert_called_once()
|
||||
|
||||
|
||||
class TestAsyncMethods:
|
||||
"""Tests for async versions: arun(), astart()"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_arun_does_not_stream_by_default(self):
|
||||
"""arun() should not stream by default"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
|
||||
with patch.object(agent, 'achat', return_value="Hello World") as mock_achat:
|
||||
result = await agent.arun("Hello")
|
||||
mock_achat.assert_called_once()
|
||||
call_kwargs = mock_achat.call_args[1]
|
||||
assert call_kwargs.get('stream') == False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_astart_streams_in_tty(self):
|
||||
"""astart() should stream when in TTY"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
# Ensure stream attribute is None so TTY detection kicks in
|
||||
agent.stream = None
|
||||
|
||||
with patch.object(sys.stdout, 'isatty', return_value=True):
|
||||
with patch.object(agent, 'achat', return_value="Hello") as mock_achat:
|
||||
await agent.astart("Hello")
|
||||
call_kwargs = mock_achat.call_args[1]
|
||||
assert call_kwargs.get('stream')
|
||||
|
||||
|
||||
class TestMethodSemanticDistinction:
|
||||
"""Tests to verify start() and run() have distinct semantics"""
|
||||
|
||||
def test_start_and_run_are_different(self):
|
||||
"""start() and run() should have different default behaviors"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", instructions="You are helpful")
|
||||
# Ensure stream attribute is None so TTY detection kicks in
|
||||
agent.stream = None
|
||||
|
||||
# In TTY context
|
||||
with patch.object(sys.stdout, 'isatty', return_value=True):
|
||||
with patch.object(agent, 'chat', return_value="Hello") as mock_chat:
|
||||
with patch.object(agent, '_start_stream') as mock_stream:
|
||||
mock_stream.return_value = iter(["Hello"])
|
||||
|
||||
# start() should stream
|
||||
agent.start("Hello")
|
||||
start_streamed = mock_stream.called
|
||||
|
||||
mock_stream.reset_mock()
|
||||
mock_chat.reset_mock()
|
||||
|
||||
# run() should not stream
|
||||
agent.run("Hello")
|
||||
run_streamed = mock_stream.called
|
||||
|
||||
# They should behave differently
|
||||
assert start_streamed
|
||||
assert not run_streamed
|
||||
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Unit tests for Agent output parameter with verbose preset.
|
||||
|
||||
Tests the fix for the bug where output="verbose" would return None
|
||||
because reasoning_steps=True in the verbose preset was causing the code
|
||||
to try to return reasoning_content, but this attribute exists as None
|
||||
for non-reasoning models.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
|
||||
|
||||
class TestReasoningContentLogic:
|
||||
"""Test the reasoning_content check logic directly."""
|
||||
|
||||
def test_condition_with_none_reasoning_content(self):
|
||||
"""Verify the fix: condition should be False when reasoning_content is None."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = None
|
||||
|
||||
reasoning_steps = True
|
||||
|
||||
# Old buggy condition:
|
||||
# if reasoning_steps and hasattr(mock_message, 'reasoning_content'):
|
||||
old_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content')
|
||||
assert old_condition == True, "Old condition would be True (the bug)"
|
||||
|
||||
# Fixed condition:
|
||||
# if reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content:
|
||||
# The truthiness of the whole expression should be False
|
||||
fixed_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content
|
||||
assert not fixed_condition, "Fixed condition should be falsy when reasoning_content is None"
|
||||
|
||||
def test_condition_with_empty_string_reasoning_content(self):
|
||||
"""Verify the fix: condition should be False when reasoning_content is empty string."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = ""
|
||||
|
||||
reasoning_steps = True
|
||||
|
||||
# Old condition would True (bug)
|
||||
old_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content')
|
||||
assert old_condition == True
|
||||
|
||||
# Fixed condition should be falsy
|
||||
fixed_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content
|
||||
assert not fixed_condition, "Fixed condition should be falsy when reasoning_content is empty string"
|
||||
|
||||
def test_condition_with_actual_reasoning_content(self):
|
||||
"""Verify the fix: condition should be True when reasoning_content has content."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = "This is my reasoning process..."
|
||||
|
||||
reasoning_steps = True
|
||||
|
||||
# Both old and new condition should be truthy
|
||||
old_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content')
|
||||
assert old_condition
|
||||
|
||||
fixed_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content
|
||||
assert fixed_condition, "Fixed condition should be truthy when reasoning_content has content"
|
||||
|
||||
def test_condition_when_reasoning_steps_false(self):
|
||||
"""Verify that when reasoning_steps is False, condition is always False."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = "This is my reasoning process..."
|
||||
|
||||
reasoning_steps = False
|
||||
|
||||
fixed_condition = reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content
|
||||
assert not fixed_condition, "Condition should be falsy when reasoning_steps is False"
|
||||
|
||||
def test_if_statement_behavior_with_none(self):
|
||||
"""Verify that an if statement using the condition skips when reasoning_content is None."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = None
|
||||
|
||||
reasoning_steps = True
|
||||
execution_path = "not_taken"
|
||||
|
||||
# This is how the fixed code behaves
|
||||
if reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content:
|
||||
execution_path = "reasoning_path"
|
||||
else:
|
||||
execution_path = "regular_path"
|
||||
|
||||
assert execution_path == "regular_path", "Should take regular path when reasoning_content is None"
|
||||
|
||||
def test_if_statement_behavior_with_reasoning(self):
|
||||
"""Verify that an if statement using the condition enters when reasoning_content has value."""
|
||||
mock_message = Mock()
|
||||
mock_message.reasoning_content = "Reasoning steps here..."
|
||||
|
||||
reasoning_steps = True
|
||||
execution_path = "not_taken"
|
||||
|
||||
if reasoning_steps and hasattr(mock_message, 'reasoning_content') and mock_message.reasoning_content:
|
||||
execution_path = "reasoning_path"
|
||||
else:
|
||||
execution_path = "regular_path"
|
||||
|
||||
assert execution_path == "reasoning_path", "Should take reasoning path when reasoning_content has value"
|
||||
|
||||
|
||||
class TestVerbosePresetConfig:
|
||||
"""Test that the verbose preset has the expected configuration."""
|
||||
|
||||
def test_verbose_preset_has_reasoning_steps_true(self):
|
||||
"""Verify that the verbose preset enables reasoning_steps."""
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
verbose_preset = OUTPUT_PRESETS.get("verbose")
|
||||
assert verbose_preset is not None, "verbose preset should exist"
|
||||
assert verbose_preset.get("reasoning_steps") == True, "verbose preset should have reasoning_steps=True"
|
||||
|
||||
def test_verbose_preset_has_verbose_true(self):
|
||||
"""Verify that the verbose preset enables verbose output."""
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
verbose_preset = OUTPUT_PRESETS.get("verbose")
|
||||
assert verbose_preset.get("verbose") == True, "verbose preset should have verbose=True"
|
||||
|
||||
def test_verbose_preset_has_stream_false(self):
|
||||
"""Verify that the verbose preset does not enable streaming by default."""
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
verbose_preset = OUTPUT_PRESETS.get("verbose")
|
||||
assert verbose_preset.get("stream") == False, "verbose preset should have stream=False"
|
||||
|
||||
|
||||
class TestChatCompletionMessageDataclass:
|
||||
"""Test the ChatCompletionMessage dataclass has reasoning_content attribute."""
|
||||
|
||||
def test_chat_completion_message_has_reasoning_content(self):
|
||||
"""Verify ChatCompletionMessage has reasoning_content attribute that defaults to None."""
|
||||
from praisonaiagents.llm.openai_client import ChatCompletionMessage
|
||||
|
||||
# Create without reasoning_content
|
||||
message = ChatCompletionMessage(content="Test", role="assistant")
|
||||
|
||||
# Should have the attribute
|
||||
assert hasattr(message, 'reasoning_content')
|
||||
# Should default to None
|
||||
assert message.reasoning_content is None
|
||||
|
||||
def test_chat_completion_message_with_reasoning(self):
|
||||
"""Verify ChatCompletionMessage can have reasoning_content set."""
|
||||
from praisonaiagents.llm.openai_client import ChatCompletionMessage
|
||||
|
||||
message = ChatCompletionMessage(
|
||||
content="Test",
|
||||
role="assistant",
|
||||
reasoning_content="This is my reasoning..."
|
||||
)
|
||||
|
||||
assert message.reasoning_content == "This is my reasoning..."
|
||||
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Tests for wrapper agent modernization - ensuring legacy args are mapped to consolidated params.
|
||||
|
||||
TDD: These tests verify that wrapper classes (PromptExpanderAgent, QueryRewriterAgent)
|
||||
correctly use consolidated params (output=) instead of legacy args (verbose=, markdown=).
|
||||
"""
|
||||
|
||||
|
||||
class TestPromptExpanderAgentModernization:
|
||||
"""Test PromptExpanderAgent uses consolidated output param."""
|
||||
|
||||
def test_agent_uses_output_config_not_legacy_args(self):
|
||||
"""PromptExpanderAgent should pass output= to Agent, not verbose=/markdown=."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
|
||||
# Create wrapper with verbose=True
|
||||
expander = PromptExpanderAgent(verbose=True)
|
||||
|
||||
# Access the lazy-initialized agent
|
||||
agent = expander.agent
|
||||
|
||||
# Verify: Agent should have output config set, not legacy verbose/markdown as direct args
|
||||
# The Agent stores resolved output in self.verbose and self.markdown after resolution
|
||||
# But the key is that the wrapper passed output= dict, not verbose=/markdown= directly
|
||||
assert hasattr(agent, 'verbose')
|
||||
assert hasattr(agent, 'markdown')
|
||||
# verbose should be True (from output config)
|
||||
assert agent.verbose == True
|
||||
# markdown should be False (from output config)
|
||||
assert agent.markdown == False
|
||||
|
||||
def test_agent_verbose_false_propagates(self):
|
||||
"""PromptExpanderAgent with verbose=False should propagate correctly."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
|
||||
expander = PromptExpanderAgent(verbose=False)
|
||||
agent = expander.agent
|
||||
|
||||
assert agent.verbose == False
|
||||
assert agent.markdown == False
|
||||
|
||||
|
||||
class TestQueryRewriterAgentModernization:
|
||||
"""Test QueryRewriterAgent uses consolidated output param."""
|
||||
|
||||
def test_agent_uses_output_config_not_legacy_args(self):
|
||||
"""QueryRewriterAgent should pass output= to Agent, not verbose=/markdown=."""
|
||||
from praisonaiagents.agent.query_rewriter_agent import QueryRewriterAgent
|
||||
|
||||
# Create wrapper with verbose=True
|
||||
rewriter = QueryRewriterAgent(verbose=True)
|
||||
|
||||
# Access the lazy-initialized agent
|
||||
agent = rewriter.agent
|
||||
|
||||
# Verify output config was applied
|
||||
assert agent.verbose == True
|
||||
assert agent.markdown == False
|
||||
|
||||
def test_agent_verbose_false_propagates(self):
|
||||
"""QueryRewriterAgent with verbose=False should propagate correctly."""
|
||||
from praisonaiagents.agent.query_rewriter_agent import QueryRewriterAgent
|
||||
|
||||
rewriter = QueryRewriterAgent(verbose=False)
|
||||
agent = rewriter.agent
|
||||
|
||||
assert agent.verbose == False
|
||||
assert agent.markdown == False
|
||||
|
||||
|
||||
class TestProcessManagerAgentModernization:
|
||||
"""Test Process class manager agents use consolidated params."""
|
||||
|
||||
def test_manager_agent_creation_uses_consolidated_params(self):
|
||||
"""Process manager agent should use output= and reflection=, not legacy args."""
|
||||
# This is a structural test - we verify the code pattern
|
||||
import inspect
|
||||
from praisonaiagents.process.process import Process
|
||||
|
||||
# Get source of hierarchical method (sync version)
|
||||
source = inspect.getsource(Process.hierarchical)
|
||||
|
||||
# After fix: should contain output= and reflection=
|
||||
assert 'output=' in source, "Process.hierarchical should use output= consolidated param"
|
||||
assert 'reflection=' in source, "Process.hierarchical should use reflection= consolidated param"
|
||||
|
||||
# Should NOT contain legacy args passed to Agent
|
||||
assert 'self_reflect=' not in source, "Process.hierarchical should not use legacy self_reflect="
|
||||
|
||||
|
||||
class TestAutoAgentsConsolidatedParams:
|
||||
"""Test AutoAgents already uses consolidated params correctly."""
|
||||
|
||||
def test_autoagents_has_consolidated_params(self):
|
||||
"""AutoAgents should accept consolidated params."""
|
||||
import inspect
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
|
||||
# Should have consolidated params
|
||||
assert 'output' in params
|
||||
assert 'reflection' in params
|
||||
assert 'caching' in params
|
||||
assert 'knowledge' in params
|
||||
assert 'execution' in params
|
||||
assert 'guardrails' in params
|
||||
assert 'web' in params
|
||||
assert 'hooks' in params
|
||||
|
||||
def test_autoagents_consolidated_takes_precedence(self):
|
||||
"""AutoAgents consolidated params should override legacy params."""
|
||||
import inspect
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
|
||||
# Get source of _create_agents_and_tasks
|
||||
source = inspect.getsource(AutoAgents._create_agents_and_tasks)
|
||||
|
||||
# Should pass consolidated params to Agent
|
||||
assert 'output=self._output' in source
|
||||
assert 'reflection=self._reflection' in source
|
||||
assert 'caching=self._caching' in source
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
TDD Tests for AutoAgents SDK Parity.
|
||||
|
||||
Tests that AutoAgents correctly maps legacy params to consolidated params
|
||||
and passes them through to Agent instances.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
class TestAutoAgentsLegacyParamMapping:
|
||||
"""Test that AutoAgents maps legacy params to consolidated params."""
|
||||
|
||||
def test_autoagents_accepts_legacy_verbose_param(self):
|
||||
"""AutoAgents should accept verbose param (legacy)."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
assert 'verbose' in params
|
||||
|
||||
def test_autoagents_accepts_legacy_markdown_param(self):
|
||||
"""AutoAgents should accept markdown param (legacy)."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
assert 'markdown' in params
|
||||
|
||||
def test_autoagents_accepts_legacy_self_reflect_param(self):
|
||||
"""AutoAgents should accept self_reflect param (legacy)."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
assert 'self_reflect' in params
|
||||
|
||||
def test_autoagents_accepts_legacy_cache_param(self):
|
||||
"""AutoAgents should accept cache param (legacy)."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
assert 'cache' in params
|
||||
|
||||
def test_autoagents_accepts_consolidated_output_param(self):
|
||||
"""AutoAgents should accept consolidated output param."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
# This test will FAIL initially - AutoAgents doesn't have output param yet
|
||||
assert 'output' in params, "AutoAgents should accept consolidated 'output' param"
|
||||
|
||||
def test_autoagents_accepts_consolidated_reflection_param(self):
|
||||
"""AutoAgents should accept consolidated reflection param."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
# This test will FAIL initially - AutoAgents doesn't have reflection param yet
|
||||
assert 'reflection' in params, "AutoAgents should accept consolidated 'reflection' param"
|
||||
|
||||
def test_autoagents_accepts_consolidated_caching_param(self):
|
||||
"""AutoAgents should accept consolidated caching param."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
# This test will FAIL initially - AutoAgents doesn't have caching param yet
|
||||
assert 'caching' in params, "AutoAgents should accept consolidated 'caching' param"
|
||||
|
||||
def test_autoagents_accepts_consolidated_knowledge_param(self):
|
||||
"""AutoAgents should accept consolidated knowledge param."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
sig = inspect.signature(AutoAgents.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
# This test will FAIL initially - AutoAgents doesn't have knowledge param yet
|
||||
assert 'knowledge' in params, "AutoAgents should accept consolidated 'knowledge' param"
|
||||
|
||||
|
||||
class TestAutoAgentsParamPrecedence:
|
||||
"""Test that consolidated params take precedence over legacy params."""
|
||||
|
||||
def test_consolidated_output_overrides_legacy_verbose(self):
|
||||
"""Consolidated output param should override legacy verbose."""
|
||||
# This tests the mapping logic once implemented
|
||||
pass # Placeholder - will be implemented after AutoAgents is updated
|
||||
|
||||
def test_consolidated_reflection_overrides_legacy_self_reflect(self):
|
||||
"""Consolidated reflection param should override legacy self_reflect."""
|
||||
pass # Placeholder
|
||||
|
||||
def test_consolidated_caching_overrides_legacy_cache(self):
|
||||
"""Consolidated caching param should override legacy cache."""
|
||||
pass # Placeholder
|
||||
|
||||
|
||||
class TestAutoAgentsAgentCreation:
|
||||
"""Test that AutoAgents creates Agent instances with correct params."""
|
||||
|
||||
@patch('praisonaiagents.agents.autoagents.AutoAgents._generate_config')
|
||||
@patch('praisonaiagents.agents.autoagents.AutoAgents._create_agents_and_tasks')
|
||||
def test_agent_receives_consolidated_params(self, mock_create, mock_config):
|
||||
"""Agents created by AutoAgents should receive consolidated params."""
|
||||
# Setup mocks to avoid LLM calls
|
||||
mock_config.return_value = MagicMock()
|
||||
mock_create.return_value = ([], [])
|
||||
|
||||
# This test verifies the param passing logic
|
||||
# Will be more detailed once implementation is done
|
||||
pass
|
||||
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
TDD tests for delegator.py protocol compliance.
|
||||
|
||||
Tests that SubagentDelegator creates agents using consolidated params
|
||||
instead of legacy params like verbose=, max_iter=.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
|
||||
class TestDelegatorProtocolCompliance:
|
||||
"""Test that delegator uses consolidated params when creating agents."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_agent_uses_output_not_verbose(self):
|
||||
"""Delegator should use output='silent' instead of verbose=False."""
|
||||
from praisonaiagents.agents.delegator import SubagentDelegator, DelegationTask
|
||||
from praisonaiagents.agents.profiles import AgentProfile
|
||||
|
||||
# Create a delegator
|
||||
delegator = SubagentDelegator()
|
||||
|
||||
# Create a profile and task
|
||||
profile = AgentProfile(
|
||||
name="test_agent",
|
||||
system_prompt="Test prompt",
|
||||
max_steps=10,
|
||||
)
|
||||
task = DelegationTask(
|
||||
task_id="test-1",
|
||||
agent_name="test_agent",
|
||||
objective="Test task",
|
||||
max_steps=5,
|
||||
)
|
||||
|
||||
# Mock Agent to capture how it's called (Agent is imported inside _create_agent)
|
||||
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
|
||||
MockAgent.return_value = Mock()
|
||||
|
||||
await delegator._create_agent(profile, task)
|
||||
|
||||
# Verify Agent was called
|
||||
MockAgent.assert_called_once()
|
||||
call_kwargs = MockAgent.call_args[1]
|
||||
|
||||
# Should NOT have verbose= param
|
||||
assert 'verbose' not in call_kwargs, "Should not use legacy verbose= param"
|
||||
|
||||
# Should have output= param with 'silent' preset
|
||||
assert 'output' in call_kwargs, "Should use consolidated output= param"
|
||||
assert call_kwargs['output'] == 'silent', "Should use output='silent' for quiet mode"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_agent_uses_execution_not_max_iter(self):
|
||||
"""Delegator should use execution= instead of max_iter=."""
|
||||
from praisonaiagents.agents.delegator import SubagentDelegator, DelegationTask
|
||||
from praisonaiagents.agents.profiles import AgentProfile
|
||||
|
||||
delegator = SubagentDelegator()
|
||||
|
||||
profile = AgentProfile(
|
||||
name="test_agent",
|
||||
system_prompt="Test prompt",
|
||||
max_steps=10,
|
||||
)
|
||||
task = DelegationTask(
|
||||
task_id="test-2",
|
||||
agent_name="test_agent",
|
||||
objective="Test task",
|
||||
max_steps=5,
|
||||
)
|
||||
|
||||
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
|
||||
MockAgent.return_value = Mock()
|
||||
|
||||
await delegator._create_agent(profile, task)
|
||||
|
||||
call_kwargs = MockAgent.call_args[1]
|
||||
|
||||
# Should NOT have max_iter= param
|
||||
assert 'max_iter' not in call_kwargs, "Should not use legacy max_iter= param"
|
||||
|
||||
# Should have execution= param with max_iter in config
|
||||
assert 'execution' in call_kwargs, "Should use consolidated execution= param"
|
||||
exec_config = call_kwargs['execution']
|
||||
assert isinstance(exec_config, dict), "execution should be a dict config"
|
||||
assert 'max_iter' in exec_config, "execution config should contain max_iter"
|
||||
assert exec_config['max_iter'] == 5, "max_iter should be min(task.max_steps, profile.max_steps)"
|
||||
|
||||
|
||||
class TestDelegatorBackwardCompatibility:
|
||||
"""Test that delegator still works correctly after modernization."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_agent_returns_valid_agent(self):
|
||||
"""Created agent should be usable."""
|
||||
from praisonaiagents.agents.delegator import SubagentDelegator, DelegationTask
|
||||
from praisonaiagents.agents.profiles import AgentProfile
|
||||
|
||||
delegator = SubagentDelegator()
|
||||
|
||||
profile = AgentProfile(
|
||||
name="test_agent",
|
||||
system_prompt="You are a helpful assistant.",
|
||||
max_steps=10,
|
||||
)
|
||||
task = DelegationTask(
|
||||
task_id="test-3",
|
||||
agent_name="test_agent",
|
||||
objective="Say hello",
|
||||
max_steps=5,
|
||||
)
|
||||
|
||||
with patch('praisonaiagents.agent.agent.Agent') as MockAgent:
|
||||
mock_agent = Mock()
|
||||
mock_agent.chat = Mock(return_value="Hello!")
|
||||
MockAgent.return_value = mock_agent
|
||||
|
||||
agent = await delegator._create_agent(profile, task)
|
||||
|
||||
# Agent should be returned
|
||||
assert agent is not None
|
||||
assert agent == mock_agent
|
||||
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Tests for LearnScope enum - verifying PRIVATE/SHARED values only.
|
||||
|
||||
These tests verify that:
|
||||
1. LearnScope only has PRIVATE and SHARED values
|
||||
2. TEAM and ORG values do NOT exist
|
||||
3. LearnConfig defaults to PRIVATE scope
|
||||
4. BaseStore defaults to "private" scope
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestLearnScopeValues:
|
||||
"""Test LearnScope enum has exactly the right values."""
|
||||
|
||||
def test_learn_scope_has_private(self):
|
||||
"""LearnScope should have PRIVATE value."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert hasattr(LearnScope, 'PRIVATE')
|
||||
assert LearnScope.PRIVATE.value == "private"
|
||||
|
||||
def test_learn_scope_has_shared(self):
|
||||
"""LearnScope should have SHARED value."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert hasattr(LearnScope, 'SHARED')
|
||||
assert LearnScope.SHARED.value == "shared"
|
||||
|
||||
def test_learn_scope_does_not_have_team(self):
|
||||
"""LearnScope should NOT have TEAM value."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert not hasattr(LearnScope, 'TEAM'), "TEAM should be removed from LearnScope"
|
||||
|
||||
def test_learn_scope_does_not_have_org(self):
|
||||
"""LearnScope should NOT have ORG value."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert not hasattr(LearnScope, 'ORG'), "ORG should be removed from LearnScope"
|
||||
|
||||
def test_learn_scope_does_not_have_user(self):
|
||||
"""LearnScope should NOT have USER value (replaced by PRIVATE)."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert not hasattr(LearnScope, 'USER'), "USER should be replaced by PRIVATE"
|
||||
|
||||
def test_learn_scope_does_not_have_global(self):
|
||||
"""LearnScope should NOT have GLOBAL value (replaced by SHARED)."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
assert not hasattr(LearnScope, 'GLOBAL'), "GLOBAL should be replaced by SHARED"
|
||||
|
||||
def test_learn_scope_has_exactly_two_values(self):
|
||||
"""LearnScope should have exactly 2 values: PRIVATE and SHARED."""
|
||||
from praisonaiagents.config.feature_configs import LearnScope
|
||||
|
||||
all_values = list(LearnScope)
|
||||
assert len(all_values) == 2, f"Expected 2 values, got {len(all_values)}: {all_values}"
|
||||
|
||||
|
||||
class TestLearnConfigDefaults:
|
||||
"""Test LearnConfig uses correct default scope."""
|
||||
|
||||
def test_learn_config_default_scope_is_private(self):
|
||||
"""LearnConfig should default to PRIVATE scope."""
|
||||
from praisonaiagents.config.feature_configs import LearnConfig, LearnScope
|
||||
|
||||
config = LearnConfig()
|
||||
assert config.scope == LearnScope.PRIVATE
|
||||
|
||||
def test_learn_config_scope_can_be_set_to_shared(self):
|
||||
"""LearnConfig scope can be set to SHARED."""
|
||||
from praisonaiagents.config.feature_configs import LearnConfig, LearnScope
|
||||
|
||||
config = LearnConfig(scope=LearnScope.SHARED)
|
||||
assert config.scope == LearnScope.SHARED
|
||||
|
||||
def test_learn_config_to_dict_includes_scope(self):
|
||||
"""LearnConfig.to_dict() should include scope with correct value."""
|
||||
from praisonaiagents.config.feature_configs import LearnConfig, LearnScope
|
||||
|
||||
config = LearnConfig()
|
||||
d = config.to_dict()
|
||||
assert d['scope'] == 'private'
|
||||
|
||||
config2 = LearnConfig(scope=LearnScope.SHARED)
|
||||
d2 = config2.to_dict()
|
||||
assert d2['scope'] == 'shared'
|
||||
|
||||
|
||||
class TestBaseStoreDefaults:
|
||||
"""Test BaseStore uses correct default scope."""
|
||||
|
||||
def test_base_store_default_scope_is_private(self):
|
||||
"""BaseStore should default to 'private' scope."""
|
||||
import inspect
|
||||
from praisonaiagents.memory.learn.stores import BaseStore
|
||||
|
||||
sig = inspect.signature(BaseStore.__init__)
|
||||
scope_param = sig.parameters.get('scope')
|
||||
assert scope_param is not None
|
||||
assert scope_param.default == 'private', f"Expected 'private', got '{scope_param.default}'"
|
||||
@@ -0,0 +1,526 @@
|
||||
"""
|
||||
Comprehensive tests for param_resolver.py - TDD for outstanding gaps.
|
||||
|
||||
Tests cover:
|
||||
1. Dict strict validation (dataclass and non-dataclass configs)
|
||||
2. Array PRESET_OVERRIDE invalid preset error UX
|
||||
3. Precedence ladder for all consolidated params
|
||||
4. Agent consolidated params via canonical resolver
|
||||
5. Workflow/WorkflowStep resolution consistency
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test Fixtures - Config Classes
|
||||
# =============================================================================
|
||||
|
||||
@dataclass
|
||||
class MockConfig:
|
||||
"""Mock config for testing."""
|
||||
enabled: bool = True
|
||||
value: str = "default"
|
||||
count: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockConfigWithSources:
|
||||
"""Mock config with sources field."""
|
||||
sources: List[str] = field(default_factory=list)
|
||||
rerank: bool = False
|
||||
|
||||
|
||||
class NonDataclassConfig:
|
||||
"""Non-dataclass config for testing validation."""
|
||||
def __init__(self, enabled: bool = True, value: str = "default"):
|
||||
self.enabled = enabled
|
||||
self.value = value
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 1A: Dict Strict Validation Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestDictStrictValidation:
|
||||
"""Tests for strict dict key validation."""
|
||||
|
||||
def test_dict_unknown_keys_error_dataclass(self):
|
||||
"""Dict with unknown keys should raise TypeError with helpful message."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
resolve(
|
||||
value={"enabled": True, "unknown_key": "bad"},
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
)
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "unknown_key" in error_msg.lower() or "Unknown keys" in error_msg
|
||||
assert "enabled" in error_msg or "Valid keys" in error_msg
|
||||
|
||||
def test_dict_valid_keys_accepted(self):
|
||||
"""Dict with valid keys should create config."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
result = resolve(
|
||||
value={"enabled": False, "value": "custom"},
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockConfig)
|
||||
assert result.enabled is False
|
||||
assert result.value == "custom"
|
||||
|
||||
def test_dict_no_config_class_error(self):
|
||||
"""Dict without config_class should raise TypeError."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
resolve(
|
||||
value={"key": "value"},
|
||||
param_name="test",
|
||||
config_class=None,
|
||||
)
|
||||
|
||||
assert "not supported" in str(exc_info.value).lower() or "no config class" in str(exc_info.value).lower()
|
||||
|
||||
def test_dict_validation_non_dataclass_config(self):
|
||||
"""Non-dataclass config should still validate keys via signature/annotations."""
|
||||
from praisonaiagents.config.param_resolver import _get_valid_keys
|
||||
|
||||
# This tests the _get_valid_keys helper handles non-dataclass
|
||||
# The implementation should use inspect.signature or __annotations__
|
||||
# For now, verify the helper exists and returns something useful
|
||||
keys = _get_valid_keys(NonDataclassConfig)
|
||||
# Should return valid keys or empty (graceful degradation)
|
||||
assert isinstance(keys, (set, list, frozenset)) or keys is None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 1B: Array PRESET_OVERRIDE Error UX Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestArrayPresetOverrideErrorUX:
|
||||
"""Tests for array preset error messages with suggestions."""
|
||||
|
||||
def test_invalid_preset_in_array_raises_error_with_suggestions(self):
|
||||
"""Invalid preset string in array should raise error with typo suggestions."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
presets = {
|
||||
"fast": {"enabled": True, "value": "fast"},
|
||||
"slow": {"enabled": True, "value": "slow"},
|
||||
"balanced": {"enabled": True, "value": "balanced"},
|
||||
}
|
||||
|
||||
with pytest.raises((ValueError, TypeError)) as exc_info:
|
||||
resolve(
|
||||
value=["fats"], # Typo for "fast"
|
||||
param_name="execution",
|
||||
config_class=MockConfig,
|
||||
presets=presets,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
error_msg = str(exc_info.value).lower()
|
||||
# Should suggest similar preset
|
||||
assert "fast" in error_msg or "fats" in error_msg
|
||||
# Should list valid presets
|
||||
assert "valid" in error_msg or "available" in error_msg or any(p in error_msg for p in presets.keys())
|
||||
|
||||
def test_valid_preset_in_array_works(self):
|
||||
"""Valid preset string in array should work."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
presets = {
|
||||
"fast": {"enabled": True, "value": "fast"},
|
||||
}
|
||||
|
||||
result = resolve(
|
||||
value=["fast"],
|
||||
param_name="execution",
|
||||
config_class=MockConfig,
|
||||
presets=presets,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockConfig)
|
||||
assert result.value == "fast"
|
||||
|
||||
def test_preset_with_overrides_in_array(self):
|
||||
"""Preset with override dict in array should merge correctly."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
presets = {
|
||||
"fast": {"enabled": True, "value": "fast", "count": 1},
|
||||
}
|
||||
|
||||
result = resolve(
|
||||
value=["fast", {"count": 99}],
|
||||
param_name="execution",
|
||||
config_class=MockConfig,
|
||||
presets=presets,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockConfig)
|
||||
assert result.value == "fast"
|
||||
assert result.count == 99
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 1C: Precedence Ladder Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestPrecedenceLadder:
|
||||
"""Tests for Instance > Config > Dict > Array > String > Bool > Default precedence."""
|
||||
|
||||
def test_instance_highest_precedence(self):
|
||||
"""Instance check should have highest precedence."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
class MockInstance:
|
||||
def search(self): pass
|
||||
def add(self): pass
|
||||
|
||||
instance = MockInstance()
|
||||
result = resolve(
|
||||
value=instance,
|
||||
param_name="memory",
|
||||
config_class=MockConfig,
|
||||
instance_check=lambda v: hasattr(v, 'search') and hasattr(v, 'add'),
|
||||
)
|
||||
|
||||
assert result is instance
|
||||
|
||||
def test_config_class_instance_precedence(self):
|
||||
"""Config class instance should be returned as-is."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
config = MockConfig(enabled=False, value="custom")
|
||||
result = resolve(
|
||||
value=config,
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
)
|
||||
|
||||
assert result is config
|
||||
|
||||
def test_dict_before_string(self):
|
||||
"""Dict should be processed before string."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
# Dict input
|
||||
result = resolve(
|
||||
value={"enabled": False},
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockConfig)
|
||||
assert result.enabled is False
|
||||
|
||||
def test_bool_true_returns_default_config(self):
|
||||
"""Bool True should return default config instance."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
result = resolve(
|
||||
value=True,
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockConfig)
|
||||
assert result.enabled is True # Default
|
||||
|
||||
def test_bool_false_returns_none(self):
|
||||
"""Bool False should return None (disabled)."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
result = resolve(
|
||||
value=False,
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_none_returns_default(self):
|
||||
"""None should return the default value."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
default = MockConfig(value="default_value")
|
||||
result = resolve(
|
||||
value=None,
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
default=default,
|
||||
)
|
||||
|
||||
assert result is default
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 1D: Agent Consolidated Params Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestAgentConsolidatedParams:
|
||||
"""Tests for Agent consolidated params using canonical resolver."""
|
||||
|
||||
def test_agent_hooks_via_resolver(self):
|
||||
"""Agent hooks param should use canonical resolver."""
|
||||
from praisonaiagents.config.feature_configs import HooksConfig
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
# Test list passthrough
|
||||
hooks_list = [lambda x: x]
|
||||
result = resolve(
|
||||
value=hooks_list,
|
||||
param_name="hooks",
|
||||
config_class=HooksConfig,
|
||||
array_mode=ArrayMode.PASSTHROUGH,
|
||||
)
|
||||
assert result == hooks_list
|
||||
|
||||
# Test HooksConfig instance
|
||||
hooks_config = HooksConfig(on_step=lambda x: x)
|
||||
result = resolve(
|
||||
value=hooks_config,
|
||||
param_name="hooks",
|
||||
config_class=HooksConfig,
|
||||
)
|
||||
assert result is hooks_config
|
||||
|
||||
# Test dict
|
||||
result = resolve(
|
||||
value={"on_step": None},
|
||||
param_name="hooks",
|
||||
config_class=HooksConfig,
|
||||
)
|
||||
assert isinstance(result, HooksConfig)
|
||||
|
||||
def test_agent_skills_via_resolver(self):
|
||||
"""Agent skills param should use canonical resolver."""
|
||||
from praisonaiagents.config.feature_configs import SkillsConfig
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
# Test SkillsConfig instance directly
|
||||
config = SkillsConfig(paths=["./skill1", "./skill2"])
|
||||
result = resolve(
|
||||
value=config,
|
||||
param_name="skills",
|
||||
config_class=SkillsConfig,
|
||||
)
|
||||
assert isinstance(result, SkillsConfig)
|
||||
assert result.paths == ["./skill1", "./skill2"]
|
||||
|
||||
# Test dict input
|
||||
result = resolve(
|
||||
value={"paths": ["./my-skill"]},
|
||||
param_name="skills",
|
||||
config_class=SkillsConfig,
|
||||
)
|
||||
assert isinstance(result, SkillsConfig)
|
||||
assert result.paths == ["./my-skill"]
|
||||
|
||||
def test_agent_knowledge_via_resolver(self):
|
||||
"""Agent knowledge param should use canonical resolver."""
|
||||
from praisonaiagents.config.feature_configs import KnowledgeConfig
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
# Test list of sources
|
||||
result = resolve(
|
||||
value=["docs/", "data.pdf"],
|
||||
param_name="knowledge",
|
||||
config_class=KnowledgeConfig,
|
||||
array_mode=ArrayMode.SOURCES_WITH_CONFIG,
|
||||
)
|
||||
assert isinstance(result, KnowledgeConfig)
|
||||
assert "docs/" in result.sources
|
||||
|
||||
# Test list with config override
|
||||
result = resolve(
|
||||
value=["docs/", {"rerank": True}],
|
||||
param_name="knowledge",
|
||||
config_class=KnowledgeConfig,
|
||||
array_mode=ArrayMode.SOURCES_WITH_CONFIG,
|
||||
)
|
||||
assert isinstance(result, KnowledgeConfig)
|
||||
assert result.rerank is True
|
||||
|
||||
def test_agent_guardrails_via_resolver(self):
|
||||
"""Agent guardrails param should use canonical resolver."""
|
||||
from praisonaiagents.config.feature_configs import GuardrailConfig
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.presets import GUARDRAIL_PRESETS
|
||||
|
||||
# Test string preset
|
||||
if GUARDRAIL_PRESETS:
|
||||
preset_name = list(GUARDRAIL_PRESETS.keys())[0]
|
||||
result = resolve(
|
||||
value=preset_name,
|
||||
param_name="guardrails",
|
||||
config_class=GuardrailConfig,
|
||||
presets=GUARDRAIL_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
assert result is not None
|
||||
|
||||
# Test callable (should be handled via instance_check)
|
||||
def validator_fn(x):
|
||||
return (True, x)
|
||||
|
||||
result = resolve(
|
||||
value=validator_fn,
|
||||
param_name="guardrails",
|
||||
config_class=GuardrailConfig,
|
||||
instance_check=callable,
|
||||
)
|
||||
assert result is validator_fn
|
||||
|
||||
# Test GuardrailConfig instance
|
||||
config = GuardrailConfig(max_retries=5)
|
||||
result = resolve(
|
||||
value=config,
|
||||
param_name="guardrails",
|
||||
config_class=GuardrailConfig,
|
||||
)
|
||||
assert result is config
|
||||
|
||||
# Test string as LLM prompt
|
||||
result = resolve(
|
||||
value="Ensure response is safe and helpful",
|
||||
param_name="guardrails",
|
||||
config_class=GuardrailConfig,
|
||||
string_mode="llm_prompt",
|
||||
)
|
||||
assert isinstance(result, GuardrailConfig)
|
||||
assert result.llm_validator == "Ensure response is safe and helpful"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 1E: Workflow/WorkflowStep Resolution Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestWorkflowResolution:
|
||||
"""Tests for Workflow and WorkflowStep resolution consistency."""
|
||||
|
||||
def test_workflow_step_context_resolved(self):
|
||||
"""WorkflowStep context should be resolved to config."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(
|
||||
name="test",
|
||||
context=["step1", "step2"],
|
||||
)
|
||||
|
||||
# Should have resolved _context_from
|
||||
assert step._context_from == ["step1", "step2"]
|
||||
|
||||
def test_workflow_step_execution_resolved(self):
|
||||
"""WorkflowStep execution should be resolved to config."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(
|
||||
name="test",
|
||||
execution="fast",
|
||||
)
|
||||
|
||||
# Should have resolved execution config
|
||||
assert step._async_execution is not None or step._max_retries is not None
|
||||
|
||||
def test_workflow_step_output_resolved(self):
|
||||
"""WorkflowStep output should be resolved to config."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
from praisonaiagents.workflows.workflow_configs import WorkflowStepOutputConfig
|
||||
|
||||
# Test with config object
|
||||
step = WorkflowStep(
|
||||
name="test",
|
||||
output=WorkflowStepOutputConfig(file="result.txt"),
|
||||
)
|
||||
|
||||
# Should have resolved output file
|
||||
assert step._output_file == "result.txt"
|
||||
|
||||
def test_workflow_output_resolved(self):
|
||||
"""Workflow output should be resolved to config."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(
|
||||
name="test",
|
||||
output="verbose",
|
||||
)
|
||||
|
||||
# Should have resolved verbose setting
|
||||
assert workflow._verbose is True
|
||||
|
||||
def test_workflow_planning_resolved(self):
|
||||
"""Workflow planning should be resolved to config."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(
|
||||
name="test",
|
||||
planning=True,
|
||||
)
|
||||
|
||||
# Should have resolved planning enabled
|
||||
assert workflow._planning_enabled is True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 1F: Naming/Alias Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestNamingAlias:
|
||||
"""Tests for Agents vs PraisonAIAgents naming."""
|
||||
|
||||
def test_agents_is_primary(self):
|
||||
"""Agents should be the primary import."""
|
||||
from praisonaiagents import Agents
|
||||
assert Agents is not None
|
||||
|
||||
def test_praisonaiagents_alias_works(self):
|
||||
"""PraisonAIAgents alias should still work."""
|
||||
from praisonaiagents import PraisonAIAgents
|
||||
assert PraisonAIAgents is not None
|
||||
|
||||
def test_both_are_same_class(self):
|
||||
"""Both names should refer to the same class."""
|
||||
from praisonaiagents import Agents, PraisonAIAgents
|
||||
assert Agents is PraisonAIAgents
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 1G: Helper Function Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestHelperFunctions:
|
||||
"""Tests for resolver helper functions."""
|
||||
|
||||
def test_get_valid_keys_dataclass(self):
|
||||
"""_get_valid_keys should return dataclass fields."""
|
||||
from praisonaiagents.config.param_resolver import _get_valid_keys
|
||||
|
||||
keys = _get_valid_keys(MockConfig)
|
||||
assert "enabled" in keys
|
||||
assert "value" in keys
|
||||
assert "count" in keys
|
||||
|
||||
def test_get_valid_keys_non_dataclass(self):
|
||||
"""_get_valid_keys should handle non-dataclass via signature."""
|
||||
from praisonaiagents.config.param_resolver import _get_valid_keys
|
||||
|
||||
keys = _get_valid_keys(NonDataclassConfig)
|
||||
# Should return keys from __init__ signature or __annotations__
|
||||
# or gracefully return empty/None
|
||||
assert keys is None or isinstance(keys, (set, list, frozenset))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -0,0 +1,237 @@
|
||||
"""Tests for precedence ladder resolvers."""
|
||||
|
||||
import pytest
|
||||
from praisonaiagents.config.feature_configs import (
|
||||
MemoryConfig,
|
||||
MemoryBackend,
|
||||
KnowledgeConfig,
|
||||
PlanningConfig,
|
||||
ReflectionConfig,
|
||||
GuardrailConfig,
|
||||
WebConfig,
|
||||
OutputConfig,
|
||||
ExecutionConfig,
|
||||
CachingConfig,
|
||||
AutonomyConfig,
|
||||
resolve_memory,
|
||||
resolve_knowledge,
|
||||
resolve_planning,
|
||||
resolve_reflection,
|
||||
resolve_guardrails,
|
||||
resolve_web,
|
||||
resolve_output,
|
||||
resolve_execution,
|
||||
resolve_caching,
|
||||
resolve_autonomy,
|
||||
)
|
||||
|
||||
|
||||
class TestResolveMemory:
|
||||
"""Test resolve_memory precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
"""Default (None) returns None."""
|
||||
assert resolve_memory(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
"""False returns None (disabled)."""
|
||||
assert resolve_memory(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
"""True returns MemoryConfig with defaults."""
|
||||
result = resolve_memory(True)
|
||||
assert isinstance(result, MemoryConfig)
|
||||
assert result.backend == MemoryBackend.FILE
|
||||
|
||||
def test_string_backend(self):
|
||||
"""String returns MemoryConfig with that backend."""
|
||||
result = resolve_memory("redis")
|
||||
assert isinstance(result, MemoryConfig)
|
||||
assert result.backend == MemoryBackend.REDIS
|
||||
|
||||
def test_string_custom_backend(self):
|
||||
"""Custom string backend is preserved."""
|
||||
result = resolve_memory("custom_backend")
|
||||
assert isinstance(result, MemoryConfig)
|
||||
assert result.backend == "custom_backend"
|
||||
|
||||
def test_dict_expansion(self):
|
||||
"""Dict expands to MemoryConfig."""
|
||||
result = resolve_memory({
|
||||
"backend": "sqlite",
|
||||
"user_id": "alice",
|
||||
"auto_memory": True,
|
||||
})
|
||||
assert isinstance(result, MemoryConfig)
|
||||
assert result.backend == MemoryBackend.SQLITE
|
||||
assert result.user_id == "alice"
|
||||
assert result.auto_memory is True
|
||||
|
||||
def test_config_passthrough(self):
|
||||
"""MemoryConfig passes through unchanged."""
|
||||
config = MemoryConfig(backend=MemoryBackend.POSTGRES, user_id="bob")
|
||||
result = resolve_memory(config)
|
||||
assert result is config
|
||||
|
||||
def test_instance_passthrough(self):
|
||||
"""Instance (non-config object) passes through."""
|
||||
class MockMemoryManager:
|
||||
pass
|
||||
instance = MockMemoryManager()
|
||||
result = resolve_memory(instance)
|
||||
assert result is instance
|
||||
|
||||
|
||||
class TestResolveKnowledge:
|
||||
"""Test resolve_knowledge precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_knowledge(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_knowledge(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_knowledge(True)
|
||||
assert isinstance(result, KnowledgeConfig)
|
||||
|
||||
def test_string_single_source(self):
|
||||
result = resolve_knowledge("docs/")
|
||||
assert isinstance(result, KnowledgeConfig)
|
||||
assert result.sources == ["docs/"]
|
||||
|
||||
def test_array_sources(self):
|
||||
result = resolve_knowledge(["docs/", "data.pdf"])
|
||||
assert isinstance(result, KnowledgeConfig)
|
||||
assert result.sources == ["docs/", "data.pdf"]
|
||||
|
||||
def test_dict_expansion(self):
|
||||
result = resolve_knowledge({
|
||||
"sources": ["docs/"],
|
||||
"rerank": True,
|
||||
})
|
||||
assert isinstance(result, KnowledgeConfig)
|
||||
assert result.sources == ["docs/"]
|
||||
assert result.rerank is True
|
||||
|
||||
def test_config_passthrough(self):
|
||||
config = KnowledgeConfig(sources=["test/"])
|
||||
result = resolve_knowledge(config)
|
||||
assert result is config
|
||||
|
||||
|
||||
class TestResolvePlanning:
|
||||
"""Test resolve_planning precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_planning(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_planning(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_planning(True)
|
||||
assert isinstance(result, PlanningConfig)
|
||||
|
||||
def test_config_passthrough(self):
|
||||
config = PlanningConfig()
|
||||
result = resolve_planning(config)
|
||||
assert result is config
|
||||
|
||||
|
||||
class TestResolveReflection:
|
||||
"""Test resolve_reflection precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_reflection(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_reflection(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_reflection(True)
|
||||
assert isinstance(result, ReflectionConfig)
|
||||
|
||||
|
||||
class TestResolveGuardrails:
|
||||
"""Test resolve_guardrails precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_guardrails(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_guardrails(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_guardrails(True)
|
||||
assert isinstance(result, GuardrailConfig)
|
||||
|
||||
def test_callable_validator(self):
|
||||
def my_validator(output):
|
||||
return True, output
|
||||
result = resolve_guardrails(my_validator)
|
||||
assert isinstance(result, GuardrailConfig)
|
||||
assert result.validator is my_validator
|
||||
|
||||
|
||||
class TestResolveWeb:
|
||||
"""Test resolve_web precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_web(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_web(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_web(True)
|
||||
assert isinstance(result, WebConfig)
|
||||
|
||||
|
||||
class TestResolveCaching:
|
||||
"""Test resolve_caching precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_caching(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_caching(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_caching(True)
|
||||
assert isinstance(result, CachingConfig)
|
||||
|
||||
|
||||
class TestResolveAutonomy:
|
||||
"""Test resolve_autonomy precedence ladder."""
|
||||
|
||||
def test_default_none(self):
|
||||
assert resolve_autonomy(None) is None
|
||||
|
||||
def test_bool_false(self):
|
||||
assert resolve_autonomy(False) is None
|
||||
|
||||
def test_bool_true(self):
|
||||
result = resolve_autonomy(True)
|
||||
assert isinstance(result, AutonomyConfig)
|
||||
|
||||
|
||||
class TestNoCortexParam:
|
||||
"""Test that cortex parameter does not exist."""
|
||||
|
||||
def test_agent_has_no_cortex_param(self):
|
||||
"""Agent.__init__ should not have cortex parameter."""
|
||||
import inspect
|
||||
from praisonaiagents import Agent
|
||||
|
||||
sig = inspect.signature(Agent.__init__)
|
||||
param_names = list(sig.parameters.keys())
|
||||
|
||||
assert "cortex" not in param_names, "cortex param should be removed from Agent"
|
||||
|
||||
def test_no_cortex_module_in_agents(self):
|
||||
"""praisonaiagents should not have cortex module."""
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.find_spec("praisonaiagents.cortex")
|
||||
assert spec is None, "praisonaiagents.cortex module should not exist"
|
||||
@@ -0,0 +1,720 @@
|
||||
"""
|
||||
TDD Tests for Resolver Standardization.
|
||||
|
||||
These tests verify:
|
||||
1. Precedence order: None → Instance → Config → Array → Dict → String → Bool → Default
|
||||
2. Memory array semantics: single=ok, multiple=error
|
||||
3. Caching array support with PRESET_OVERRIDE
|
||||
4. Workflow/WorkflowStep using canonical resolve
|
||||
5. CLI resolver integration
|
||||
6. Naming standardization (guardrails plural)
|
||||
7. All precedence levels per consolidated param
|
||||
|
||||
Run with: pytest tests/unit/config/test_resolver_standardization.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test Fixtures - Config Classes
|
||||
# =============================================================================
|
||||
|
||||
@dataclass
|
||||
class MockConfig:
|
||||
"""Mock config class for testing."""
|
||||
enabled: bool = True
|
||||
value: str = "default"
|
||||
count: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockMemoryConfig:
|
||||
"""Mock memory config for testing."""
|
||||
backend: str = "file"
|
||||
user_id: Optional[str] = None
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
sources: Optional[List[str]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockCachingConfig:
|
||||
"""Mock caching config for testing."""
|
||||
enabled: bool = True
|
||||
ttl: int = 3600
|
||||
prompt_caching: bool = False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TODO 1.1: Tests for Precedence Order (Array BEFORE Dict)
|
||||
# =============================================================================
|
||||
|
||||
class TestPrecedenceOrder:
|
||||
"""Test that precedence order is: None → Instance → Config → Array → Dict → String → Bool → Default"""
|
||||
|
||||
def test_array_takes_precedence_over_dict_interpretation(self):
|
||||
"""Array input should be processed as array, not as dict."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
# When given an array, it should use array handling, not dict handling
|
||||
result = resolve(
|
||||
value=["preset1"],
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
presets={"preset1": {"enabled": True, "value": "from_preset"}},
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
# Should resolve via array path (preset), not dict path
|
||||
assert isinstance(result, MockConfig)
|
||||
assert result.value == "from_preset"
|
||||
|
||||
def test_precedence_instance_over_config(self):
|
||||
"""Instance check should take precedence over config class check."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
class CustomInstance:
|
||||
def __init__(self):
|
||||
self.is_custom = True
|
||||
|
||||
custom = CustomInstance()
|
||||
|
||||
result = resolve(
|
||||
value=custom,
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
instance_check=lambda v: hasattr(v, 'is_custom'),
|
||||
)
|
||||
|
||||
# Should return instance as-is, not try to convert
|
||||
assert result is custom
|
||||
assert result.is_custom is True
|
||||
|
||||
def test_precedence_config_over_array(self):
|
||||
"""Config class instance should take precedence over array handling."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
config = MockConfig(enabled=False, value="config_value")
|
||||
|
||||
result = resolve(
|
||||
value=config,
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
# Should return config as-is
|
||||
assert result is config
|
||||
assert result.value == "config_value"
|
||||
|
||||
def test_precedence_array_over_dict(self):
|
||||
"""Array should be checked BEFORE dict in precedence."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
# This test verifies the fix: Array must be checked before Dict
|
||||
# A list input should go through array handling, not dict handling
|
||||
result = resolve(
|
||||
value=["verbose", {"count": 5}],
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
presets={"verbose": {"enabled": True, "value": "verbose_mode"}},
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockConfig)
|
||||
assert result.value == "verbose_mode"
|
||||
assert result.count == 5 # Override applied
|
||||
|
||||
def test_precedence_dict_over_string(self):
|
||||
"""Dict should take precedence over string handling."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
result = resolve(
|
||||
value={"enabled": False, "value": "dict_value"},
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
presets={"dict_value": {"enabled": True}}, # Should NOT use this
|
||||
)
|
||||
|
||||
assert isinstance(result, MockConfig)
|
||||
assert result.value == "dict_value"
|
||||
assert result.enabled is False
|
||||
|
||||
def test_precedence_string_over_bool(self):
|
||||
"""String preset should take precedence over bool handling."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
result = resolve(
|
||||
value="verbose",
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
presets={"verbose": {"enabled": True, "value": "verbose_mode"}},
|
||||
)
|
||||
|
||||
assert isinstance(result, MockConfig)
|
||||
assert result.value == "verbose_mode"
|
||||
|
||||
def test_precedence_bool_over_default(self):
|
||||
"""Bool True should create default config, not return default value."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
result = resolve(
|
||||
value=True,
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
default={"should": "not_use"},
|
||||
)
|
||||
|
||||
assert isinstance(result, MockConfig)
|
||||
# Should be default MockConfig(), not the default dict
|
||||
|
||||
def test_precedence_none_returns_default(self):
|
||||
"""None should return the default value."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
default_config = MockConfig(value="default_value")
|
||||
|
||||
result = resolve(
|
||||
value=None,
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
default=default_config,
|
||||
)
|
||||
|
||||
assert result is default_config
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TODO 1.2: Tests for Memory Array Semantics
|
||||
# =============================================================================
|
||||
|
||||
class TestMemoryArraySemantics:
|
||||
"""Test memory array semantics: single=ok, multiple=error."""
|
||||
|
||||
def test_memory_single_item_array_resolves_as_scalar(self):
|
||||
"""memory=["redis"] should resolve as scalar preset."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
result = resolve(
|
||||
value=["redis"],
|
||||
param_name="memory",
|
||||
config_class=MockMemoryConfig,
|
||||
presets={"redis": {"backend": "redis"}},
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockMemoryConfig)
|
||||
assert result.backend == "redis"
|
||||
|
||||
def test_memory_multiple_items_raises_error(self):
|
||||
"""memory=["redis", "file"] should raise TypeError."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
resolve(
|
||||
value=["redis", "file"],
|
||||
param_name="memory",
|
||||
config_class=MockMemoryConfig,
|
||||
presets={"redis": {"backend": "redis"}, "file": {"backend": "file"}},
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST,
|
||||
)
|
||||
|
||||
# Error message should be helpful
|
||||
assert "memory" in str(exc_info.value).lower()
|
||||
assert "multiple" in str(exc_info.value).lower() or "single" in str(exc_info.value).lower()
|
||||
|
||||
def test_memory_single_url_in_array_resolves(self):
|
||||
"""memory=["postgresql://..."] should resolve as URL."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
result = resolve(
|
||||
value=["postgresql://localhost/db"],
|
||||
param_name="memory",
|
||||
config_class=MockMemoryConfig,
|
||||
url_schemes={"postgresql": "postgres"},
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockMemoryConfig)
|
||||
assert result.backend == "postgres"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TODO 1.3: Tests for Caching Array Support
|
||||
# =============================================================================
|
||||
|
||||
class TestCachingArraySupport:
|
||||
"""Test caching array support with PRESET_OVERRIDE."""
|
||||
|
||||
def test_caching_array_preset_override(self):
|
||||
"""caching=["enabled", {"ttl": 7200}] should work."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
result = resolve(
|
||||
value=["enabled", {"ttl": 7200}],
|
||||
param_name="caching",
|
||||
config_class=MockCachingConfig,
|
||||
presets={"enabled": {"enabled": True, "ttl": 3600}},
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockCachingConfig)
|
||||
assert result.enabled is True
|
||||
assert result.ttl == 7200 # Override applied
|
||||
|
||||
def test_caching_string_preset(self):
|
||||
"""caching="enabled" should work."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
|
||||
result = resolve(
|
||||
value="enabled",
|
||||
param_name="caching",
|
||||
config_class=MockCachingConfig,
|
||||
presets={"enabled": {"enabled": True}},
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockCachingConfig)
|
||||
assert result.enabled is True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TODO 1.4: Tests for Workflow/WorkflowStep Using Canonical Resolve
|
||||
# =============================================================================
|
||||
|
||||
class TestWorkflowCanonicalResolve:
|
||||
"""Test that Workflow and WorkflowStep use canonical resolve directly."""
|
||||
|
||||
def test_workflow_output_uses_canonical_resolve(self):
|
||||
"""Workflow output param should use canonical resolve."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(
|
||||
name="test",
|
||||
steps=[],
|
||||
output="verbose",
|
||||
)
|
||||
|
||||
assert workflow._verbose is True
|
||||
|
||||
def test_workflow_output_dict_validation(self):
|
||||
"""Workflow output dict should have strict validation."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
Workflow(
|
||||
name="test",
|
||||
steps=[],
|
||||
output={"invalid_key": True},
|
||||
)
|
||||
|
||||
assert "invalid_key" in str(exc_info.value).lower() or "unknown" in str(exc_info.value).lower()
|
||||
|
||||
def test_workflowstep_context_uses_canonical_resolve(self):
|
||||
"""WorkflowStep context param should use canonical resolve."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(
|
||||
name="test",
|
||||
context=["step1", "step2"],
|
||||
)
|
||||
|
||||
assert step._context_from == ["step1", "step2"]
|
||||
|
||||
def test_workflowstep_execution_preset(self):
|
||||
"""WorkflowStep execution preset should work."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(
|
||||
name="test",
|
||||
execution="thorough",
|
||||
)
|
||||
|
||||
# Should have thorough preset values
|
||||
assert step._max_retries >= 3 # Thorough has more retries
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TODO 1.5: Tests for CLI Resolver Integration
|
||||
# =============================================================================
|
||||
|
||||
class TestCLIResolverIntegration:
|
||||
"""Test CLI flags use canonical resolver."""
|
||||
|
||||
def test_cli_memory_flag_true(self):
|
||||
"""CLI --memory flag (true) should resolve correctly."""
|
||||
# This tests the integration point - CLI should pass value to canonical resolver
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.presets import MEMORY_PRESETS, MEMORY_URL_SCHEMES
|
||||
|
||||
# Simulate CLI passing True
|
||||
result = resolve(
|
||||
value=True,
|
||||
param_name="memory",
|
||||
config_class=MockMemoryConfig,
|
||||
presets=MEMORY_PRESETS,
|
||||
url_schemes=MEMORY_URL_SCHEMES,
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockMemoryConfig)
|
||||
|
||||
def test_cli_memory_flag_preset_string(self):
|
||||
"""CLI --memory=redis should resolve as preset."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.presets import MEMORY_PRESETS, MEMORY_URL_SCHEMES
|
||||
|
||||
result = resolve(
|
||||
value="redis",
|
||||
param_name="memory",
|
||||
config_class=MockMemoryConfig,
|
||||
presets=MEMORY_PRESETS,
|
||||
url_schemes=MEMORY_URL_SCHEMES,
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockMemoryConfig)
|
||||
assert result.backend == "redis"
|
||||
|
||||
def test_cli_memory_flag_url(self):
|
||||
"""CLI --memory=postgresql://... should resolve as URL."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.presets import MEMORY_PRESETS, MEMORY_URL_SCHEMES
|
||||
|
||||
result = resolve(
|
||||
value="postgresql://localhost/db",
|
||||
param_name="memory",
|
||||
config_class=MockMemoryConfig,
|
||||
presets=MEMORY_PRESETS,
|
||||
url_schemes=MEMORY_URL_SCHEMES,
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST,
|
||||
)
|
||||
|
||||
assert isinstance(result, MockMemoryConfig)
|
||||
assert result.backend == "postgres"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TODO 1.6: Tests for Naming Standardization (guardrails plural)
|
||||
# =============================================================================
|
||||
|
||||
class TestNamingStandardization:
|
||||
"""Test guardrail → guardrails naming standardization."""
|
||||
|
||||
def test_workflowstep_accepts_guardrails_plural(self):
|
||||
"""WorkflowStep should accept guardrails= (plural)."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
def my_guardrail(result):
|
||||
return (True, result)
|
||||
|
||||
# Should accept guardrails= (plural)
|
||||
step = WorkflowStep(
|
||||
name="test",
|
||||
guardrails=my_guardrail,
|
||||
)
|
||||
|
||||
# Should store in guardrails attribute
|
||||
assert step.guardrails is my_guardrail or step.guardrail is my_guardrail
|
||||
|
||||
def test_workflowstep_guardrail_singular_compat(self):
|
||||
"""WorkflowStep should accept guardrail= (singular) for compatibility."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
def my_guardrail(result):
|
||||
return (True, result)
|
||||
|
||||
# Should still accept guardrail= (singular) for backward compat
|
||||
step = WorkflowStep(
|
||||
name="test",
|
||||
guardrail=my_guardrail,
|
||||
)
|
||||
|
||||
# Should work (backward compat)
|
||||
assert step.guardrail is my_guardrail
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TODO 1.7: Tests for All Precedence Levels Per Consolidated Param
|
||||
# =============================================================================
|
||||
|
||||
class TestAllPrecedenceLevels:
|
||||
"""Test each consolidated param has all 7 precedence levels working."""
|
||||
|
||||
def test_output_all_precedence_levels(self):
|
||||
"""Test output param at all precedence levels."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
# 1. None → Default
|
||||
result = resolve(value=None, param_name="output", config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS, default=OutputConfig())
|
||||
assert isinstance(result, OutputConfig)
|
||||
|
||||
# 2. Instance (custom object with expected attributes)
|
||||
class CustomOutput:
|
||||
verbose = True
|
||||
stream = True
|
||||
custom = CustomOutput()
|
||||
result = resolve(value=custom, param_name="output", config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS, instance_check=lambda v: hasattr(v, 'verbose'))
|
||||
assert result is custom
|
||||
|
||||
# 3. Config class instance
|
||||
config = OutputConfig(verbose=True, stream=False)
|
||||
result = resolve(value=config, param_name="output", config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS)
|
||||
assert result is config
|
||||
|
||||
# 4. Array [preset, overrides]
|
||||
result = resolve(value=["verbose", {"stream": False}], param_name="output",
|
||||
config_class=OutputConfig, presets=OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE)
|
||||
assert isinstance(result, OutputConfig)
|
||||
|
||||
# 5. Dict
|
||||
result = resolve(value={"verbose": True, "stream": False}, param_name="output",
|
||||
config_class=OutputConfig, presets=OUTPUT_PRESETS)
|
||||
assert isinstance(result, OutputConfig)
|
||||
assert result.verbose is True
|
||||
|
||||
# 6. String preset
|
||||
result = resolve(value="verbose", param_name="output", config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS)
|
||||
assert isinstance(result, OutputConfig)
|
||||
|
||||
# 7. Bool True
|
||||
result = resolve(value=True, param_name="output", config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS)
|
||||
assert isinstance(result, OutputConfig)
|
||||
|
||||
# 8. Bool False → None
|
||||
result = resolve(value=False, param_name="output", config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS)
|
||||
assert result is None
|
||||
|
||||
def test_memory_all_precedence_levels(self):
|
||||
"""Test memory param at all precedence levels."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.feature_configs import MemoryConfig
|
||||
from praisonaiagents.config.presets import MEMORY_PRESETS, MEMORY_URL_SCHEMES
|
||||
|
||||
# 1. None → Default
|
||||
result = resolve(value=None, param_name="memory", config_class=MemoryConfig,
|
||||
presets=MEMORY_PRESETS, url_schemes=MEMORY_URL_SCHEMES,
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST, default=None)
|
||||
assert result is None
|
||||
|
||||
# 2. Config class instance
|
||||
config = MemoryConfig(backend="redis")
|
||||
result = resolve(value=config, param_name="memory", config_class=MemoryConfig,
|
||||
presets=MEMORY_PRESETS, url_schemes=MEMORY_URL_SCHEMES,
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST)
|
||||
assert result is config
|
||||
|
||||
# 3. Dict
|
||||
result = resolve(value={"backend": "sqlite"}, param_name="memory",
|
||||
config_class=MemoryConfig, presets=MEMORY_PRESETS,
|
||||
url_schemes=MEMORY_URL_SCHEMES, array_mode=ArrayMode.SINGLE_OR_LIST)
|
||||
assert isinstance(result, MemoryConfig)
|
||||
assert result.backend == "sqlite"
|
||||
|
||||
# 4. String preset
|
||||
result = resolve(value="redis", param_name="memory", config_class=MemoryConfig,
|
||||
presets=MEMORY_PRESETS, url_schemes=MEMORY_URL_SCHEMES,
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST)
|
||||
assert isinstance(result, MemoryConfig)
|
||||
|
||||
# 5. String URL
|
||||
result = resolve(value="postgresql://localhost/db", param_name="memory",
|
||||
config_class=MemoryConfig, presets=MEMORY_PRESETS,
|
||||
url_schemes=MEMORY_URL_SCHEMES, array_mode=ArrayMode.SINGLE_OR_LIST)
|
||||
assert isinstance(result, MemoryConfig)
|
||||
|
||||
# 6. Bool True
|
||||
result = resolve(value=True, param_name="memory", config_class=MemoryConfig,
|
||||
presets=MEMORY_PRESETS, url_schemes=MEMORY_URL_SCHEMES,
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST)
|
||||
assert isinstance(result, MemoryConfig)
|
||||
|
||||
# 7. Bool False → None
|
||||
result = resolve(value=False, param_name="memory", config_class=MemoryConfig,
|
||||
presets=MEMORY_PRESETS, url_schemes=MEMORY_URL_SCHEMES,
|
||||
array_mode=ArrayMode.SINGLE_OR_LIST)
|
||||
assert result is None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tests for Error Messages and Typo Suggestions
|
||||
# =============================================================================
|
||||
|
||||
class TestErrorMessages:
|
||||
"""Test error messages are helpful with typo suggestions."""
|
||||
|
||||
def test_invalid_preset_suggests_similar(self):
|
||||
"""Invalid preset should suggest similar valid presets."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
resolve(
|
||||
value="verbos", # Typo
|
||||
param_name="output",
|
||||
config_class=MockConfig,
|
||||
presets={"verbose": {}, "minimal": {}, "debug": {}},
|
||||
)
|
||||
|
||||
error_msg = str(exc_info.value).lower()
|
||||
assert "verbose" in error_msg # Should suggest "verbose"
|
||||
|
||||
def test_unknown_dict_keys_lists_valid_keys(self):
|
||||
"""Unknown dict keys should list valid keys."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
resolve(
|
||||
value={"invalid_key": True},
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
)
|
||||
|
||||
error_msg = str(exc_info.value).lower()
|
||||
assert "invalid_key" in error_msg or "unknown" in error_msg
|
||||
assert "enabled" in error_msg or "valid" in error_msg
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tests for Strict Dict Validation
|
||||
# =============================================================================
|
||||
|
||||
class TestStrictDictValidation:
|
||||
"""Test strict dict validation."""
|
||||
|
||||
def test_dict_without_config_class_raises_error(self):
|
||||
"""Dict input without config_class should raise TypeError."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
resolve(
|
||||
value={"key": "value"},
|
||||
param_name="test",
|
||||
config_class=None, # No config class
|
||||
)
|
||||
|
||||
assert "dict" in str(exc_info.value).lower()
|
||||
|
||||
def test_dict_with_unknown_keys_raises_error(self):
|
||||
"""Dict with unknown keys should raise TypeError."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
resolve(
|
||||
value={"enabled": True, "unknown_field": "value"},
|
||||
param_name="test",
|
||||
config_class=MockConfig,
|
||||
)
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "unknown_field" in error_msg.lower() or "unknown" in error_msg.lower()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 1 TDD: Tests for Workflow/WorkflowStep New Params (ALL GREEN target)
|
||||
# =============================================================================
|
||||
|
||||
class TestWorkflowNewParams:
|
||||
"""Test Workflow supports all consolidated params like Agents."""
|
||||
|
||||
def test_workflow_has_autonomy_param(self):
|
||||
"""Workflow should have autonomy param."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
# Should accept autonomy param without error
|
||||
workflow = Workflow(steps=[], autonomy=True)
|
||||
assert hasattr(workflow, 'autonomy') or hasattr(workflow, '_autonomy_config')
|
||||
|
||||
def test_workflow_has_knowledge_param(self):
|
||||
"""Workflow should have knowledge param."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(steps=[], knowledge=["docs/"])
|
||||
assert hasattr(workflow, 'knowledge') or hasattr(workflow, '_knowledge_config')
|
||||
|
||||
def test_workflow_has_guardrails_param(self):
|
||||
"""Workflow should have guardrails param."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(steps=[], guardrails=True)
|
||||
assert hasattr(workflow, 'guardrails') or hasattr(workflow, '_guardrails_config')
|
||||
|
||||
def test_workflow_has_web_param(self):
|
||||
"""Workflow should have web param."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(steps=[], web=True)
|
||||
assert hasattr(workflow, 'web') or hasattr(workflow, '_web_config')
|
||||
|
||||
def test_workflow_has_reflection_param(self):
|
||||
"""Workflow should have reflection param."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(steps=[], reflection=True)
|
||||
assert hasattr(workflow, 'reflection') or hasattr(workflow, '_reflection_config')
|
||||
|
||||
|
||||
class TestWorkflowStepNewParams:
|
||||
"""Test WorkflowStep supports all consolidated params."""
|
||||
|
||||
def test_workflowstep_has_autonomy_param(self):
|
||||
"""WorkflowStep should have autonomy param."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(name="test", autonomy=True)
|
||||
assert hasattr(step, 'autonomy') or hasattr(step, '_autonomy_config')
|
||||
|
||||
def test_workflowstep_has_knowledge_param(self):
|
||||
"""WorkflowStep should have knowledge param."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(name="test", knowledge=["docs/"])
|
||||
assert hasattr(step, 'knowledge') or hasattr(step, '_knowledge_config')
|
||||
|
||||
def test_workflowstep_has_web_param(self):
|
||||
"""WorkflowStep should have web param."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(name="test", web=True)
|
||||
assert hasattr(step, 'web') or hasattr(step, '_web_config')
|
||||
|
||||
def test_workflowstep_has_reflection_param(self):
|
||||
"""WorkflowStep should have reflection param."""
|
||||
from praisonaiagents.workflows.workflows import WorkflowStep
|
||||
|
||||
step = WorkflowStep(name="test", reflection=True)
|
||||
assert hasattr(step, 'reflection') or hasattr(step, '_reflection_config')
|
||||
|
||||
|
||||
class TestWorkflowPropagation:
|
||||
"""Test workflow defaults propagate to steps/agents."""
|
||||
|
||||
def test_workflow_defaults_available_for_steps(self):
|
||||
"""Workflow-level configs should be accessible for step propagation."""
|
||||
from praisonaiagents.workflows.workflows import Workflow
|
||||
|
||||
workflow = Workflow(
|
||||
steps=[],
|
||||
knowledge=["docs/"],
|
||||
web=True,
|
||||
guardrails=True,
|
||||
)
|
||||
|
||||
# Workflow should store resolved configs for propagation
|
||||
assert workflow.knowledge is not None or hasattr(workflow, '_knowledge_config')
|
||||
assert workflow.web is not None or hasattr(workflow, '_web_config')
|
||||
assert workflow.guardrails is not None or hasattr(workflow, '_guardrails_config')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -0,0 +1,292 @@
|
||||
"""Tests for the learn module - continuous learning within memory system."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from praisonaiagents.config.feature_configs import LearnConfig, LearnScope, MemoryConfig
|
||||
from praisonaiagents.memory.learn.stores import (
|
||||
PersonaStore,
|
||||
InsightStore,
|
||||
ThreadStore,
|
||||
PatternStore,
|
||||
DecisionStore,
|
||||
FeedbackStore,
|
||||
ImprovementStore,
|
||||
LearnEntry,
|
||||
)
|
||||
from praisonaiagents.memory.learn.manager import LearnManager
|
||||
|
||||
|
||||
class TestLearnConfig:
|
||||
"""Test LearnConfig dataclass."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default LearnConfig values."""
|
||||
config = LearnConfig()
|
||||
assert config.persona is True
|
||||
assert config.insights is True
|
||||
assert config.thread is True
|
||||
assert config.patterns is False
|
||||
assert config.decisions is False
|
||||
assert config.feedback is False
|
||||
assert config.improvements is False
|
||||
assert config.scope == LearnScope.USER
|
||||
assert config.auto_consolidate is True
|
||||
assert config.retention_days is None
|
||||
|
||||
def test_custom_values(self):
|
||||
"""Test LearnConfig with custom values."""
|
||||
config = LearnConfig(
|
||||
persona=False,
|
||||
insights=True,
|
||||
patterns=True,
|
||||
scope=LearnScope.TEAM,
|
||||
retention_days=30,
|
||||
)
|
||||
assert config.persona is False
|
||||
assert config.patterns is True
|
||||
assert config.scope == LearnScope.TEAM
|
||||
assert config.retention_days == 30
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test LearnConfig to_dict method."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
d = config.to_dict()
|
||||
assert d["persona"] is True
|
||||
assert d["insights"] is True
|
||||
assert d["scope"] == "user"
|
||||
|
||||
|
||||
class TestMemoryConfigWithLearn:
|
||||
"""Test MemoryConfig with learn field."""
|
||||
|
||||
def test_learn_none_by_default(self):
|
||||
"""Test that learn is None by default."""
|
||||
config = MemoryConfig()
|
||||
assert config.learn is None
|
||||
|
||||
def test_learn_bool_true(self):
|
||||
"""Test learn=True."""
|
||||
config = MemoryConfig(learn=True)
|
||||
assert config.learn is True
|
||||
d = config.to_dict()
|
||||
assert d["learn"] is not None
|
||||
assert d["learn"]["persona"] is True
|
||||
|
||||
def test_learn_config_instance(self):
|
||||
"""Test learn=LearnConfig(...)."""
|
||||
learn_config = LearnConfig(persona=True, patterns=True)
|
||||
config = MemoryConfig(learn=learn_config)
|
||||
assert config.learn is learn_config
|
||||
d = config.to_dict()
|
||||
assert d["learn"]["patterns"] is True
|
||||
|
||||
|
||||
class TestLearnEntry:
|
||||
"""Test LearnEntry dataclass."""
|
||||
|
||||
def test_create_entry(self):
|
||||
"""Test creating a LearnEntry."""
|
||||
entry = LearnEntry(
|
||||
id="test_1",
|
||||
content="Test content",
|
||||
metadata={"key": "value"},
|
||||
)
|
||||
assert entry.id == "test_1"
|
||||
assert entry.content == "Test content"
|
||||
assert entry.metadata["key"] == "value"
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test LearnEntry to_dict."""
|
||||
entry = LearnEntry(id="test_1", content="Test")
|
||||
d = entry.to_dict()
|
||||
assert d["id"] == "test_1"
|
||||
assert d["content"] == "Test"
|
||||
assert "created_at" in d
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test LearnEntry from_dict."""
|
||||
data = {
|
||||
"id": "test_1",
|
||||
"content": "Test content",
|
||||
"metadata": {"key": "value"},
|
||||
}
|
||||
entry = LearnEntry.from_dict(data)
|
||||
assert entry.id == "test_1"
|
||||
assert entry.content == "Test content"
|
||||
|
||||
|
||||
class TestPersonaStore:
|
||||
"""Test PersonaStore."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.store_path = os.path.join(self.temp_dir, "persona.json")
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test fixtures."""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_add_preference(self):
|
||||
"""Test adding a preference."""
|
||||
store = PersonaStore(store_path=self.store_path, user_id="test_user")
|
||||
entry = store.add_preference("User prefers concise responses", "communication")
|
||||
assert entry is not None
|
||||
assert "concise" in entry.content
|
||||
assert entry.metadata["category"] == "communication"
|
||||
|
||||
def test_search(self):
|
||||
"""Test searching entries."""
|
||||
store = PersonaStore(store_path=self.store_path, user_id="test_user")
|
||||
store.add_preference("User likes Python", "programming")
|
||||
store.add_preference("User prefers dark mode", "ui")
|
||||
|
||||
results = store.search("Python")
|
||||
assert len(results) == 1
|
||||
assert "Python" in results[0].content
|
||||
|
||||
|
||||
class TestInsightStore:
|
||||
"""Test InsightStore."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.store_path = os.path.join(self.temp_dir, "insights.json")
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test fixtures."""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_add_insight(self):
|
||||
"""Test adding an insight."""
|
||||
store = InsightStore(store_path=self.store_path, user_id="test_user")
|
||||
entry = store.add_insight("User works in data science", "conversation")
|
||||
assert entry is not None
|
||||
assert "data science" in entry.content
|
||||
|
||||
|
||||
class TestLearnManager:
|
||||
"""Test LearnManager."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test fixtures."""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_create_manager_with_defaults(self):
|
||||
"""Test creating LearnManager with default config."""
|
||||
config = LearnConfig()
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
assert "persona" in manager._stores
|
||||
assert "insights" in manager._stores
|
||||
assert "threads" in manager._stores
|
||||
|
||||
def test_capture_persona(self):
|
||||
"""Test capturing persona."""
|
||||
config = LearnConfig(persona=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
entry = manager.capture_persona("User prefers detailed explanations")
|
||||
assert entry is not None
|
||||
assert "detailed" in entry.content
|
||||
|
||||
def test_capture_insight(self):
|
||||
"""Test capturing insight."""
|
||||
config = LearnConfig(insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
entry = manager.capture_insight("User is experienced with Python")
|
||||
assert entry is not None
|
||||
|
||||
def test_get_learning_context(self):
|
||||
"""Test getting learning context."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
manager.capture_persona("User likes concise code")
|
||||
manager.capture_insight("User knows Python")
|
||||
|
||||
context = manager.get_learning_context()
|
||||
assert "persona" in context
|
||||
assert "insights" in context
|
||||
assert len(context["persona"]) == 1
|
||||
assert len(context["insights"]) == 1
|
||||
|
||||
def test_search(self):
|
||||
"""Test searching across stores."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
manager.capture_persona("User prefers Python")
|
||||
manager.capture_insight("User knows Python well")
|
||||
|
||||
results = manager.search("Python")
|
||||
assert "persona" in results or "insights" in results
|
||||
|
||||
def test_clear_all(self):
|
||||
"""Test clearing all stores."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
manager.capture_persona("Test 1")
|
||||
manager.capture_insight("Test 2")
|
||||
|
||||
cleared = manager.clear_all()
|
||||
assert cleared["persona"] == 1
|
||||
assert cleared["insights"] == 1
|
||||
|
||||
def test_get_stats(self):
|
||||
"""Test getting stats."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
manager.capture_persona("Test 1")
|
||||
manager.capture_persona("Test 2")
|
||||
manager.capture_insight("Test 3")
|
||||
|
||||
stats = manager.get_stats()
|
||||
assert stats["persona"] == 2
|
||||
assert stats["insights"] == 1
|
||||
|
||||
def test_to_system_prompt_context(self):
|
||||
"""Test generating system prompt context."""
|
||||
config = LearnConfig(persona=True, insights=True)
|
||||
manager = LearnManager(config=config, user_id="test_user", store_path=self.temp_dir)
|
||||
|
||||
manager.capture_persona("User prefers concise responses")
|
||||
manager.capture_insight("User is a Python developer")
|
||||
|
||||
context = manager.to_system_prompt_context()
|
||||
assert "User Preferences" in context
|
||||
assert "concise" in context
|
||||
assert "Learned Insights" in context
|
||||
assert "Python" in context
|
||||
|
||||
|
||||
class TestNoCortexParam:
|
||||
"""Test that cortex parameter does not exist."""
|
||||
|
||||
def test_agent_has_no_cortex_param(self):
|
||||
"""Agent.__init__ should not have cortex parameter."""
|
||||
import inspect
|
||||
from praisonaiagents import Agent
|
||||
|
||||
sig = inspect.signature(Agent.__init__)
|
||||
param_names = list(sig.parameters.keys())
|
||||
|
||||
assert "cortex" not in param_names, "cortex param should be removed from Agent"
|
||||
|
||||
def test_no_cortex_module_in_agents(self):
|
||||
"""praisonaiagents should not have cortex module."""
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.find_spec("praisonaiagents.cortex")
|
||||
assert spec is None, "praisonaiagents.cortex module should not exist"
|
||||
@@ -0,0 +1 @@
|
||||
"""Unit tests for streaming module."""
|
||||
@@ -0,0 +1,387 @@
|
||||
"""
|
||||
Unit tests for streaming events module.
|
||||
|
||||
Tests cover:
|
||||
- StreamEvent creation and representation
|
||||
- StreamEventType enum values
|
||||
- StreamMetrics calculations (TTFT, stream duration, total time)
|
||||
- StreamEventEmitter callback management
|
||||
- create_text_printer_callback and create_metrics_callback helpers
|
||||
"""
|
||||
|
||||
import time
|
||||
from praisonaiagents.streaming.events import (
|
||||
StreamEvent,
|
||||
StreamEventType,
|
||||
StreamMetrics,
|
||||
StreamEventEmitter,
|
||||
create_text_printer_callback,
|
||||
create_metrics_callback,
|
||||
)
|
||||
|
||||
|
||||
class TestStreamEventType:
|
||||
"""Tests for StreamEventType enum."""
|
||||
|
||||
def test_event_types_exist(self):
|
||||
"""All required event types should exist."""
|
||||
assert StreamEventType.REQUEST_START.value == "request_start"
|
||||
assert StreamEventType.HEADERS_RECEIVED.value == "headers_received"
|
||||
assert StreamEventType.FIRST_TOKEN.value == "first_token"
|
||||
assert StreamEventType.DELTA_TEXT.value == "delta_text"
|
||||
assert StreamEventType.DELTA_TOOL_CALL.value == "delta_tool_call"
|
||||
assert StreamEventType.TOOL_CALL_END.value == "tool_call_end"
|
||||
assert StreamEventType.LAST_TOKEN.value == "last_token"
|
||||
assert StreamEventType.STREAM_END.value == "stream_end"
|
||||
assert StreamEventType.ERROR.value == "error"
|
||||
|
||||
|
||||
class TestStreamEvent:
|
||||
"""Tests for StreamEvent dataclass."""
|
||||
|
||||
def test_create_basic_event(self):
|
||||
"""Create a basic event with type only."""
|
||||
event = StreamEvent(type=StreamEventType.REQUEST_START)
|
||||
assert event.type == StreamEventType.REQUEST_START
|
||||
assert event.timestamp > 0
|
||||
assert event.content is None
|
||||
assert event.tool_call is None
|
||||
assert event.metadata is None
|
||||
assert event.error is None
|
||||
|
||||
def test_create_event_with_content(self):
|
||||
"""Create an event with content."""
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
content="Hello"
|
||||
)
|
||||
assert event.type == StreamEventType.DELTA_TEXT
|
||||
assert event.content == "Hello"
|
||||
|
||||
def test_create_event_with_tool_call(self):
|
||||
"""Create an event with tool call data."""
|
||||
tool_data = {"name": "calculator", "arguments": '{"x": 1}'}
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.DELTA_TOOL_CALL,
|
||||
tool_call=tool_data
|
||||
)
|
||||
assert event.type == StreamEventType.DELTA_TOOL_CALL
|
||||
assert event.tool_call == tool_data
|
||||
|
||||
def test_create_event_with_metadata(self):
|
||||
"""Create an event with metadata."""
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.REQUEST_START,
|
||||
metadata={"model": "gpt-4o-mini", "message_count": 2}
|
||||
)
|
||||
assert event.metadata["model"] == "gpt-4o-mini"
|
||||
assert event.metadata["message_count"] == 2
|
||||
|
||||
def test_create_error_event(self):
|
||||
"""Create an error event."""
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.ERROR,
|
||||
error="Connection timeout"
|
||||
)
|
||||
assert event.type == StreamEventType.ERROR
|
||||
assert event.error == "Connection timeout"
|
||||
|
||||
def test_event_repr(self):
|
||||
"""Event repr should be informative."""
|
||||
event = StreamEvent(
|
||||
type=StreamEventType.DELTA_TEXT,
|
||||
content="Hello world"
|
||||
)
|
||||
repr_str = repr(event)
|
||||
assert "DELTA_TEXT" in repr_str or "delta_text" in repr_str
|
||||
assert "Hello world" in repr_str
|
||||
|
||||
|
||||
class TestStreamMetrics:
|
||||
"""Tests for StreamMetrics dataclass."""
|
||||
|
||||
def test_default_metrics(self):
|
||||
"""Default metrics should be zero."""
|
||||
metrics = StreamMetrics()
|
||||
assert metrics.request_start == 0.0
|
||||
assert metrics.headers_received == 0.0
|
||||
assert metrics.first_token == 0.0
|
||||
assert metrics.last_token == 0.0
|
||||
assert metrics.stream_end == 0.0
|
||||
assert metrics.token_count == 0
|
||||
|
||||
def test_ttft_calculation(self):
|
||||
"""TTFT should be first_token - request_start."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.request_start = 100.0
|
||||
metrics.first_token = 100.5
|
||||
assert metrics.ttft == 0.5
|
||||
|
||||
def test_ttft_zero_when_no_first_token(self):
|
||||
"""TTFT should be 0 when first_token not set."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.request_start = 100.0
|
||||
assert metrics.ttft == 0.0
|
||||
|
||||
def test_stream_duration_calculation(self):
|
||||
"""Stream duration should be last_token - first_token."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.first_token = 100.0
|
||||
metrics.last_token = 102.0
|
||||
assert metrics.stream_duration == 2.0
|
||||
|
||||
def test_total_time_calculation(self):
|
||||
"""Total time should be stream_end - request_start."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.request_start = 100.0
|
||||
metrics.stream_end = 103.0
|
||||
assert metrics.total_time == 3.0
|
||||
|
||||
def test_tokens_per_second(self):
|
||||
"""Tokens per second calculation."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.first_token = 100.0
|
||||
metrics.last_token = 102.0 # 2 second duration
|
||||
metrics.token_count = 100
|
||||
assert metrics.tokens_per_second == 50.0
|
||||
|
||||
def test_tokens_per_second_zero_duration(self):
|
||||
"""Tokens per second should be 0 when duration is 0."""
|
||||
metrics = StreamMetrics()
|
||||
assert metrics.tokens_per_second == 0.0
|
||||
|
||||
def test_update_from_event(self):
|
||||
"""Metrics should update from events."""
|
||||
metrics = StreamMetrics()
|
||||
|
||||
# Request start
|
||||
event1 = StreamEvent(type=StreamEventType.REQUEST_START, timestamp=100.0)
|
||||
metrics.update_from_event(event1)
|
||||
assert metrics.request_start == 100.0
|
||||
|
||||
# Headers received
|
||||
event2 = StreamEvent(type=StreamEventType.HEADERS_RECEIVED, timestamp=100.2)
|
||||
metrics.update_from_event(event2)
|
||||
assert metrics.headers_received == 100.2
|
||||
|
||||
# First token
|
||||
event3 = StreamEvent(type=StreamEventType.FIRST_TOKEN, timestamp=100.5)
|
||||
metrics.update_from_event(event3)
|
||||
assert metrics.first_token == 100.5
|
||||
assert metrics.token_count == 1
|
||||
|
||||
# Delta text
|
||||
event4 = StreamEvent(type=StreamEventType.DELTA_TEXT, timestamp=100.6)
|
||||
metrics.update_from_event(event4)
|
||||
assert metrics.token_count == 2
|
||||
|
||||
# Last token
|
||||
event5 = StreamEvent(type=StreamEventType.LAST_TOKEN, timestamp=102.0)
|
||||
metrics.update_from_event(event5)
|
||||
assert metrics.last_token == 102.0
|
||||
|
||||
# Stream end
|
||||
event6 = StreamEvent(type=StreamEventType.STREAM_END, timestamp=102.1)
|
||||
metrics.update_from_event(event6)
|
||||
assert metrics.stream_end == 102.1
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Metrics should convert to dict."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.request_start = 100.0
|
||||
metrics.first_token = 100.5
|
||||
metrics.last_token = 102.0
|
||||
metrics.stream_end = 102.1
|
||||
metrics.token_count = 50
|
||||
|
||||
d = metrics.to_dict()
|
||||
assert "ttft_ms" in d
|
||||
assert "stream_duration_ms" in d
|
||||
assert "total_time_ms" in d
|
||||
assert "token_count" in d
|
||||
assert "tokens_per_second" in d
|
||||
|
||||
def test_format_summary(self):
|
||||
"""Metrics should format as summary string."""
|
||||
metrics = StreamMetrics()
|
||||
metrics.request_start = 100.0
|
||||
metrics.first_token = 100.5
|
||||
metrics.last_token = 102.0
|
||||
metrics.stream_end = 102.1
|
||||
metrics.token_count = 50
|
||||
|
||||
summary = metrics.format_summary()
|
||||
assert "TTFT" in summary
|
||||
assert "Stream" in summary
|
||||
assert "Total" in summary
|
||||
assert "Tokens" in summary
|
||||
|
||||
|
||||
class TestStreamEventEmitter:
|
||||
"""Tests for StreamEventEmitter."""
|
||||
|
||||
def test_create_emitter(self):
|
||||
"""Create an emitter."""
|
||||
emitter = StreamEventEmitter()
|
||||
assert not emitter.has_callbacks
|
||||
|
||||
def test_add_callback(self):
|
||||
"""Add a callback."""
|
||||
emitter = StreamEventEmitter()
|
||||
events = []
|
||||
|
||||
def callback(event):
|
||||
events.append(event)
|
||||
|
||||
emitter.add_callback(callback)
|
||||
assert emitter.has_callbacks
|
||||
|
||||
def test_remove_callback(self):
|
||||
"""Remove a callback."""
|
||||
emitter = StreamEventEmitter()
|
||||
events = []
|
||||
|
||||
def callback(event):
|
||||
events.append(event)
|
||||
|
||||
emitter.add_callback(callback)
|
||||
emitter.remove_callback(callback)
|
||||
assert not emitter.has_callbacks
|
||||
|
||||
def test_emit_calls_callbacks(self):
|
||||
"""Emit should call all callbacks."""
|
||||
emitter = StreamEventEmitter()
|
||||
events1 = []
|
||||
events2 = []
|
||||
|
||||
emitter.add_callback(lambda e: events1.append(e))
|
||||
emitter.add_callback(lambda e: events2.append(e))
|
||||
|
||||
event = StreamEvent(type=StreamEventType.DELTA_TEXT, content="Hello")
|
||||
emitter.emit(event)
|
||||
|
||||
assert len(events1) == 1
|
||||
assert len(events2) == 1
|
||||
assert events1[0].content == "Hello"
|
||||
|
||||
def test_emit_with_metrics(self):
|
||||
"""Emit should update metrics when enabled."""
|
||||
emitter = StreamEventEmitter()
|
||||
emitter.enable_metrics()
|
||||
|
||||
emitter.emit(StreamEvent(type=StreamEventType.REQUEST_START, timestamp=100.0))
|
||||
emitter.emit(StreamEvent(type=StreamEventType.FIRST_TOKEN, timestamp=100.5))
|
||||
emitter.emit(StreamEvent(type=StreamEventType.STREAM_END, timestamp=102.0))
|
||||
|
||||
metrics = emitter.get_metrics()
|
||||
assert metrics is not None
|
||||
assert metrics.request_start == 100.0
|
||||
assert metrics.first_token == 100.5
|
||||
assert metrics.stream_end == 102.0
|
||||
|
||||
def test_callback_error_does_not_break_emission(self):
|
||||
"""Callback errors should not break other callbacks."""
|
||||
emitter = StreamEventEmitter()
|
||||
events = []
|
||||
|
||||
def bad_callback(event):
|
||||
raise ValueError("Test error")
|
||||
|
||||
def good_callback(event):
|
||||
events.append(event)
|
||||
|
||||
emitter.add_callback(bad_callback)
|
||||
emitter.add_callback(good_callback)
|
||||
|
||||
event = StreamEvent(type=StreamEventType.DELTA_TEXT)
|
||||
emitter.emit(event) # Should not raise
|
||||
|
||||
assert len(events) == 1
|
||||
|
||||
def test_reset_metrics(self):
|
||||
"""Reset metrics should create new metrics."""
|
||||
emitter = StreamEventEmitter()
|
||||
emitter.enable_metrics()
|
||||
|
||||
emitter.emit(StreamEvent(type=StreamEventType.REQUEST_START, timestamp=100.0))
|
||||
emitter.reset_metrics()
|
||||
|
||||
metrics = emitter.get_metrics()
|
||||
assert metrics.request_start == 0.0
|
||||
|
||||
|
||||
class TestHelperFunctions:
|
||||
"""Tests for helper functions."""
|
||||
|
||||
def test_create_text_printer_callback(self, capsys):
|
||||
"""Text printer callback should print content."""
|
||||
callback = create_text_printer_callback(flush=True)
|
||||
|
||||
callback(StreamEvent(type=StreamEventType.DELTA_TEXT, content="Hello"))
|
||||
callback(StreamEvent(type=StreamEventType.DELTA_TEXT, content=" World"))
|
||||
callback(StreamEvent(type=StreamEventType.STREAM_END))
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Hello World" in captured.out
|
||||
|
||||
def test_create_metrics_callback(self):
|
||||
"""Metrics callback should update metrics."""
|
||||
metrics = StreamMetrics()
|
||||
callback = create_metrics_callback(metrics)
|
||||
|
||||
callback(StreamEvent(type=StreamEventType.REQUEST_START, timestamp=100.0))
|
||||
callback(StreamEvent(type=StreamEventType.FIRST_TOKEN, timestamp=100.5))
|
||||
|
||||
assert metrics.request_start == 100.0
|
||||
assert metrics.first_token == 100.5
|
||||
|
||||
|
||||
class TestStreamingEventOrdering:
|
||||
"""Tests for event ordering and pass-through behavior."""
|
||||
|
||||
def test_events_emitted_immediately(self):
|
||||
"""Events should be emitted immediately, not buffered."""
|
||||
emitter = StreamEventEmitter()
|
||||
timestamps = []
|
||||
|
||||
def callback(event):
|
||||
timestamps.append((event.type, time.perf_counter()))
|
||||
|
||||
emitter.add_callback(callback)
|
||||
|
||||
# Simulate streaming with small delays
|
||||
emitter.emit(StreamEvent(type=StreamEventType.REQUEST_START))
|
||||
time.sleep(0.01)
|
||||
emitter.emit(StreamEvent(type=StreamEventType.FIRST_TOKEN))
|
||||
time.sleep(0.01)
|
||||
emitter.emit(StreamEvent(type=StreamEventType.DELTA_TEXT))
|
||||
|
||||
# Verify events were captured in order
|
||||
assert len(timestamps) == 3
|
||||
assert timestamps[0][0] == StreamEventType.REQUEST_START
|
||||
assert timestamps[1][0] == StreamEventType.FIRST_TOKEN
|
||||
assert timestamps[2][0] == StreamEventType.DELTA_TEXT
|
||||
|
||||
# Verify timestamps are increasing (no buffering)
|
||||
assert timestamps[0][1] < timestamps[1][1] < timestamps[2][1]
|
||||
|
||||
def test_no_buffering_before_first_token(self):
|
||||
"""Events should not be buffered before first token."""
|
||||
emitter = StreamEventEmitter()
|
||||
emitter.enable_metrics()
|
||||
|
||||
# Emit request start
|
||||
t1 = time.perf_counter()
|
||||
emitter.emit(StreamEvent(type=StreamEventType.REQUEST_START, timestamp=t1))
|
||||
|
||||
# Small delay (simulating network)
|
||||
time.sleep(0.05)
|
||||
|
||||
# Emit first token
|
||||
t2 = time.perf_counter()
|
||||
emitter.emit(StreamEvent(type=StreamEventType.FIRST_TOKEN, timestamp=t2))
|
||||
|
||||
metrics = emitter.get_metrics()
|
||||
|
||||
# TTFT should reflect the actual delay
|
||||
assert metrics.ttft >= 0.04 # Allow some tolerance
|
||||
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
Tests for Gap Closure - LLM Consolidation + Parity
|
||||
|
||||
TDD: These tests define the expected behavior for:
|
||||
1. GAP A: No deprecation warnings for model/llm alias
|
||||
2. GAP B: YAML parser supports consolidated params
|
||||
3. GAP C: Wrapper agents use output= (not verbose/markdown)
|
||||
4. GAP D: AutoAgents uses consolidated llm= (not function_calling_llm)
|
||||
5. GAP E: model= and llm= are aliases with NO warnings
|
||||
"""
|
||||
import pytest
|
||||
import warnings
|
||||
|
||||
|
||||
class TestNoDeprecationWarnings:
|
||||
"""GAP A + E: No deprecation warnings for model/llm or legacy params."""
|
||||
|
||||
def test_no_warning_for_model_alias(self):
|
||||
"""model= should work without any deprecation warning."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(instructions="Test", model="gpt-4o-mini")
|
||||
|
||||
# Should NOT emit any deprecation warnings
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_no_warning_for_llm_param(self):
|
||||
"""llm= should work without any deprecation warning."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(instructions="Test", llm="gpt-4o-mini")
|
||||
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_no_warning_for_llm_config(self):
|
||||
"""llm_config= should work without deprecation warning (backward compat)."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(instructions="Test", llm_config={"temperature": 0.7})
|
||||
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_no_warning_for_function_calling_llm(self):
|
||||
"""function_calling_llm= should work without deprecation warning (backward compat)."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(instructions="Test", function_calling_llm="gpt-4o-mini")
|
||||
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_model_and_llm_are_equivalent(self):
|
||||
"""model= and llm= should produce identical behavior."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent1 = Agent(instructions="Test", llm="gpt-4o")
|
||||
agent2 = Agent(instructions="Test", model="gpt-4o")
|
||||
|
||||
assert agent1.llm == agent2.llm == "gpt-4o"
|
||||
|
||||
|
||||
class TestAutoAgentsConsolidation:
|
||||
"""GAP D: AutoAgents uses consolidated llm= (not function_calling_llm)."""
|
||||
|
||||
def test_autoagents_does_not_pass_function_calling_llm_to_agent(self):
|
||||
"""AutoAgents should not pass function_calling_llm= to Agent()."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
|
||||
# Get the source code of the _create_agents_and_tasks method
|
||||
source = inspect.getsource(AutoAgents._create_agents_and_tasks)
|
||||
|
||||
# Should NOT contain function_calling_llm= in Agent() call
|
||||
# This is a static analysis test
|
||||
assert "function_calling_llm=self.function_calling_llm" not in source, \
|
||||
"AutoAgents._create_agents_and_tasks still passes function_calling_llm= to Agent()"
|
||||
|
||||
def test_autoagents_accepts_llm_param(self):
|
||||
"""AutoAgents should accept llm= parameter."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
|
||||
# Should not raise
|
||||
auto = AutoAgents(instructions="Test task", llm="gpt-4o-mini")
|
||||
assert auto.llm == "gpt-4o-mini"
|
||||
|
||||
def test_autoagents_no_deprecation_warnings(self):
|
||||
"""AutoAgents should not emit deprecation warnings."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
auto = AutoAgents(
|
||||
instructions="Test task",
|
||||
llm="gpt-4o-mini",
|
||||
function_calling_llm="gpt-4o-mini" # Legacy param
|
||||
)
|
||||
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
|
||||
class TestYAMLParserConsolidation:
|
||||
"""GAP B: YAML parser supports consolidated params."""
|
||||
|
||||
def test_yaml_parser_maps_legacy_reflection_fields(self):
|
||||
"""YAML parser should map legacy reflect_llm/min_reflect/max_reflect to reflection config."""
|
||||
from praisonaiagents.workflows.yaml_parser import YAMLWorkflowParser
|
||||
|
||||
yaml_content = """
|
||||
roles:
|
||||
researcher:
|
||||
role: Researcher
|
||||
goal: Research topics
|
||||
backstory: Expert researcher
|
||||
reflect_llm: gpt-4o
|
||||
min_reflect: 2
|
||||
max_reflect: 5
|
||||
tasks:
|
||||
- description: Research AI
|
||||
expected_output: Report
|
||||
"""
|
||||
|
||||
import yaml
|
||||
config = yaml.safe_load(yaml_content)
|
||||
|
||||
parser = YAMLWorkflowParser()
|
||||
# The parser should handle legacy fields without error
|
||||
# and map them appropriately
|
||||
agent = parser._create_agent("researcher", config["roles"]["researcher"])
|
||||
|
||||
# Legacy fields should be stored for later use
|
||||
assert hasattr(agent, '_yaml_reflect_llm')
|
||||
assert hasattr(agent, '_yaml_min_reflect')
|
||||
assert hasattr(agent, '_yaml_max_reflect')
|
||||
|
||||
def test_yaml_parser_supports_new_reflection_structure(self):
|
||||
"""YAML parser should support new reflection: structure."""
|
||||
from praisonaiagents.workflows.yaml_parser import YAMLWorkflowParser
|
||||
|
||||
yaml_content = """
|
||||
roles:
|
||||
researcher:
|
||||
role: Researcher
|
||||
goal: Research topics
|
||||
backstory: Expert researcher
|
||||
reflection:
|
||||
enabled: true
|
||||
max_iterations: 5
|
||||
min_iterations: 2
|
||||
tasks:
|
||||
- description: Research AI
|
||||
expected_output: Report
|
||||
"""
|
||||
|
||||
import yaml
|
||||
config = yaml.safe_load(yaml_content)
|
||||
|
||||
parser = YAMLWorkflowParser()
|
||||
agent = parser._create_agent("researcher", config["roles"]["researcher"])
|
||||
|
||||
# Should handle new structure
|
||||
assert agent is not None
|
||||
|
||||
|
||||
class TestWrapperAgentsModernization:
|
||||
"""GAP C: Wrapper agents use output= (not verbose/markdown)."""
|
||||
|
||||
def test_prompt_expander_agent_uses_output_in_agent_call(self):
|
||||
"""PromptExpanderAgent should use output= when creating internal Agent."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
import inspect
|
||||
|
||||
# Get the source of the agent property
|
||||
source = inspect.getsource(PromptExpanderAgent.agent.fget)
|
||||
|
||||
# Should use output= not verbose= or markdown=
|
||||
assert "output=" in source, "PromptExpanderAgent.agent should use output="
|
||||
assert "verbose=" not in source or "output=" in source, \
|
||||
"PromptExpanderAgent.agent should not pass verbose= directly to Agent()"
|
||||
|
||||
def test_query_rewriter_agent_uses_output_in_agent_call(self):
|
||||
"""QueryRewriterAgent should use output= when creating internal Agent."""
|
||||
from praisonaiagents.agent.query_rewriter_agent import QueryRewriterAgent
|
||||
import inspect
|
||||
|
||||
# Get the source of the agent property
|
||||
source = inspect.getsource(QueryRewriterAgent.agent.fget)
|
||||
|
||||
# Should use output= not verbose= or markdown=
|
||||
assert "output=" in source, "QueryRewriterAgent.agent should use output="
|
||||
assert "verbose=" not in source or "output=" in source, \
|
||||
"QueryRewriterAgent.agent should not pass verbose= directly to Agent()"
|
||||
|
||||
def test_prompt_expander_accepts_model_param(self):
|
||||
"""PromptExpanderAgent should accept model= parameter."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
|
||||
agent = PromptExpanderAgent(model="gpt-4o-mini")
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
|
||||
def test_query_rewriter_accepts_model_param(self):
|
||||
"""QueryRewriterAgent should accept model= parameter."""
|
||||
from praisonaiagents.agent.query_rewriter_agent import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent(model="gpt-4o-mini")
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
|
||||
|
||||
class TestAutoAgentsNoLegacyVerboseMarkdown:
|
||||
"""AutoAgents should not pass verbose/markdown to Agent when output= is set."""
|
||||
|
||||
def test_autoagents_uses_output_not_verbose_markdown(self):
|
||||
"""AutoAgents should use output= and not pass verbose/markdown when output is set."""
|
||||
from praisonaiagents.agents.autoagents import AutoAgents
|
||||
import inspect
|
||||
|
||||
source = inspect.getsource(AutoAgents._create_agents_and_tasks)
|
||||
|
||||
# Should pass output= to Agent
|
||||
assert "output=self._output" in source or "output=" in source, \
|
||||
"AutoAgents should pass output= to Agent()"
|
||||
|
||||
# Should NOT pass verbose= or markdown= directly
|
||||
assert "verbose=self.verbose" not in source, \
|
||||
"AutoAgents should not pass verbose= to Agent()"
|
||||
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
Tests for LLM parameter consolidation.
|
||||
|
||||
TDD: These tests define the expected behavior for:
|
||||
1. Agent accepts `llm=` consolidated usage (new)
|
||||
2. Agent accepts `model=` alias (new)
|
||||
3. Agent still accepts `llm_config=` and `function_calling_llm=` but maps them internally (deprecated)
|
||||
4. base_url/api_key remain separate and unchanged
|
||||
5. Deprecation warnings emitted only when legacy params are used
|
||||
"""
|
||||
import pytest
|
||||
import warnings
|
||||
|
||||
|
||||
class TestLLMConsolidation:
|
||||
"""Tests for LLM parameter consolidation in Agent."""
|
||||
|
||||
def test_agent_accepts_llm_string(self):
|
||||
"""Agent accepts llm= as a string model name."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test agent",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
assert agent.llm == "gpt-4o-mini"
|
||||
|
||||
def test_agent_accepts_model_alias(self):
|
||||
"""Agent accepts model= as an alias for llm=."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test agent",
|
||||
model="gpt-4o-mini"
|
||||
)
|
||||
# model= should behave identically to llm=
|
||||
assert agent.llm == "gpt-4o-mini"
|
||||
|
||||
def test_model_and_llm_are_equivalent(self):
|
||||
"""model= and llm= produce identical agents."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent1 = Agent(instructions="Test", llm="gpt-4o")
|
||||
agent2 = Agent(instructions="Test", model="gpt-4o")
|
||||
|
||||
assert agent1.llm == agent2.llm
|
||||
|
||||
def test_llm_takes_precedence_over_model(self):
|
||||
"""If both llm= and model= are provided, llm= takes precedence."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o",
|
||||
model="gpt-3.5-turbo" # Should be ignored
|
||||
)
|
||||
assert agent.llm == "gpt-4o"
|
||||
|
||||
def test_llm_config_still_works_without_warning(self):
|
||||
"""llm_config= still works without deprecation warning (backward compat)."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm_config={"temperature": 0.7}
|
||||
)
|
||||
# Should NOT emit deprecation warning (per new policy)
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_function_calling_llm_still_works_without_warning(self):
|
||||
"""function_calling_llm= still works without deprecation warning (backward compat)."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
function_calling_llm="gpt-4o-mini"
|
||||
)
|
||||
# Should NOT emit deprecation warning (per new policy)
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
def test_base_url_remains_separate(self):
|
||||
"""base_url= remains a separate parameter, not consolidated."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o",
|
||||
base_url="https://custom.api.com/v1"
|
||||
)
|
||||
assert agent.base_url == "https://custom.api.com/v1"
|
||||
|
||||
def test_api_key_remains_separate(self):
|
||||
"""api_key= remains a separate parameter, not consolidated."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o",
|
||||
api_key="sk-test-key"
|
||||
)
|
||||
assert agent.api_key == "sk-test-key"
|
||||
|
||||
def test_no_deprecation_warning_on_normal_path(self):
|
||||
"""No deprecation warnings when using only new consolidated params."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o",
|
||||
base_url="https://api.openai.com/v1",
|
||||
api_key="sk-test"
|
||||
)
|
||||
# Should NOT emit deprecation warnings for llm/base_url/api_key
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
llm_related = [x for x in deprecation_warnings
|
||||
if "llm_config" in str(x.message).lower()
|
||||
or "function_calling_llm" in str(x.message).lower()]
|
||||
assert len(llm_related) == 0
|
||||
|
||||
|
||||
class TestWebFetchGating:
|
||||
"""Tests for web_fetch gating (Claude-only)."""
|
||||
|
||||
def test_non_claude_model_with_web_fetch_raises_error(self):
|
||||
"""Non-Claude model + web fetch requested → error."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
|
||||
# This should raise an error or warning when fetch is explicitly requested
|
||||
with pytest.raises((ValueError, RuntimeError)) as exc_info:
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o", # OpenAI model - does NOT support fetch
|
||||
web=WebConfig(fetch=True) # Explicitly request fetch
|
||||
)
|
||||
# Force initialization to trigger the check
|
||||
_ = agent.web
|
||||
|
||||
assert "fetch" in str(exc_info.value).lower() or "claude" in str(exc_info.value).lower()
|
||||
|
||||
def test_claude_model_with_web_fetch_allowed(self):
|
||||
"""Claude model + web fetch requested → allowed."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
|
||||
# This should work without error
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="anthropic/claude-sonnet-4", # Claude model - supports fetch
|
||||
web=WebConfig(fetch=True)
|
||||
)
|
||||
# Should not raise
|
||||
assert agent is not None
|
||||
|
||||
def test_web_enabled_without_fetch_works_on_any_model(self):
|
||||
"""web enabled with search only (no fetch) works on any model."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
|
||||
# Web search (not fetch) should work on any model that supports it
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o-search-preview", # OpenAI search model
|
||||
web=WebConfig(search=True, fetch=False) # Explicitly disable fetch
|
||||
)
|
||||
# Should not raise - web search is allowed
|
||||
assert agent is not None
|
||||
|
||||
def test_web_fetch_false_on_non_claude_no_error(self):
|
||||
"""web=True with fetch=False on non-Claude should not error."""
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.config.feature_configs import WebConfig
|
||||
|
||||
agent = Agent(
|
||||
instructions="Test",
|
||||
llm="gpt-4o",
|
||||
web=WebConfig(search=True, fetch=False) # Explicitly disable fetch
|
||||
)
|
||||
assert agent is not None
|
||||
|
||||
|
||||
class TestWrapperAgentConsolidation:
|
||||
"""Tests for wrapper agents using consolidated params."""
|
||||
|
||||
def test_prompt_expander_accepts_model_param(self):
|
||||
"""PromptExpanderAgent accepts model= parameter."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
|
||||
agent = PromptExpanderAgent(model="gpt-4o-mini")
|
||||
# PromptExpanderAgent stores model in self.model attribute
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
|
||||
def test_prompt_expander_model_is_used(self):
|
||||
"""PromptExpanderAgent uses the model parameter correctly."""
|
||||
from praisonaiagents.agent.prompt_expander_agent import PromptExpanderAgent
|
||||
|
||||
# Should accept model= parameter without error
|
||||
agent = PromptExpanderAgent(model="gpt-4o-mini")
|
||||
assert agent is not None
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
|
||||
def test_query_rewriter_accepts_model_param(self):
|
||||
"""QueryRewriterAgent accepts model= parameter."""
|
||||
from praisonaiagents.agent.query_rewriter_agent import QueryRewriterAgent
|
||||
|
||||
agent = QueryRewriterAgent(model="gpt-4o-mini")
|
||||
assert agent.model == "gpt-4o-mini"
|
||||
@@ -0,0 +1,423 @@
|
||||
"""
|
||||
Unit tests for Dict support in the canonical param resolver.
|
||||
|
||||
Tests the NEW_FEATURE_SPEC:
|
||||
1. dict → config success
|
||||
2. dict with unknown key → error
|
||||
3. dict where no config class → error
|
||||
|
||||
Also tests:
|
||||
- Precedence ordering (Instance > Config > Dict > Array > String > Bool > Default)
|
||||
- Array semantics unchanged
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test Config Classes (mimicking real config classes)
|
||||
# =============================================================================
|
||||
|
||||
@dataclass
|
||||
class TestOutputConfig:
|
||||
"""Test config for output parameter."""
|
||||
verbose: bool = False
|
||||
stream: bool = True
|
||||
markdown: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestExecutionConfig:
|
||||
"""Test config for execution parameter."""
|
||||
max_iter: int = 10
|
||||
max_retries: int = 3
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestMemoryConfig:
|
||||
"""Test config for memory parameter."""
|
||||
backend: str = "file"
|
||||
user_id: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestContextConfig:
|
||||
"""Test config for context parameter with list field."""
|
||||
from_steps: Optional[List[str]] = None
|
||||
retain_full: bool = True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test Fixtures
|
||||
# =============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def resolver():
|
||||
"""Import the canonical resolver."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
return resolve, ArrayMode
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def output_presets():
|
||||
"""Sample presets for output parameter."""
|
||||
return {
|
||||
"verbose": {"verbose": True, "stream": True, "markdown": True},
|
||||
"silent": {"verbose": False, "stream": False, "markdown": False},
|
||||
"minimal": {"verbose": False, "stream": True, "markdown": False},
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DICT SUCCESS TESTS
|
||||
# =============================================================================
|
||||
|
||||
class TestDictSuccess:
|
||||
"""Test dict → config conversion success cases."""
|
||||
|
||||
def test_dict_to_config_basic(self, resolver):
|
||||
"""Dict with valid keys converts to config."""
|
||||
resolve, _ = resolver
|
||||
result = resolve(
|
||||
value={"verbose": True, "stream": False},
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
)
|
||||
assert isinstance(result, TestOutputConfig)
|
||||
assert result.verbose is True
|
||||
assert result.stream is False
|
||||
assert result.markdown is False # default
|
||||
|
||||
def test_dict_to_config_all_fields(self, resolver):
|
||||
"""Dict with all fields converts correctly."""
|
||||
resolve, _ = resolver
|
||||
result = resolve(
|
||||
value={"verbose": True, "stream": True, "markdown": True},
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
)
|
||||
assert isinstance(result, TestOutputConfig)
|
||||
assert result.verbose is True
|
||||
assert result.stream is True
|
||||
assert result.markdown is True
|
||||
|
||||
def test_dict_to_config_partial_fields(self, resolver):
|
||||
"""Dict with partial fields uses defaults for missing."""
|
||||
resolve, _ = resolver
|
||||
result = resolve(
|
||||
value={"max_iter": 50},
|
||||
param_name="execution",
|
||||
config_class=TestExecutionConfig,
|
||||
)
|
||||
assert isinstance(result, TestExecutionConfig)
|
||||
assert result.max_iter == 50
|
||||
assert result.max_retries == 3 # default
|
||||
|
||||
def test_dict_to_config_empty_dict(self, resolver):
|
||||
"""Empty dict creates config with all defaults."""
|
||||
resolve, _ = resolver
|
||||
result = resolve(
|
||||
value={},
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
)
|
||||
assert isinstance(result, TestOutputConfig)
|
||||
assert result.verbose is False
|
||||
assert result.stream is True
|
||||
assert result.markdown is False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DICT ERROR TESTS - Unknown Keys
|
||||
# =============================================================================
|
||||
|
||||
class TestDictUnknownKeyError:
|
||||
"""Test dict with unknown key raises helpful error."""
|
||||
|
||||
def test_unknown_key_raises_typeerror(self, resolver):
|
||||
"""Unknown key in dict raises TypeError with helpful message."""
|
||||
resolve, _ = resolver
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
resolve(
|
||||
value={"verbose": True, "unknown_key": "value"},
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
)
|
||||
error_msg = str(exc_info.value)
|
||||
assert "Unknown keys for output" in error_msg
|
||||
assert "unknown_key" in error_msg
|
||||
assert "Valid keys" in error_msg
|
||||
|
||||
def test_multiple_unknown_keys(self, resolver):
|
||||
"""Multiple unknown keys are all reported."""
|
||||
resolve, _ = resolver
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
resolve(
|
||||
value={"foo": 1, "bar": 2, "verbose": True},
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
)
|
||||
error_msg = str(exc_info.value)
|
||||
assert "foo" in error_msg or "bar" in error_msg
|
||||
|
||||
def test_typo_in_key_raises_error(self, resolver):
|
||||
"""Typo in key name raises error with valid keys listed."""
|
||||
resolve, _ = resolver
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
resolve(
|
||||
value={"verbos": True}, # typo: verbos instead of verbose
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
)
|
||||
error_msg = str(exc_info.value)
|
||||
assert "verbos" in error_msg
|
||||
assert "verbose" in error_msg # valid key should be shown
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DICT ERROR TESTS - No Config Class
|
||||
# =============================================================================
|
||||
|
||||
class TestDictNoConfigClassError:
|
||||
"""Test dict where no config class is defined raises error."""
|
||||
|
||||
def test_dict_without_config_class_raises_typeerror(self, resolver):
|
||||
"""Dict input without config_class raises TypeError."""
|
||||
resolve, _ = resolver
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
resolve(
|
||||
value={"some": "dict"},
|
||||
param_name="custom_param",
|
||||
config_class=None,
|
||||
)
|
||||
error_msg = str(exc_info.value)
|
||||
assert "Dict input not supported" in error_msg
|
||||
assert "custom_param" in error_msg
|
||||
assert "no config class" in error_msg
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRECEDENCE TESTS
|
||||
# =============================================================================
|
||||
|
||||
class TestPrecedence:
|
||||
"""Test precedence ordering: Instance > Config > Dict > Array > String > Bool > Default."""
|
||||
|
||||
def test_instance_beats_dict(self, resolver):
|
||||
"""Instance check takes precedence over dict."""
|
||||
resolve, _ = resolver
|
||||
|
||||
class CustomInstance:
|
||||
def __init__(self):
|
||||
self.custom = True
|
||||
|
||||
instance = CustomInstance()
|
||||
result = resolve(
|
||||
value=instance,
|
||||
param_name="test",
|
||||
config_class=TestOutputConfig,
|
||||
instance_check=lambda v: isinstance(v, CustomInstance),
|
||||
)
|
||||
assert result is instance
|
||||
|
||||
def test_config_instance_beats_dict(self, resolver):
|
||||
"""Config instance takes precedence over dict conversion."""
|
||||
resolve, _ = resolver
|
||||
config = TestOutputConfig(verbose=True, stream=False)
|
||||
result = resolve(
|
||||
value=config,
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
)
|
||||
assert result is config
|
||||
|
||||
def test_dict_beats_string(self, resolver, output_presets):
|
||||
"""Dict takes precedence over string preset lookup."""
|
||||
resolve, _ = resolver
|
||||
# Dict should be processed, not treated as string
|
||||
result = resolve(
|
||||
value={"verbose": False, "stream": False},
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
presets=output_presets,
|
||||
)
|
||||
assert isinstance(result, TestOutputConfig)
|
||||
assert result.verbose is False
|
||||
assert result.stream is False
|
||||
|
||||
def test_dict_beats_bool(self, resolver):
|
||||
"""Dict takes precedence over bool handling."""
|
||||
resolve, _ = resolver
|
||||
# Dict should be processed, not treated as bool
|
||||
result = resolve(
|
||||
value={"verbose": True},
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
)
|
||||
assert isinstance(result, TestOutputConfig)
|
||||
assert result.verbose is True
|
||||
|
||||
def test_bool_true_creates_default_config(self, resolver):
|
||||
"""Bool True creates default config instance."""
|
||||
resolve, _ = resolver
|
||||
result = resolve(
|
||||
value=True,
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
)
|
||||
assert isinstance(result, TestOutputConfig)
|
||||
assert result.verbose is False # default
|
||||
|
||||
def test_bool_false_returns_none(self, resolver):
|
||||
"""Bool False returns None (disabled)."""
|
||||
resolve, _ = resolver
|
||||
result = resolve(
|
||||
value=False,
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
)
|
||||
assert result is None
|
||||
|
||||
def test_none_returns_default(self, resolver):
|
||||
"""None returns the default value."""
|
||||
resolve, _ = resolver
|
||||
default = TestOutputConfig(verbose=True)
|
||||
result = resolve(
|
||||
value=None,
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
default=default,
|
||||
)
|
||||
assert result is default
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ARRAY SEMANTICS UNCHANGED TESTS
|
||||
# =============================================================================
|
||||
|
||||
class TestArraySemanticsUnchanged:
|
||||
"""Test that array semantics remain unchanged."""
|
||||
|
||||
def test_preset_override_array(self, resolver, output_presets):
|
||||
"""[preset, {overrides}] pattern still works."""
|
||||
resolve, ArrayMode = resolver
|
||||
result = resolve(
|
||||
value=["verbose", {"stream": False}],
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
presets=output_presets,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
assert isinstance(result, TestOutputConfig)
|
||||
assert result.verbose is True # from preset
|
||||
assert result.stream is False # overridden
|
||||
|
||||
def test_step_names_array(self, resolver):
|
||||
"""List of step names creates config with from_steps."""
|
||||
resolve, ArrayMode = resolver
|
||||
result = resolve(
|
||||
value=["step1", "step2"],
|
||||
param_name="context",
|
||||
config_class=TestContextConfig,
|
||||
array_mode=ArrayMode.STEP_NAMES,
|
||||
)
|
||||
assert isinstance(result, TestContextConfig)
|
||||
assert result.from_steps == ["step1", "step2"]
|
||||
|
||||
def test_passthrough_array(self, resolver):
|
||||
"""Passthrough mode returns list as-is."""
|
||||
resolve, ArrayMode = resolver
|
||||
input_list = ["a", "b", "c"]
|
||||
result = resolve(
|
||||
value=input_list,
|
||||
param_name="hooks",
|
||||
config_class=None,
|
||||
array_mode=ArrayMode.PASSTHROUGH,
|
||||
)
|
||||
assert result == input_list
|
||||
|
||||
def test_empty_array_returns_none(self, resolver):
|
||||
"""Empty array returns None (disabled)."""
|
||||
resolve, ArrayMode = resolver
|
||||
result = resolve(
|
||||
value=[],
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
assert result is None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTION TESTS
|
||||
# =============================================================================
|
||||
|
||||
class TestHelperFunctions:
|
||||
"""Test helper functions for dict validation."""
|
||||
|
||||
def test_get_config_fields(self):
|
||||
"""_get_config_fields returns field names."""
|
||||
from praisonaiagents.config.param_resolver import _get_config_fields
|
||||
fields = _get_config_fields(TestOutputConfig)
|
||||
assert "verbose" in fields
|
||||
assert "stream" in fields
|
||||
assert "markdown" in fields
|
||||
|
||||
def test_validate_dict_keys_valid(self):
|
||||
"""_validate_dict_keys returns empty list for valid keys."""
|
||||
from praisonaiagents.config.param_resolver import _validate_dict_keys
|
||||
unknown = _validate_dict_keys(
|
||||
{"verbose": True, "stream": False},
|
||||
TestOutputConfig
|
||||
)
|
||||
assert unknown == []
|
||||
|
||||
def test_validate_dict_keys_invalid(self):
|
||||
"""_validate_dict_keys returns unknown keys."""
|
||||
from praisonaiagents.config.param_resolver import _validate_dict_keys
|
||||
unknown = _validate_dict_keys(
|
||||
{"verbose": True, "bad_key": "value"},
|
||||
TestOutputConfig
|
||||
)
|
||||
assert "bad_key" in unknown
|
||||
|
||||
def test_get_example_dict(self):
|
||||
"""_get_example_dict returns example snippet."""
|
||||
from praisonaiagents.config.param_resolver import _get_example_dict
|
||||
example = _get_example_dict(TestOutputConfig)
|
||||
assert "verbose" in example or "stream" in example
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# STRING PRESET TESTS (regression)
|
||||
# =============================================================================
|
||||
|
||||
class TestStringPresets:
|
||||
"""Test string preset resolution still works."""
|
||||
|
||||
def test_string_preset_resolves(self, resolver, output_presets):
|
||||
"""String preset name resolves to config."""
|
||||
resolve, _ = resolver
|
||||
result = resolve(
|
||||
value="verbose",
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
presets=output_presets,
|
||||
)
|
||||
assert isinstance(result, TestOutputConfig)
|
||||
assert result.verbose is True
|
||||
|
||||
def test_invalid_preset_raises_error(self, resolver, output_presets):
|
||||
"""Invalid preset name raises helpful error."""
|
||||
resolve, _ = resolver
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
resolve(
|
||||
value="invalid_preset",
|
||||
param_name="output",
|
||||
config_class=TestOutputConfig,
|
||||
presets=output_presets,
|
||||
)
|
||||
error_msg = str(exc_info.value)
|
||||
assert "invalid_preset" in error_msg
|
||||
@@ -0,0 +1,371 @@
|
||||
"""
|
||||
Unit tests for the core SDK profiling module.
|
||||
|
||||
Tests cover:
|
||||
- Profiler enable/disable
|
||||
- Timing recording
|
||||
- Streaming tracking
|
||||
- Phase tracking
|
||||
- Report generation
|
||||
- Zero overhead when disabled
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestProfilerBasics:
|
||||
"""Test basic Profiler functionality."""
|
||||
|
||||
def test_profiler_disabled_by_default(self):
|
||||
"""Profiler should be disabled by default."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
Profiler.disable() # Ensure clean state
|
||||
assert not Profiler.is_enabled()
|
||||
|
||||
def test_profiler_enable_disable(self):
|
||||
"""Test enable/disable functionality."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.disable()
|
||||
assert not Profiler.is_enabled()
|
||||
|
||||
Profiler.enable()
|
||||
assert Profiler.is_enabled()
|
||||
|
||||
Profiler.disable()
|
||||
assert not Profiler.is_enabled()
|
||||
|
||||
def test_profiler_env_var_enable(self):
|
||||
"""Test enabling via environment variable."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
with patch.dict('os.environ', {'PRAISONAI_PROFILE': '1'}):
|
||||
assert Profiler.is_enabled()
|
||||
|
||||
with patch.dict('os.environ', {'PRAISONAI_PROFILE': 'true'}):
|
||||
assert Profiler.is_enabled()
|
||||
|
||||
with patch.dict('os.environ', {'PRAISONAI_PROFILE': 'yes'}):
|
||||
assert Profiler.is_enabled()
|
||||
|
||||
def test_profiler_clear(self):
|
||||
"""Test clearing profiling data."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.record_timing("test", 100.0)
|
||||
assert len(Profiler.get_timings()) > 0
|
||||
|
||||
Profiler.clear()
|
||||
assert len(Profiler.get_timings()) == 0
|
||||
Profiler.disable()
|
||||
|
||||
|
||||
class TestTimingRecording:
|
||||
"""Test timing recording functionality."""
|
||||
|
||||
def test_record_timing_when_enabled(self):
|
||||
"""Timing should be recorded when profiler is enabled."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("test_op", 123.45, category="test")
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert len(timings) == 1
|
||||
assert timings[0].name == "test_op"
|
||||
assert timings[0].duration_ms == 123.45
|
||||
assert timings[0].category == "test"
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_record_timing_when_disabled(self):
|
||||
"""Timing should NOT be recorded when profiler is disabled."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("test_op", 123.45)
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert len(timings) == 0
|
||||
|
||||
def test_record_timing_with_metadata(self):
|
||||
"""Timing should include metadata."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("test_op", 100.0, metadata={"key": "value"})
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert timings[0].metadata == {"key": "value"}
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
|
||||
class TestPhaseTracking:
|
||||
"""Test phase tracking functionality."""
|
||||
|
||||
def test_phase_context_manager(self):
|
||||
"""Phase context manager should record timing."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
with Profiler.phase("test_phase"):
|
||||
time.sleep(0.01) # 10ms
|
||||
|
||||
timings = Profiler.get_timings(category="phase")
|
||||
assert len(timings) == 1
|
||||
assert timings[0].name == "test_phase"
|
||||
assert timings[0].duration_ms >= 10 # At least 10ms
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_nested_phases(self):
|
||||
"""Nested phases should track parent."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
with Profiler.phase("outer"):
|
||||
with Profiler.phase("inner"):
|
||||
time.sleep(0.005)
|
||||
|
||||
timings = Profiler.get_timings(category="phase")
|
||||
assert len(timings) == 2
|
||||
|
||||
inner = next(t for t in timings if t.name == "inner")
|
||||
assert inner.parent == "outer"
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
|
||||
class TestStreamingTracker:
|
||||
"""Test streaming tracker functionality."""
|
||||
|
||||
def test_streaming_ttft(self):
|
||||
"""Streaming tracker should measure TTFT."""
|
||||
from praisonaiagents.profiling import Profiler, StreamingTracker
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
tracker = StreamingTracker("test_stream")
|
||||
tracker.start()
|
||||
time.sleep(0.01) # 10ms to first token
|
||||
tracker.first_token()
|
||||
time.sleep(0.02) # 20ms more
|
||||
tracker.end()
|
||||
|
||||
assert tracker.ttft_ms >= 10
|
||||
assert tracker.elapsed_ms >= 30
|
||||
|
||||
records = Profiler.get_streaming_records()
|
||||
assert len(records) == 1
|
||||
assert records[0].name == "test_stream"
|
||||
assert records[0].ttft_ms >= 10
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_streaming_context_manager(self):
|
||||
"""Streaming context manager should work."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
with Profiler.streaming("test_stream") as tracker:
|
||||
time.sleep(0.01)
|
||||
tracker.first_token()
|
||||
tracker.chunk()
|
||||
tracker.chunk()
|
||||
|
||||
records = Profiler.get_streaming_records()
|
||||
assert len(records) == 1
|
||||
assert records[0].chunk_count == 2
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
|
||||
class TestProfileDecorators:
|
||||
"""Test profiling decorators."""
|
||||
|
||||
def test_profile_func_decorator(self):
|
||||
"""profile_func decorator should record function timing."""
|
||||
from praisonaiagents.profiling import Profiler, profile_func
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
@profile_func
|
||||
def slow_function():
|
||||
time.sleep(0.01)
|
||||
return "done"
|
||||
|
||||
result = slow_function()
|
||||
assert result == "done"
|
||||
|
||||
timings = Profiler.get_timings(category="function")
|
||||
assert len(timings) == 1
|
||||
assert timings[0].name == "slow_function"
|
||||
assert timings[0].duration_ms >= 10
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_profile_func_disabled(self):
|
||||
"""profile_func should have zero overhead when disabled."""
|
||||
from praisonaiagents.profiling import Profiler, profile_func
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
@profile_func
|
||||
def fast_function():
|
||||
return "done"
|
||||
|
||||
result = fast_function()
|
||||
assert result == "done"
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert len(timings) == 0
|
||||
|
||||
|
||||
class TestProfileBlock:
|
||||
"""Test profile_block context manager."""
|
||||
|
||||
def test_profile_block(self):
|
||||
"""profile_block should record block timing."""
|
||||
from praisonaiagents.profiling import Profiler, profile_block
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
with profile_block("my_block", category="custom"):
|
||||
time.sleep(0.01)
|
||||
|
||||
timings = Profiler.get_timings(category="custom")
|
||||
assert len(timings) == 1
|
||||
assert timings[0].name == "my_block"
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_profile_block_disabled(self):
|
||||
"""profile_block should have zero overhead when disabled."""
|
||||
from praisonaiagents.profiling import Profiler, profile_block
|
||||
|
||||
Profiler.disable()
|
||||
Profiler.clear()
|
||||
|
||||
with profile_block("my_block"):
|
||||
pass
|
||||
|
||||
timings = Profiler.get_timings()
|
||||
assert len(timings) == 0
|
||||
|
||||
|
||||
class TestReporting:
|
||||
"""Test report generation."""
|
||||
|
||||
def test_get_summary(self):
|
||||
"""get_summary should return structured data."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("op1", 100.0, category="cat1")
|
||||
Profiler.record_timing("op2", 200.0, category="cat2")
|
||||
|
||||
summary = Profiler.get_summary()
|
||||
|
||||
assert summary['total_time_ms'] == 300.0
|
||||
assert summary['timing_count'] == 2
|
||||
assert 'cat1' in summary['by_category']
|
||||
assert 'cat2' in summary['by_category']
|
||||
assert len(summary['slowest_operations']) == 2
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
def test_export_json(self):
|
||||
"""export_json should return valid JSON."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
import json
|
||||
|
||||
Profiler.enable()
|
||||
Profiler.clear()
|
||||
|
||||
Profiler.record_timing("test", 100.0)
|
||||
|
||||
json_str = Profiler.export_json()
|
||||
data = json.loads(json_str)
|
||||
|
||||
assert 'summary' in data
|
||||
assert 'timings' in data
|
||||
assert len(data['timings']) == 1
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
|
||||
class TestWarmup:
|
||||
"""Test warmup function."""
|
||||
|
||||
def test_warmup_returns_timings(self):
|
||||
"""warmup should return timing information."""
|
||||
from praisonaiagents import warmup
|
||||
|
||||
result = warmup(include_litellm=False, include_openai=False)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
def test_warmup_with_litellm(self):
|
||||
"""warmup with litellm should return litellm timing."""
|
||||
from praisonaiagents import warmup
|
||||
|
||||
result = warmup(include_litellm=True, include_openai=False)
|
||||
assert 'litellm' in result
|
||||
# Should be positive (imported) or -1 (not available)
|
||||
assert result['litellm'] > 0 or result['litellm'] == -1
|
||||
|
||||
|
||||
class TestZeroOverhead:
|
||||
"""Test that profiling has zero overhead when disabled."""
|
||||
|
||||
def test_record_timing_overhead(self):
|
||||
"""record_timing should be fast when disabled."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
start = time.perf_counter()
|
||||
for _ in range(10000):
|
||||
Profiler.record_timing("test", 100.0)
|
||||
elapsed = (time.perf_counter() - start) * 1000
|
||||
|
||||
# Should complete 10k calls in under 10ms when disabled
|
||||
assert elapsed < 10, f"record_timing took {elapsed}ms for 10k calls"
|
||||
|
||||
def test_phase_context_overhead(self):
|
||||
"""phase context manager should be fast when disabled."""
|
||||
from praisonaiagents.profiling import Profiler
|
||||
|
||||
Profiler.disable()
|
||||
|
||||
start = time.perf_counter()
|
||||
for _ in range(1000):
|
||||
with Profiler.phase("test"):
|
||||
pass
|
||||
elapsed = (time.perf_counter() - start) * 1000
|
||||
|
||||
# Should complete 1k calls in under 10ms when disabled
|
||||
assert elapsed < 10, f"phase took {elapsed}ms for 1k calls"
|
||||
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
Tests for the 'stream' output preset and streaming behavior.
|
||||
|
||||
Verifies:
|
||||
1. output="stream" is a valid preset string
|
||||
2. Streaming path yields chunks without buffering
|
||||
3. LLM call count == 1 for simple streaming case
|
||||
4. Precedence: start(stream=...) overrides OutputConfig.stream
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import warnings
|
||||
|
||||
|
||||
class TestStreamPresetValidation:
|
||||
"""Test that 'stream' is a valid output preset."""
|
||||
|
||||
def test_stream_preset_exists_in_output_presets(self):
|
||||
"""'stream' should be a valid key in OUTPUT_PRESETS."""
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
assert "stream" in OUTPUT_PRESETS, "Missing 'stream' preset in OUTPUT_PRESETS"
|
||||
assert OUTPUT_PRESETS["stream"]["stream"] is True, "'stream' preset should have stream=True"
|
||||
|
||||
def test_stream_preset_resolves_correctly(self):
|
||||
"""output='stream' should resolve to OutputConfig with stream=True."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
result = resolve(
|
||||
value="stream",
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
)
|
||||
|
||||
assert isinstance(result, OutputConfig)
|
||||
assert result.stream is True
|
||||
assert result.verbose is True # stream preset enables verbose
|
||||
|
||||
def test_all_output_presets_are_valid(self):
|
||||
"""All documented presets should be valid."""
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
|
||||
expected_presets = {"minimal", "normal", "verbose", "debug", "silent", "stream"}
|
||||
actual_presets = set(OUTPUT_PRESETS.keys())
|
||||
|
||||
assert expected_presets.issubset(actual_presets), \
|
||||
f"Missing presets: {expected_presets - actual_presets}"
|
||||
|
||||
def test_invalid_preset_raises_helpful_error(self):
|
||||
"""Invalid preset should raise error listing valid options including 'stream'."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
resolve(
|
||||
value="invalid_preset",
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
)
|
||||
|
||||
error_msg = str(exc_info.value).lower()
|
||||
assert "invalid_preset" in error_msg
|
||||
assert "stream" in error_msg, "Error should list 'stream' as valid preset"
|
||||
|
||||
|
||||
class TestStreamPresetWithOverrides:
|
||||
"""Test array form with stream preset and overrides."""
|
||||
|
||||
def test_stream_preset_with_override_to_disable(self):
|
||||
"""['stream', {'stream': False}] should override stream to False."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
result = resolve(
|
||||
value=["stream", {"stream": False}],
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
assert isinstance(result, OutputConfig)
|
||||
assert result.stream is False # Override takes effect
|
||||
assert result.verbose is True # Base preset value preserved
|
||||
|
||||
def test_verbose_preset_with_stream_override(self):
|
||||
"""['verbose', {'stream': True}] should enable streaming."""
|
||||
from praisonaiagents.config.param_resolver import resolve, ArrayMode
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
result = resolve(
|
||||
value=["verbose", {"stream": True}],
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
array_mode=ArrayMode.PRESET_OVERRIDE,
|
||||
)
|
||||
|
||||
assert isinstance(result, OutputConfig)
|
||||
assert result.stream is True
|
||||
assert result.metrics is True # verbose preset has metrics=True
|
||||
|
||||
|
||||
class TestStreamPrecedence:
|
||||
"""Test precedence: start(stream=...) should override OutputConfig.stream."""
|
||||
|
||||
def test_start_stream_kwarg_overrides_output_config(self):
|
||||
"""start(stream=True) should enable streaming even if output='silent'."""
|
||||
# This is a behavioral test - we verify the logic in start() method
|
||||
# The actual streaming behavior is tested in integration tests
|
||||
|
||||
# Verify the start method checks kwargs['stream'] first
|
||||
import inspect
|
||||
from praisonaiagents.agent.agent import Agent
|
||||
|
||||
source = inspect.getsource(Agent.start)
|
||||
|
||||
# start() should check kwargs.get('stream', ...) OR getattr(self, 'stream', ...)
|
||||
assert "kwargs.get('stream'" in source or "stream" in source, \
|
||||
"start() should check stream kwarg"
|
||||
|
||||
|
||||
class TestNoDeprecationWarnings:
|
||||
"""Ensure no deprecation warnings for stream preset."""
|
||||
|
||||
def test_stream_preset_no_warnings(self):
|
||||
"""Using output='stream' should not emit any warnings."""
|
||||
from praisonaiagents.config.param_resolver import resolve
|
||||
from praisonaiagents.config.presets import OUTPUT_PRESETS
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
resolve(
|
||||
value="stream",
|
||||
param_name="output",
|
||||
config_class=OutputConfig,
|
||||
presets=OUTPUT_PRESETS,
|
||||
)
|
||||
|
||||
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
|
||||
assert len(deprecation_warnings) == 0, \
|
||||
f"Unexpected warnings: {[str(x.message) for x in deprecation_warnings]}"
|
||||
|
||||
|
||||
class TestOutputConfigStreamField:
|
||||
"""Test OutputConfig has stream field."""
|
||||
|
||||
def test_output_config_has_stream_field(self):
|
||||
"""OutputConfig should have a stream field."""
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
config = OutputConfig()
|
||||
assert hasattr(config, 'stream'), "OutputConfig missing 'stream' field"
|
||||
|
||||
def test_output_config_stream_default_is_false(self):
|
||||
"""OutputConfig.stream should default to False."""
|
||||
from praisonaiagents.config.feature_configs import OutputConfig
|
||||
|
||||
config = OutputConfig()
|
||||
assert config.stream is False, "OutputConfig.stream should default to False"
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for the trace module."""
|
||||
Loaded 100 of 208 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user