praisonaiagents 0.0.127__py3-none-any.whl → 0.0.129__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- praisonaiagents/agent/__init__.py +2 -1
- praisonaiagents/agent/router_agent.py +334 -0
- praisonaiagents/agents/agents.py +15 -17
- praisonaiagents/agents/autoagents.py +1 -1
- praisonaiagents/llm/__init__.py +11 -1
- praisonaiagents/llm/llm.py +240 -274
- praisonaiagents/llm/model_capabilities.py +20 -3
- praisonaiagents/llm/model_router.py +348 -0
- praisonaiagents/process/process.py +71 -61
- praisonaiagents/task/task.py +17 -4
- {praisonaiagents-0.0.127.dist-info → praisonaiagents-0.0.129.dist-info}/METADATA +1 -1
- {praisonaiagents-0.0.127.dist-info → praisonaiagents-0.0.129.dist-info}/RECORD +14 -12
- {praisonaiagents-0.0.127.dist-info → praisonaiagents-0.0.129.dist-info}/WHEEL +0 -0
- {praisonaiagents-0.0.127.dist-info → praisonaiagents-0.0.129.dist-info}/top_level.txt +0 -0
@@ -30,6 +30,16 @@ MODELS_SUPPORTING_STRUCTURED_OUTPUTS = {
|
|
30
30
|
"gpt-4.1-mini",
|
31
31
|
"o4-mini",
|
32
32
|
"o3",
|
33
|
+
|
34
|
+
# Gemini models that support structured outputs
|
35
|
+
"gemini-2.0-flash",
|
36
|
+
"gemini-2.0-flash-exp",
|
37
|
+
"gemini-1.5-pro",
|
38
|
+
"gemini-1.5-pro-latest",
|
39
|
+
"gemini-1.5-flash",
|
40
|
+
"gemini-1.5-flash-latest",
|
41
|
+
"gemini-1.5-flash-8b",
|
42
|
+
"gemini-1.5-flash-8b-latest",
|
33
43
|
}
|
34
44
|
|
35
45
|
# Models that explicitly DON'T support structured outputs
|
@@ -57,16 +67,23 @@ def supports_structured_outputs(model_name: str) -> bool:
|
|
57
67
|
if not model_name:
|
58
68
|
return False
|
59
69
|
|
70
|
+
# Strip provider prefixes (e.g., 'google/', 'openai/', etc.)
|
71
|
+
model_without_provider = model_name
|
72
|
+
for prefix in ['google/', 'openai/', 'anthropic/', 'gemini/', 'mistral/', 'deepseek/', 'groq/']:
|
73
|
+
if model_name.startswith(prefix):
|
74
|
+
model_without_provider = model_name[len(prefix):]
|
75
|
+
break
|
76
|
+
|
60
77
|
# First check if it's explicitly in the NOT supporting list
|
61
|
-
if
|
78
|
+
if model_without_provider in MODELS_NOT_SUPPORTING_STRUCTURED_OUTPUTS:
|
62
79
|
return False
|
63
80
|
|
64
81
|
# Then check if it's in the supporting list
|
65
|
-
if
|
82
|
+
if model_without_provider in MODELS_SUPPORTING_STRUCTURED_OUTPUTS:
|
66
83
|
return True
|
67
84
|
|
68
85
|
# For models with version suffixes, check the base model name
|
69
|
-
base_model =
|
86
|
+
base_model = model_without_provider.split('-2024-')[0].split('-2025-')[0]
|
70
87
|
if base_model in MODELS_SUPPORTING_STRUCTURED_OUTPUTS:
|
71
88
|
return True
|
72
89
|
|
@@ -0,0 +1,348 @@
|
|
1
|
+
"""
|
2
|
+
Model Router for intelligent model selection based on task characteristics.
|
3
|
+
|
4
|
+
This module provides functionality to automatically select the most appropriate
|
5
|
+
LLM model/provider based on task complexity, cost considerations, and model capabilities.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import logging
|
10
|
+
from typing import Dict, List, Optional, Tuple
|
11
|
+
from dataclasses import dataclass
|
12
|
+
from enum import IntEnum
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class TaskComplexity(IntEnum):
|
18
|
+
"""Enum for task complexity levels"""
|
19
|
+
SIMPLE = 1 # Basic queries, math, factual questions
|
20
|
+
MODERATE = 2 # Summarization, basic analysis
|
21
|
+
COMPLEX = 3 # Code generation, deep reasoning
|
22
|
+
VERY_COMPLEX = 4 # Multi-step reasoning, complex analysis
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class ModelProfile:
|
27
|
+
"""Profile for an LLM model with its characteristics"""
|
28
|
+
name: str
|
29
|
+
provider: str
|
30
|
+
complexity_range: Tuple[TaskComplexity, TaskComplexity]
|
31
|
+
cost_per_1k_tokens: float # Average of input/output costs
|
32
|
+
strengths: List[str]
|
33
|
+
capabilities: List[str]
|
34
|
+
context_window: int
|
35
|
+
supports_tools: bool = True
|
36
|
+
supports_streaming: bool = True
|
37
|
+
|
38
|
+
|
39
|
+
class ModelRouter:
|
40
|
+
"""
|
41
|
+
Intelligent model router that selects the best model based on task requirements.
|
42
|
+
|
43
|
+
This router implements a strategy pattern for model selection, considering:
|
44
|
+
- Task complexity
|
45
|
+
- Cost optimization
|
46
|
+
- Model capabilities
|
47
|
+
- Specific strengths for different task types
|
48
|
+
"""
|
49
|
+
|
50
|
+
# Default model profiles - can be customized via configuration
|
51
|
+
DEFAULT_MODELS = [
|
52
|
+
# Lightweight/cheap models for simple tasks
|
53
|
+
ModelProfile(
|
54
|
+
name="gpt-4o-mini",
|
55
|
+
provider="openai",
|
56
|
+
complexity_range=(TaskComplexity.SIMPLE, TaskComplexity.MODERATE),
|
57
|
+
cost_per_1k_tokens=0.00075, # Average of $0.00015 input, $0.0006 output
|
58
|
+
strengths=["speed", "cost-effective", "basic-reasoning"],
|
59
|
+
capabilities=["text", "function-calling"],
|
60
|
+
context_window=128000
|
61
|
+
),
|
62
|
+
ModelProfile(
|
63
|
+
name="gemini/gemini-1.5-flash",
|
64
|
+
provider="google",
|
65
|
+
complexity_range=(TaskComplexity.SIMPLE, TaskComplexity.MODERATE),
|
66
|
+
cost_per_1k_tokens=0.000125, # Very cost-effective
|
67
|
+
strengths=["speed", "cost-effective", "multimodal"],
|
68
|
+
capabilities=["text", "vision", "function-calling"],
|
69
|
+
context_window=1048576
|
70
|
+
),
|
71
|
+
ModelProfile(
|
72
|
+
name="claude-3-haiku-20240307",
|
73
|
+
provider="anthropic",
|
74
|
+
complexity_range=(TaskComplexity.SIMPLE, TaskComplexity.MODERATE),
|
75
|
+
cost_per_1k_tokens=0.0008, # Average of $0.00025 input, $0.00125 output
|
76
|
+
strengths=["speed", "instruction-following"],
|
77
|
+
capabilities=["text", "function-calling"],
|
78
|
+
context_window=200000
|
79
|
+
),
|
80
|
+
|
81
|
+
# Mid-tier models for moderate complexity
|
82
|
+
ModelProfile(
|
83
|
+
name="gpt-4o",
|
84
|
+
provider="openai",
|
85
|
+
complexity_range=(TaskComplexity.MODERATE, TaskComplexity.COMPLEX),
|
86
|
+
cost_per_1k_tokens=0.0075, # Average of $0.0025 input, $0.01 output
|
87
|
+
strengths=["reasoning", "code-generation", "general-purpose"],
|
88
|
+
capabilities=["text", "vision", "function-calling"],
|
89
|
+
context_window=128000
|
90
|
+
),
|
91
|
+
ModelProfile(
|
92
|
+
name="claude-3-5-sonnet-20241022",
|
93
|
+
provider="anthropic",
|
94
|
+
complexity_range=(TaskComplexity.MODERATE, TaskComplexity.VERY_COMPLEX),
|
95
|
+
cost_per_1k_tokens=0.009, # Average of $0.003 input, $0.015 output
|
96
|
+
strengths=["reasoning", "code-generation", "analysis", "writing"],
|
97
|
+
capabilities=["text", "vision", "function-calling"],
|
98
|
+
context_window=200000
|
99
|
+
),
|
100
|
+
|
101
|
+
# High-end models for complex tasks
|
102
|
+
ModelProfile(
|
103
|
+
name="gemini/gemini-1.5-pro",
|
104
|
+
provider="google",
|
105
|
+
complexity_range=(TaskComplexity.COMPLEX, TaskComplexity.VERY_COMPLEX),
|
106
|
+
cost_per_1k_tokens=0.00625, # Average of $0.00125 input, $0.005 output
|
107
|
+
strengths=["reasoning", "long-context", "multimodal"],
|
108
|
+
capabilities=["text", "vision", "function-calling"],
|
109
|
+
context_window=2097152 # 2M context
|
110
|
+
),
|
111
|
+
ModelProfile(
|
112
|
+
name="claude-3-opus-20240229",
|
113
|
+
provider="anthropic",
|
114
|
+
complexity_range=(TaskComplexity.COMPLEX, TaskComplexity.VERY_COMPLEX),
|
115
|
+
cost_per_1k_tokens=0.045, # Average of $0.015 input, $0.075 output
|
116
|
+
strengths=["deep-reasoning", "complex-analysis", "creative-writing"],
|
117
|
+
capabilities=["text", "vision", "function-calling"],
|
118
|
+
context_window=200000
|
119
|
+
),
|
120
|
+
ModelProfile(
|
121
|
+
name="deepseek-chat",
|
122
|
+
provider="deepseek",
|
123
|
+
complexity_range=(TaskComplexity.COMPLEX, TaskComplexity.VERY_COMPLEX),
|
124
|
+
cost_per_1k_tokens=0.0014, # Very cost-effective for capability
|
125
|
+
strengths=["reasoning", "code-generation", "mathematics"],
|
126
|
+
capabilities=["text", "function-calling"],
|
127
|
+
context_window=128000
|
128
|
+
),
|
129
|
+
]
|
130
|
+
|
131
|
+
def __init__(
|
132
|
+
self,
|
133
|
+
models: Optional[List[ModelProfile]] = None,
|
134
|
+
default_model: Optional[str] = None,
|
135
|
+
cost_threshold: Optional[float] = None,
|
136
|
+
preferred_providers: Optional[List[str]] = None
|
137
|
+
):
|
138
|
+
"""
|
139
|
+
Initialize the ModelRouter.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
models: Custom list of model profiles to use
|
143
|
+
default_model: Default model to use if no suitable model found
|
144
|
+
cost_threshold: Maximum cost per 1k tokens to consider
|
145
|
+
preferred_providers: List of preferred providers in order
|
146
|
+
"""
|
147
|
+
self.models = models or self.DEFAULT_MODELS
|
148
|
+
self.default_model = default_model or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
|
149
|
+
self.cost_threshold = cost_threshold
|
150
|
+
self.preferred_providers = preferred_providers or []
|
151
|
+
|
152
|
+
# Build lookup indices for efficient access
|
153
|
+
self._model_by_name = {m.name: m for m in self.models}
|
154
|
+
self._models_by_complexity = self._build_complexity_index()
|
155
|
+
|
156
|
+
def _build_complexity_index(self) -> Dict[TaskComplexity, List[ModelProfile]]:
|
157
|
+
"""Build an index of models by complexity level"""
|
158
|
+
index = {level: [] for level in TaskComplexity}
|
159
|
+
|
160
|
+
for model in self.models:
|
161
|
+
min_complexity, max_complexity = model.complexity_range
|
162
|
+
for level in TaskComplexity:
|
163
|
+
if min_complexity.value <= level.value <= max_complexity.value:
|
164
|
+
index[level].append(model)
|
165
|
+
|
166
|
+
return index
|
167
|
+
|
168
|
+
def analyze_task_complexity(
|
169
|
+
self,
|
170
|
+
task_description: str,
|
171
|
+
tools_required: Optional[List[str]] = None,
|
172
|
+
context_size: Optional[int] = None
|
173
|
+
) -> TaskComplexity:
|
174
|
+
"""
|
175
|
+
Analyze task description to determine complexity level.
|
176
|
+
|
177
|
+
This is a simple heuristic-based approach. In production, this could be
|
178
|
+
replaced with a more sophisticated ML-based classifier.
|
179
|
+
"""
|
180
|
+
description_lower = task_description.lower()
|
181
|
+
|
182
|
+
# Keywords indicating different complexity levels
|
183
|
+
simple_keywords = [
|
184
|
+
"calculate", "compute", "what is", "define", "list", "count",
|
185
|
+
"simple", "basic", "check", "verify", "yes or no", "true or false"
|
186
|
+
]
|
187
|
+
|
188
|
+
moderate_keywords = [
|
189
|
+
"summarize", "explain", "compare", "describe", "analyze briefly",
|
190
|
+
"find", "search", "extract", "classify", "categorize"
|
191
|
+
]
|
192
|
+
|
193
|
+
complex_keywords = [
|
194
|
+
"implement", "code", "develop", "design", "create algorithm",
|
195
|
+
"optimize", "debug", "refactor", "architect", "solve"
|
196
|
+
]
|
197
|
+
|
198
|
+
very_complex_keywords = [
|
199
|
+
"multi-step", "comprehensive analysis", "deep dive", "research",
|
200
|
+
"strategic", "framework", "system design", "proof", "theorem"
|
201
|
+
]
|
202
|
+
|
203
|
+
# Check for keyword matches
|
204
|
+
if any(keyword in description_lower for keyword in very_complex_keywords):
|
205
|
+
return TaskComplexity.VERY_COMPLEX
|
206
|
+
elif any(keyword in description_lower for keyword in complex_keywords):
|
207
|
+
return TaskComplexity.COMPLEX
|
208
|
+
elif any(keyword in description_lower for keyword in moderate_keywords):
|
209
|
+
return TaskComplexity.MODERATE
|
210
|
+
elif any(keyword in description_lower for keyword in simple_keywords):
|
211
|
+
return TaskComplexity.SIMPLE
|
212
|
+
|
213
|
+
# Consider tool requirements
|
214
|
+
if tools_required and len(tools_required) > 3:
|
215
|
+
return TaskComplexity.COMPLEX
|
216
|
+
|
217
|
+
# Consider context size requirements
|
218
|
+
if context_size and context_size > 50000:
|
219
|
+
return TaskComplexity.COMPLEX
|
220
|
+
|
221
|
+
# Default to moderate
|
222
|
+
return TaskComplexity.MODERATE
|
223
|
+
|
224
|
+
def select_model(
|
225
|
+
self,
|
226
|
+
task_description: str,
|
227
|
+
required_capabilities: Optional[List[str]] = None,
|
228
|
+
tools_required: Optional[List[str]] = None,
|
229
|
+
context_size: Optional[int] = None,
|
230
|
+
budget_conscious: bool = True
|
231
|
+
) -> str:
|
232
|
+
"""
|
233
|
+
Select the most appropriate model for a given task.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
task_description: Description of the task to perform
|
237
|
+
required_capabilities: List of required capabilities (e.g., ["vision", "function-calling"])
|
238
|
+
tools_required: List of tools that will be used
|
239
|
+
context_size: Estimated context size needed
|
240
|
+
budget_conscious: Whether to optimize for cost
|
241
|
+
|
242
|
+
Returns:
|
243
|
+
Model name string to use
|
244
|
+
"""
|
245
|
+
# Analyze task complexity
|
246
|
+
complexity = self.analyze_task_complexity(task_description, tools_required, context_size)
|
247
|
+
|
248
|
+
# Get candidate models for this complexity level
|
249
|
+
candidates = self._models_by_complexity.get(complexity, [])
|
250
|
+
|
251
|
+
if not candidates:
|
252
|
+
logger.warning(f"No models found for complexity {complexity}, using default")
|
253
|
+
return self.default_model
|
254
|
+
|
255
|
+
# Filter by required capabilities
|
256
|
+
if required_capabilities:
|
257
|
+
candidates = [
|
258
|
+
m for m in candidates
|
259
|
+
if all(cap in m.capabilities for cap in required_capabilities)
|
260
|
+
]
|
261
|
+
|
262
|
+
# Filter by tool support if needed
|
263
|
+
if tools_required:
|
264
|
+
candidates = [m for m in candidates if m.supports_tools]
|
265
|
+
|
266
|
+
# Filter by context window if specified
|
267
|
+
if context_size:
|
268
|
+
candidates = [m for m in candidates if m.context_window >= context_size]
|
269
|
+
|
270
|
+
# Filter by cost threshold if specified
|
271
|
+
if self.cost_threshold:
|
272
|
+
candidates = [m for m in candidates if m.cost_per_1k_tokens <= self.cost_threshold]
|
273
|
+
|
274
|
+
if not candidates:
|
275
|
+
logger.warning("No models meet all criteria, using default")
|
276
|
+
return self.default_model
|
277
|
+
|
278
|
+
# Sort by selection criteria
|
279
|
+
if budget_conscious:
|
280
|
+
# Sort by cost (ascending)
|
281
|
+
candidates.sort(key=lambda m: m.cost_per_1k_tokens)
|
282
|
+
else:
|
283
|
+
# Sort by capability (descending complexity)
|
284
|
+
candidates.sort(key=lambda m: m.complexity_range[1].value, reverse=True)
|
285
|
+
|
286
|
+
# Apply provider preferences
|
287
|
+
if self.preferred_providers:
|
288
|
+
for provider in self.preferred_providers:
|
289
|
+
for model in candidates:
|
290
|
+
if model.provider == provider:
|
291
|
+
logger.info(f"Selected model: {model.name} (complexity: {complexity}, cost: ${model.cost_per_1k_tokens}/1k tokens)")
|
292
|
+
return model.name
|
293
|
+
|
294
|
+
# Return the best candidate
|
295
|
+
selected = candidates[0]
|
296
|
+
logger.info(f"Selected model: {selected.name} (complexity: {complexity}, cost: ${selected.cost_per_1k_tokens}/1k tokens)")
|
297
|
+
return selected.name
|
298
|
+
|
299
|
+
def get_model_info(self, model_name: str) -> Optional[ModelProfile]:
|
300
|
+
"""Get profile information for a specific model"""
|
301
|
+
return self._model_by_name.get(model_name)
|
302
|
+
|
303
|
+
def estimate_cost(self, model_name: str, estimated_tokens: int) -> float:
|
304
|
+
"""Estimate the cost for a given model and token count"""
|
305
|
+
model = self._model_by_name.get(model_name)
|
306
|
+
if not model:
|
307
|
+
return 0.0
|
308
|
+
return (model.cost_per_1k_tokens * estimated_tokens) / 1000
|
309
|
+
|
310
|
+
|
311
|
+
def create_routing_agent(
|
312
|
+
models: Optional[List[str]] = None,
|
313
|
+
router: Optional[ModelRouter] = None,
|
314
|
+
**agent_kwargs
|
315
|
+
) -> 'Agent':
|
316
|
+
"""
|
317
|
+
Create a specialized routing agent that can select models dynamically.
|
318
|
+
|
319
|
+
Args:
|
320
|
+
models: List of model names to route between
|
321
|
+
router: Custom ModelRouter instance
|
322
|
+
**agent_kwargs: Additional arguments to pass to Agent constructor
|
323
|
+
|
324
|
+
Returns:
|
325
|
+
Agent configured for model routing
|
326
|
+
"""
|
327
|
+
from ..agent import Agent
|
328
|
+
|
329
|
+
if not router:
|
330
|
+
router = ModelRouter()
|
331
|
+
|
332
|
+
routing_agent = Agent(
|
333
|
+
name=agent_kwargs.pop('name', 'ModelRouter'),
|
334
|
+
role=agent_kwargs.pop('role', 'Intelligent Model Router'),
|
335
|
+
goal=agent_kwargs.pop('goal', 'Select the most appropriate AI model based on task requirements'),
|
336
|
+
backstory=agent_kwargs.pop('backstory',
|
337
|
+
'I analyze tasks and route them to the most suitable AI model, '
|
338
|
+
'optimizing for performance, cost, and capability requirements.'
|
339
|
+
),
|
340
|
+
**agent_kwargs
|
341
|
+
)
|
342
|
+
|
343
|
+
# TODO: Consider creating a proper RoutingAgent subclass instead of setting private attributes
|
344
|
+
# For now, store the router on the agent for use in execution
|
345
|
+
routing_agent._model_router = router
|
346
|
+
routing_agent._available_models = models or [m.name for m in router.models]
|
347
|
+
|
348
|
+
return routing_agent
|
@@ -6,9 +6,9 @@ from pydantic import BaseModel, ConfigDict
|
|
6
6
|
from ..agent.agent import Agent
|
7
7
|
from ..task.task import Task
|
8
8
|
from ..main import display_error
|
9
|
+
from ..llm import LLM
|
9
10
|
import csv
|
10
11
|
import os
|
11
|
-
from openai import AsyncOpenAI, OpenAI
|
12
12
|
|
13
13
|
class LoopItems(BaseModel):
|
14
14
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
@@ -34,6 +34,29 @@ class Process:
|
|
34
34
|
self.task_retry_counter: Dict[str, int] = {} # Initialize retry counter
|
35
35
|
self.workflow_finished = False # ADDED: Workflow finished flag
|
36
36
|
|
37
|
+
def _create_llm_instance(self):
|
38
|
+
"""Create and return a configured LLM instance for manager tasks."""
|
39
|
+
return LLM(model=self.manager_llm, temperature=0.7)
|
40
|
+
|
41
|
+
def _parse_manager_instructions(self, response, ManagerInstructions):
|
42
|
+
"""Parse LLM response and return ManagerInstructions instance.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
response: String response from LLM
|
46
|
+
ManagerInstructions: Pydantic model class for validation
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
ManagerInstructions instance
|
50
|
+
|
51
|
+
Raises:
|
52
|
+
Exception: If parsing fails
|
53
|
+
"""
|
54
|
+
try:
|
55
|
+
parsed_json = json.loads(response)
|
56
|
+
return ManagerInstructions(**parsed_json)
|
57
|
+
except (json.JSONDecodeError, ValueError, TypeError) as e:
|
58
|
+
raise Exception(f"Failed to parse response: {response}") from e
|
59
|
+
|
37
60
|
def _create_loop_subtasks(self, loop_task: Task):
|
38
61
|
"""Create subtasks for a loop task from input file."""
|
39
62
|
logging.warning(f"_create_loop_subtasks called for {loop_task.name} but method not fully implemented")
|
@@ -167,24 +190,24 @@ class Process:
|
|
167
190
|
|
168
191
|
def _get_manager_instructions_with_fallback(self, manager_task, manager_prompt, ManagerInstructions):
|
169
192
|
"""Sync version of getting manager instructions with fallback"""
|
170
|
-
# Create
|
171
|
-
|
193
|
+
# Create LLM instance with the manager_llm
|
194
|
+
llm = self._create_llm_instance()
|
195
|
+
|
172
196
|
try:
|
173
|
-
#
|
197
|
+
# Use LLM with output_pydantic for structured output
|
174
198
|
logging.info("Attempting structured output...")
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
{"role": "user", "content": manager_prompt}
|
180
|
-
],
|
181
|
-
temperature=0.7,
|
182
|
-
response_format=ManagerInstructions
|
199
|
+
response = llm.get_response(
|
200
|
+
prompt=manager_prompt,
|
201
|
+
system_prompt=manager_task.description,
|
202
|
+
output_pydantic=ManagerInstructions
|
183
203
|
)
|
184
|
-
|
204
|
+
|
205
|
+
# Parse the response and validate with Pydantic
|
206
|
+
return self._parse_manager_instructions(response, ManagerInstructions)
|
207
|
+
|
185
208
|
except Exception as e:
|
186
209
|
logging.info(f"Structured output failed: {e}, falling back to JSON mode...")
|
187
|
-
# Fallback to regular JSON mode
|
210
|
+
# Fallback to regular JSON mode with explicit JSON instructions
|
188
211
|
try:
|
189
212
|
# Generate JSON structure description from Pydantic model
|
190
213
|
try:
|
@@ -200,23 +223,14 @@ class Process:
|
|
200
223
|
# Fallback to hardcoded prompt if schema generation fails
|
201
224
|
enhanced_prompt = manager_prompt + "\n\nIMPORTANT: Respond with valid JSON only, using this exact structure: {\"task_id\": <int>, \"agent_name\": \"<string>\", \"action\": \"<execute or stop>\"}"
|
202
225
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
{"role": "user", "content": enhanced_prompt}
|
208
|
-
],
|
209
|
-
temperature=0.7,
|
210
|
-
response_format={"type": "json_object"}
|
226
|
+
response = llm.get_response(
|
227
|
+
prompt=enhanced_prompt,
|
228
|
+
system_prompt=manager_task.description,
|
229
|
+
output_json=True
|
211
230
|
)
|
212
231
|
|
213
232
|
# Parse JSON and validate with Pydantic
|
214
|
-
|
215
|
-
json_content = manager_response.choices[0].message.content
|
216
|
-
parsed_json = json.loads(json_content)
|
217
|
-
return ManagerInstructions(**parsed_json)
|
218
|
-
except (json.JSONDecodeError, ValueError) as e:
|
219
|
-
raise Exception(f"Failed to parse JSON response: {json_content}") from e
|
233
|
+
return self._parse_manager_instructions(response, ManagerInstructions)
|
220
234
|
except Exception as fallback_error:
|
221
235
|
error_msg = f"Both structured output and JSON fallback failed: {fallback_error}"
|
222
236
|
logging.error(error_msg, exc_info=True)
|
@@ -224,40 +238,32 @@ class Process:
|
|
224
238
|
|
225
239
|
async def _get_structured_response_async(self, manager_task, manager_prompt, ManagerInstructions):
|
226
240
|
"""Async version of structured response"""
|
227
|
-
# Create
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
temperature=0.7,
|
236
|
-
response_format=ManagerInstructions
|
241
|
+
# Create LLM instance with the manager_llm
|
242
|
+
llm = self._create_llm_instance()
|
243
|
+
|
244
|
+
# Use async get_response with output_pydantic
|
245
|
+
response = await llm.get_response_async(
|
246
|
+
prompt=manager_prompt,
|
247
|
+
system_prompt=manager_task.description,
|
248
|
+
output_pydantic=ManagerInstructions
|
237
249
|
)
|
238
|
-
|
250
|
+
|
251
|
+
# Parse the response and validate with Pydantic
|
252
|
+
return self._parse_manager_instructions(response, ManagerInstructions)
|
239
253
|
|
240
254
|
async def _get_json_response_async(self, manager_task, enhanced_prompt, ManagerInstructions):
|
241
255
|
"""Async version of JSON fallback response"""
|
242
|
-
# Create
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
],
|
250
|
-
temperature=0.7,
|
251
|
-
response_format={"type": "json_object"}
|
256
|
+
# Create LLM instance with the manager_llm
|
257
|
+
llm = self._create_llm_instance()
|
258
|
+
|
259
|
+
response = await llm.get_response_async(
|
260
|
+
prompt=enhanced_prompt,
|
261
|
+
system_prompt=manager_task.description,
|
262
|
+
output_json=True
|
252
263
|
)
|
253
264
|
|
254
265
|
# Parse JSON and validate with Pydantic
|
255
|
-
|
256
|
-
json_content = manager_response.choices[0].message.content
|
257
|
-
parsed_json = json.loads(json_content)
|
258
|
-
return ManagerInstructions(**parsed_json)
|
259
|
-
except (json.JSONDecodeError, ValueError) as e:
|
260
|
-
raise Exception(f"Failed to parse JSON response: {json_content}") from e
|
266
|
+
return self._parse_manager_instructions(response, ManagerInstructions)
|
261
267
|
|
262
268
|
|
263
269
|
async def aworkflow(self) -> AsyncGenerator[str, None]:
|
@@ -469,16 +475,18 @@ Subtask: {st.name}
|
|
469
475
|
logging.debug(f"Task type: {task_to_check.task_type}")
|
470
476
|
logging.debug(f"Task status before reset check: {task_to_check.status}")
|
471
477
|
logging.debug(f"Task rerun: {getattr(task_to_check, 'rerun', True)}") # default to True if not set
|
478
|
+
logging.debug(f"Task async_execution: {task_to_check.async_execution}")
|
472
479
|
|
473
480
|
if (getattr(task_to_check, 'rerun', True) and # Corrected condition - reset only if rerun is True (or default True)
|
474
481
|
task_to_check.task_type != "loop" and # Removed "decision" from exclusion
|
475
482
|
not any(t.task_type == "loop" and subtask_name.startswith(t.name + "_")
|
476
|
-
for t in self.tasks.values())
|
477
|
-
|
483
|
+
for t in self.tasks.values()) and
|
484
|
+
not task_to_check.async_execution): # Don't reset async parallel tasks
|
485
|
+
logging.debug(f"=== Resetting non-loop, non-decision, non-parallel task {subtask_name} to 'not started' ===")
|
478
486
|
self.tasks[task_id].status = "not started"
|
479
487
|
logging.debug(f"Task status after reset: {self.tasks[task_id].status}")
|
480
488
|
else:
|
481
|
-
logging.debug(f"=== Skipping reset for loop/decision/subtask or rerun=False: {subtask_name} ===")
|
489
|
+
logging.debug(f"=== Skipping reset for loop/decision/subtask/parallel or rerun=False: {subtask_name} ===")
|
482
490
|
logging.debug(f"Keeping status as: {self.tasks[task_id].status}")
|
483
491
|
|
484
492
|
# Handle loop progression
|
@@ -1099,16 +1107,18 @@ Subtask: {st.name}
|
|
1099
1107
|
logging.debug(f"Task type: {task_to_check.task_type}")
|
1100
1108
|
logging.debug(f"Task status before reset check: {task_to_check.status}")
|
1101
1109
|
logging.debug(f"Task rerun: {getattr(task_to_check, 'rerun', True)}") # default to True if not set
|
1110
|
+
logging.debug(f"Task async_execution: {task_to_check.async_execution}")
|
1102
1111
|
|
1103
1112
|
if (getattr(task_to_check, 'rerun', True) and # Corrected condition - reset only if rerun is True (or default True)
|
1104
1113
|
task_to_check.task_type != "loop" and # Removed "decision" from exclusion
|
1105
1114
|
not any(t.task_type == "loop" and subtask_name.startswith(t.name + "_")
|
1106
|
-
for t in self.tasks.values())
|
1107
|
-
|
1115
|
+
for t in self.tasks.values()) and
|
1116
|
+
not task_to_check.async_execution): # Don't reset async parallel tasks
|
1117
|
+
logging.debug(f"=== Resetting non-loop, non-decision, non-parallel task {subtask_name} to 'not started' ===")
|
1108
1118
|
self.tasks[task_id].status = "not started"
|
1109
1119
|
logging.debug(f"Task status after reset: {self.tasks[task_id].status}")
|
1110
1120
|
else:
|
1111
|
-
logging.debug(f"=== Skipping reset for loop/decision/subtask or rerun=False: {subtask_name} ===")
|
1121
|
+
logging.debug(f"=== Skipping reset for loop/decision/subtask/parallel or rerun=False: {subtask_name} ===")
|
1112
1122
|
logging.debug(f"Keeping status as: {self.tasks[task_id].status}")
|
1113
1123
|
|
1114
1124
|
|
praisonaiagents/task/task.py
CHANGED
@@ -172,8 +172,15 @@ class Task:
|
|
172
172
|
# Check return annotation if present
|
173
173
|
return_annotation = sig.return_annotation
|
174
174
|
if return_annotation != inspect.Signature.empty:
|
175
|
+
# Import GuardrailResult for checking
|
176
|
+
from ..guardrails import GuardrailResult
|
177
|
+
|
178
|
+
# Check if it's a GuardrailResult type
|
179
|
+
is_guardrail_result = return_annotation is GuardrailResult
|
180
|
+
|
181
|
+
# Check for tuple return type
|
175
182
|
return_annotation_args = get_args(return_annotation)
|
176
|
-
|
183
|
+
is_tuple = (
|
177
184
|
get_origin(return_annotation) is tuple
|
178
185
|
and len(return_annotation_args) == 2
|
179
186
|
and return_annotation_args[0] is bool
|
@@ -183,9 +190,11 @@ class Task:
|
|
183
190
|
or return_annotation_args[1] is TaskOutput
|
184
191
|
or return_annotation_args[1] == Union[str, TaskOutput]
|
185
192
|
)
|
186
|
-
)
|
193
|
+
)
|
194
|
+
|
195
|
+
if not (is_guardrail_result or is_tuple):
|
187
196
|
raise ValueError(
|
188
|
-
"If return type is annotated, it must be Tuple[bool, Any]"
|
197
|
+
"If return type is annotated, it must be GuardrailResult or Tuple[bool, Any]"
|
189
198
|
)
|
190
199
|
|
191
200
|
self._guardrail_fn = self.guardrail
|
@@ -447,7 +456,11 @@ Context:
|
|
447
456
|
# Call the guardrail function
|
448
457
|
result = self._guardrail_fn(task_output)
|
449
458
|
|
450
|
-
#
|
459
|
+
# Check if result is already a GuardrailResult
|
460
|
+
if isinstance(result, GuardrailResult):
|
461
|
+
return result
|
462
|
+
|
463
|
+
# Otherwise, convert the tuple result to a GuardrailResult
|
451
464
|
return GuardrailResult.from_tuple(result)
|
452
465
|
|
453
466
|
except Exception as e:
|