praisonaiagents 0.0.16__py3-none-any.whl → 0.0.18__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,6 +16,8 @@ from .main import (
16
16
  display_generating,
17
17
  clean_triple_backticks,
18
18
  error_logs,
19
+ register_display_callback,
20
+ display_callbacks,
19
21
  )
20
22
 
21
23
  __all__ = [
@@ -32,4 +34,6 @@ __all__ = [
32
34
  'display_generating',
33
35
  'clean_triple_backticks',
34
36
  'error_logs',
37
+ 'register_display_callback',
38
+ 'display_callbacks',
35
39
  ]
@@ -1,9 +1,12 @@
1
- import logging
2
- import json
1
+ import os
3
2
  import time
3
+ import json
4
+ import logging
5
+ import asyncio
4
6
  from typing import List, Optional, Any, Dict, Union, Literal
5
7
  from rich.console import Console
6
8
  from rich.live import Live
9
+ from openai import AsyncOpenAI
7
10
  from ..main import (
8
11
  display_error,
9
12
  display_tool_call,
@@ -192,6 +195,12 @@ class Agent:
192
195
  self.min_reflect = min_reflect
193
196
  self.reflect_llm = reflect_llm
194
197
  self.console = Console() # Create a single console instance for the agent
198
+
199
+ # Initialize system prompt
200
+ self.system_prompt = f"""{self.backstory}\n
201
+ Your Role: {self.role}\n
202
+ Your Goal: {self.goal}
203
+ """
195
204
 
196
205
  def execute_tool(self, function_name, arguments):
197
206
  """
@@ -536,4 +545,143 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
536
545
  cleaned = cleaned[len("```"):].strip()
537
546
  if cleaned.endswith("```"):
538
547
  cleaned = cleaned[:-3].strip()
539
- return cleaned
548
+ return cleaned
549
+
550
+ async def achat(self, prompt, temperature=0.2, tools=None, output_json=None):
551
+ """Async version of chat method"""
552
+ try:
553
+ # Build system prompt
554
+ system_prompt = self.system_prompt
555
+ if output_json:
556
+ system_prompt += f"\nReturn ONLY a JSON object that matches this Pydantic model: {output_json.schema_json()}"
557
+
558
+ # Build messages
559
+ if isinstance(prompt, str):
560
+ messages = [
561
+ {"role": "system", "content": system_prompt},
562
+ {"role": "user", "content": prompt + ("\nReturn ONLY a valid JSON object. No other text or explanation." if output_json else "")}
563
+ ]
564
+ else:
565
+ # For multimodal prompts
566
+ messages = [
567
+ {"role": "system", "content": system_prompt},
568
+ {"role": "user", "content": prompt}
569
+ ]
570
+ if output_json:
571
+ # Add JSON instruction to text content
572
+ for item in messages[-1]["content"]:
573
+ if item["type"] == "text":
574
+ item["text"] += "\nReturn ONLY a valid JSON object. No other text or explanation."
575
+ break
576
+
577
+ # Format tools if provided
578
+ formatted_tools = []
579
+ if tools:
580
+ for tool in tools:
581
+ if isinstance(tool, str):
582
+ tool_def = self._generate_tool_definition(tool)
583
+ if tool_def:
584
+ formatted_tools.append(tool_def)
585
+ elif isinstance(tool, dict):
586
+ formatted_tools.append(tool)
587
+ elif hasattr(tool, "to_openai_tool"):
588
+ formatted_tools.append(tool.to_openai_tool())
589
+ elif callable(tool):
590
+ formatted_tools.append(self._generate_tool_definition(tool.__name__))
591
+
592
+ # Create async OpenAI client
593
+ async_client = AsyncOpenAI()
594
+
595
+ # Make the API call based on the type of request
596
+ if tools:
597
+ response = await async_client.chat.completions.create(
598
+ model=self.llm,
599
+ messages=messages,
600
+ temperature=temperature,
601
+ tools=formatted_tools
602
+ )
603
+ return await self._achat_completion(response, tools)
604
+ elif output_json:
605
+ response = await async_client.chat.completions.create(
606
+ model=self.llm,
607
+ messages=messages,
608
+ temperature=temperature,
609
+ response_format={"type": "json_object"}
610
+ )
611
+ result = response.choices[0].message.content
612
+ # Clean and parse the JSON response
613
+ cleaned_json = self.clean_json_output(result)
614
+ try:
615
+ parsed = json.loads(cleaned_json)
616
+ return output_json(**parsed)
617
+ except Exception as e:
618
+ display_error(f"Error parsing JSON response: {e}")
619
+ return None
620
+ else:
621
+ response = await async_client.chat.completions.create(
622
+ model=self.llm,
623
+ messages=messages,
624
+ temperature=temperature
625
+ )
626
+ return response.choices[0].message.content
627
+ except Exception as e:
628
+ display_error(f"Error in chat completion: {e}")
629
+ return None
630
+
631
+ async def _achat_completion(self, response, tools):
632
+ """Async version of _chat_completion method"""
633
+ try:
634
+ message = response.choices[0].message
635
+ if not hasattr(message, 'tool_calls') or not message.tool_calls:
636
+ return message.content
637
+
638
+ results = []
639
+ for tool_call in message.tool_calls:
640
+ try:
641
+ function_name = tool_call.function.name
642
+ arguments = json.loads(tool_call.function.arguments)
643
+
644
+ # Find the matching tool
645
+ tool = next((t for t in tools if t.__name__ == function_name), None)
646
+ if not tool:
647
+ display_error(f"Tool {function_name} not found")
648
+ continue
649
+
650
+ # Check if the tool is async
651
+ if asyncio.iscoroutinefunction(tool):
652
+ result = await tool(**arguments)
653
+ else:
654
+ # Run sync function in executor to avoid blocking
655
+ loop = asyncio.get_event_loop()
656
+ result = await loop.run_in_executor(None, lambda: tool(**arguments))
657
+
658
+ results.append(result)
659
+ except Exception as e:
660
+ display_error(f"Error executing tool {function_name}: {e}")
661
+ results.append(None)
662
+
663
+ # If we have results, format them into a response
664
+ if results:
665
+ formatted_results = "\n".join([str(r) for r in results if r is not None])
666
+ if formatted_results:
667
+ messages = [
668
+ {"role": "system", "content": self.system_prompt},
669
+ {"role": "assistant", "content": "Here are the tool results:"},
670
+ {"role": "user", "content": formatted_results + "\nPlease process these results and provide a final response."}
671
+ ]
672
+ try:
673
+ async_client = AsyncOpenAI()
674
+ final_response = await async_client.chat.completions.create(
675
+ model=self.llm,
676
+ messages=messages,
677
+ temperature=0.2
678
+ )
679
+ return final_response.choices[0].message.content
680
+ except Exception as e:
681
+ display_error(f"Error in final chat completion: {e}")
682
+ return formatted_results
683
+ return formatted_results
684
+ return None
685
+ except Exception as e:
686
+ display_error(f"Error in _achat_completion: {e}")
687
+ return None
@@ -11,6 +11,7 @@ from ..main import display_error, TaskOutput, error_logs, client
11
11
  from ..agent.agent import Agent
12
12
  from ..task.task import Task
13
13
  from ..process.process import Process, LoopItems
14
+ import asyncio
14
15
 
15
16
  def encode_file_to_base64(file_path: str) -> str:
16
17
  """Base64-encode a file."""
@@ -82,7 +83,8 @@ class PraisonAIAgents:
82
83
  return True
83
84
  return len(agent_output.strip()) > 0
84
85
 
85
- def execute_task(self, task_id):
86
+ async def aexecute_task(self, task_id):
87
+ """Async version of execute_task method"""
86
88
  if task_id not in self.tasks:
87
89
  display_error(f"Error: Task with ID {task_id} does not exist")
88
90
  return
@@ -159,12 +161,12 @@ Expected Output: {task.expected_output}.
159
161
  })
160
162
  return content
161
163
 
162
- agent_output = executor_agent.chat(
164
+ agent_output = await executor_agent.achat(
163
165
  _get_multimodal_message(task_prompt, task.images),
164
166
  tools=task.tools
165
167
  )
166
168
  else:
167
- agent_output = executor_agent.chat(task_prompt, tools=task.tools)
169
+ agent_output = await executor_agent.achat(task_prompt, tools=task.tools)
168
170
 
169
171
  if agent_output:
170
172
  task_output = TaskOutput(
@@ -202,6 +204,83 @@ Expected Output: {task.expected_output}.
202
204
  task.status = "failed"
203
205
  return None
204
206
 
207
+ async def arun_task(self, task_id):
208
+ """Async version of run_task method"""
209
+ if task_id not in self.tasks:
210
+ display_error(f"Error: Task with ID {task_id} does not exist")
211
+ return
212
+ task = self.tasks[task_id]
213
+ if task.status == "completed":
214
+ logging.info(f"Task with ID {task_id} is already completed")
215
+ return
216
+
217
+ retries = 0
218
+ while task.status != "completed" and retries < self.max_retries:
219
+ logging.debug(f"Attempt {retries+1} for task {task_id}")
220
+ if task.status in ["not started", "in progress"]:
221
+ task_output = await self.aexecute_task(task_id)
222
+ if task_output and self.completion_checker(task, task_output.raw):
223
+ task.status = "completed"
224
+ if task.callback:
225
+ await task.execute_callback(task_output)
226
+ self.save_output_to_file(task, task_output)
227
+ if self.verbose >= 1:
228
+ logging.info(f"Task {task_id} completed successfully.")
229
+ else:
230
+ task.status = "in progress"
231
+ if self.verbose >= 1:
232
+ logging.info(f"Task {task_id} not completed, retrying")
233
+ await asyncio.sleep(1)
234
+ retries += 1
235
+ else:
236
+ if task.status == "failed":
237
+ logging.info("Task is failed, resetting to in-progress for another try...")
238
+ task.status = "in progress"
239
+ else:
240
+ logging.info("Invalid Task status")
241
+ break
242
+
243
+ if retries == self.max_retries and task.status != "completed":
244
+ logging.info(f"Task {task_id} failed after {self.max_retries} retries.")
245
+
246
+ async def arun_all_tasks(self):
247
+ """Async version of run_all_tasks method"""
248
+ process = Process(
249
+ tasks=self.tasks,
250
+ agents=self.agents,
251
+ manager_llm=self.manager_llm,
252
+ verbose=self.verbose
253
+ )
254
+
255
+ if self.process == "workflow":
256
+ async for task_id in process.aworkflow():
257
+ if self.tasks[task_id].async_execution:
258
+ await self.arun_task(task_id)
259
+ else:
260
+ self.run_task(task_id)
261
+ elif self.process == "sequential":
262
+ async for task_id in process.asequential():
263
+ if self.tasks[task_id].async_execution:
264
+ await self.arun_task(task_id)
265
+ else:
266
+ self.run_task(task_id)
267
+ elif self.process == "hierarchical":
268
+ async for task_id in process.ahierarchical():
269
+ if isinstance(task_id, Task):
270
+ task_id = self.add_task(task_id)
271
+ if self.tasks[task_id].async_execution:
272
+ await self.arun_task(task_id)
273
+ else:
274
+ self.run_task(task_id)
275
+
276
+ async def astart(self):
277
+ """Async version of start method"""
278
+ await self.arun_all_tasks()
279
+ return {
280
+ "task_status": self.get_all_tasks_status(),
281
+ "task_results": {task_id: self.get_task_result(task_id) for task_id in self.tasks}
282
+ }
283
+
205
284
  def save_output_to_file(self, task, task_output):
206
285
  if task.output_file:
207
286
  try:
@@ -214,7 +293,129 @@ Expected Output: {task.expected_output}.
214
293
  except Exception as e:
215
294
  display_error(f"Error saving task output to file: {e}")
216
295
 
296
+ def execute_task(self, task_id):
297
+ """Synchronous version of execute_task method"""
298
+ if task_id not in self.tasks:
299
+ display_error(f"Error: Task with ID {task_id} does not exist")
300
+ return
301
+ task = self.tasks[task_id]
302
+
303
+ # Only import multimodal dependencies if task has images
304
+ if task.images and task.status == "not started":
305
+ try:
306
+ import cv2
307
+ import base64
308
+ from moviepy import VideoFileClip
309
+ except ImportError as e:
310
+ display_error(f"Error: Missing required dependencies for image/video processing: {e}")
311
+ display_error("Please install with: pip install opencv-python moviepy")
312
+ task.status = "failed"
313
+ return None
314
+
315
+ if task.status == "not started":
316
+ task.status = "in progress"
317
+
318
+ executor_agent = task.agent
319
+
320
+ task_prompt = f"""
321
+ You need to do the following task: {task.description}.
322
+ Expected Output: {task.expected_output}.
323
+ """
324
+ if task.context:
325
+ context_results = ""
326
+ for context_task in task.context:
327
+ if context_task.result:
328
+ context_results += f"Result of previous task {context_task.name if context_task.name else context_task.description}: {context_task.result.raw}\n"
329
+ else:
330
+ context_results += f"Previous task {context_task.name if context_task.name else context_task.description} had no result.\n"
331
+ task_prompt += f"""
332
+ Here are the results of previous tasks that might be useful:\n
333
+ {context_results}
334
+ """
335
+ task_prompt += "Please provide only the final result of your work. Do not add any conversation or extra explanation."
336
+
337
+ if self.verbose >= 2:
338
+ logging.info(f"Executing task {task_id}: {task.description} using {executor_agent.name}")
339
+ logging.debug(f"Starting execution of task {task_id} with prompt:\n{task_prompt}")
340
+
341
+ if task.images:
342
+ def _get_multimodal_message(text_prompt, images):
343
+ content = [{"type": "text", "text": text_prompt}]
344
+
345
+ for img in images:
346
+ # If local file path for a valid image
347
+ if os.path.exists(img):
348
+ ext = os.path.splitext(img)[1].lower()
349
+ # If it's a .mp4, convert to frames
350
+ if ext == ".mp4":
351
+ frames = process_video(img, seconds_per_frame=1)
352
+ content.append({"type": "text", "text": "These are frames from the video."})
353
+ for f in frames:
354
+ content.append({
355
+ "type": "image_url",
356
+ "image_url": {"url": f"data:image/jpg;base64,{f}"}
357
+ })
358
+ else:
359
+ encoded = encode_file_to_base64(img)
360
+ content.append({
361
+ "type": "image_url",
362
+ "image_url": {
363
+ "url": f"data:image/{ext.lstrip('.')};base64,{encoded}"
364
+ }
365
+ })
366
+ else:
367
+ # Treat as a remote URL
368
+ content.append({
369
+ "type": "image_url",
370
+ "image_url": {"url": img}
371
+ })
372
+ return content
373
+
374
+ agent_output = executor_agent.chat(
375
+ _get_multimodal_message(task_prompt, task.images),
376
+ tools=task.tools
377
+ )
378
+ else:
379
+ agent_output = executor_agent.chat(task_prompt, tools=task.tools)
380
+
381
+ if agent_output:
382
+ task_output = TaskOutput(
383
+ description=task.description,
384
+ summary=task.description[:10],
385
+ raw=agent_output,
386
+ agent=executor_agent.name,
387
+ output_format="RAW"
388
+ )
389
+
390
+ if task.output_json:
391
+ cleaned = self.clean_json_output(agent_output)
392
+ try:
393
+ parsed = json.loads(cleaned)
394
+ task_output.json_dict = parsed
395
+ task_output.output_format = "JSON"
396
+ except:
397
+ logging.warning(f"Warning: Could not parse output of task {task_id} as JSON")
398
+ logging.debug(f"Output that failed JSON parsing: {agent_output}")
399
+
400
+ if task.output_pydantic:
401
+ cleaned = self.clean_json_output(agent_output)
402
+ try:
403
+ parsed = json.loads(cleaned)
404
+ pyd_obj = task.output_pydantic(**parsed)
405
+ task_output.pydantic = pyd_obj
406
+ task_output.output_format = "Pydantic"
407
+ except:
408
+ logging.warning(f"Warning: Could not parse output of task {task_id} as Pydantic Model")
409
+ logging.debug(f"Output that failed Pydantic parsing: {agent_output}")
410
+
411
+ task.result = task_output
412
+ return task_output
413
+ else:
414
+ task.status = "failed"
415
+ return None
416
+
217
417
  def run_task(self, task_id):
418
+ """Synchronous version of run_task method"""
218
419
  if task_id not in self.tasks:
219
420
  display_error(f"Error: Task with ID {task_id} does not exist")
220
421
  return
@@ -253,7 +454,7 @@ Expected Output: {task.expected_output}.
253
454
  logging.info(f"Task {task_id} failed after {self.max_retries} retries.")
254
455
 
255
456
  def run_all_tasks(self):
256
- """Execute tasks based on execution mode"""
457
+ """Synchronous version of run_all_tasks method"""
257
458
  process = Process(
258
459
  tasks=self.tasks,
259
460
  agents=self.agents,
praisonaiagents/main.py CHANGED
@@ -25,6 +25,13 @@ logging.basicConfig(
25
25
  # Global list to store error logs
26
26
  error_logs = []
27
27
 
28
+ # Global callback registry
29
+ display_callbacks = {}
30
+
31
+ def register_display_callback(display_type: str, callback_fn):
32
+ """Register a callback function for a specific display type."""
33
+ display_callbacks[display_type] = callback_fn
34
+
28
35
  def _clean_display_content(content: str, max_length: int = 20000) -> str:
29
36
  """Helper function to clean and truncate content for display."""
30
37
  if not content or not str(content).strip():
@@ -49,18 +56,28 @@ def display_interaction(message, response, markdown=True, generation_time=None,
49
56
  """Display the interaction between user and assistant."""
50
57
  if console is None:
51
58
  console = Console()
52
- if generation_time:
53
- console.print(Text(f"Response generated in {generation_time:.1f}s", style="dim"))
54
-
59
+
55
60
  # Handle multimodal content (list)
56
61
  if isinstance(message, list):
57
- # Extract just the text content from the multimodal message
58
62
  text_content = next((item["text"] for item in message if item["type"] == "text"), "")
59
63
  message = text_content
60
64
 
61
65
  message = _clean_display_content(str(message))
62
66
  response = _clean_display_content(str(response))
63
67
 
68
+ # Execute callback if registered
69
+ if 'interaction' in display_callbacks:
70
+ display_callbacks['interaction'](
71
+ message=message,
72
+ response=response,
73
+ markdown=markdown,
74
+ generation_time=generation_time
75
+ )
76
+
77
+ # Existing display logic...
78
+ if generation_time:
79
+ console.print(Text(f"Response generated in {generation_time:.1f}s", style="dim"))
80
+
64
81
  if markdown:
65
82
  console.print(Panel.fit(Markdown(message), title="Message", border_style="cyan"))
66
83
  console.print(Panel.fit(Markdown(response), title="Response", border_style="cyan"))
@@ -74,6 +91,11 @@ def display_self_reflection(message: str, console=None):
74
91
  if console is None:
75
92
  console = Console()
76
93
  message = _clean_display_content(str(message))
94
+
95
+ # Execute callback if registered
96
+ if 'self_reflection' in display_callbacks:
97
+ display_callbacks['self_reflection'](message=message)
98
+
77
99
  console.print(Panel.fit(Text(message, style="bold yellow"), title="Self Reflection", border_style="magenta"))
78
100
 
79
101
  def display_instruction(message: str, console=None):
@@ -82,6 +104,11 @@ def display_instruction(message: str, console=None):
82
104
  if console is None:
83
105
  console = Console()
84
106
  message = _clean_display_content(str(message))
107
+
108
+ # Execute callback if registered
109
+ if 'instruction' in display_callbacks:
110
+ display_callbacks['instruction'](message=message)
111
+
85
112
  console.print(Panel.fit(Text(message, style="bold blue"), title="Instruction", border_style="cyan"))
86
113
 
87
114
  def display_tool_call(message: str, console=None):
@@ -90,6 +117,11 @@ def display_tool_call(message: str, console=None):
90
117
  if console is None:
91
118
  console = Console()
92
119
  message = _clean_display_content(str(message))
120
+
121
+ # Execute callback if registered
122
+ if 'tool_call' in display_callbacks:
123
+ display_callbacks['tool_call'](message=message)
124
+
93
125
  console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
94
126
 
95
127
  def display_error(message: str, console=None):
@@ -98,19 +130,32 @@ def display_error(message: str, console=None):
98
130
  if console is None:
99
131
  console = Console()
100
132
  message = _clean_display_content(str(message))
133
+
134
+ # Execute callback if registered
135
+ if 'error' in display_callbacks:
136
+ display_callbacks['error'](message=message)
137
+
101
138
  console.print(Panel.fit(Text(message, style="bold red"), title="Error", border_style="red"))
102
- # Store errors
103
139
  error_logs.append(message)
104
140
 
105
141
  def display_generating(content: str = "", start_time: Optional[float] = None):
106
142
  if not content or not str(content).strip():
107
- return Panel("", title="", border_style="green") # Return empty panel when no content
143
+ return Panel("", title="", border_style="green")
144
+
108
145
  elapsed_str = ""
109
146
  if start_time is not None:
110
147
  elapsed = time.time() - start_time
111
148
  elapsed_str = f" {elapsed:.1f}s"
112
149
 
113
150
  content = _clean_display_content(str(content))
151
+
152
+ # Execute callback if registered
153
+ if 'generating' in display_callbacks:
154
+ display_callbacks['generating'](
155
+ content=content,
156
+ elapsed_time=elapsed_str.strip() if elapsed_str else None
157
+ )
158
+
114
159
  return Panel(Markdown(content), title=f"Generating...{elapsed_str}", border_style="green")
115
160
 
116
161
  def clean_triple_backticks(text: str) -> str:
@@ -1,5 +1,6 @@
1
1
  import logging
2
- from typing import Dict, Optional, List, Any
2
+ import asyncio
3
+ from typing import Dict, Optional, List, Any, AsyncGenerator
3
4
  from pydantic import BaseModel
4
5
  from ..agent.agent import Agent
5
6
  from ..task.task import Task
@@ -15,7 +16,275 @@ class Process:
15
16
  self.manager_llm = manager_llm
16
17
  self.verbose = verbose
17
18
 
19
+ async def aworkflow(self) -> AsyncGenerator[str, None]:
20
+ """Async version of workflow method"""
21
+ # Build workflow relationships first
22
+ for task in self.tasks.values():
23
+ if task.next_tasks:
24
+ for next_task_name in task.next_tasks:
25
+ next_task = next((t for t in self.tasks.values() if t.name == next_task_name), None)
26
+ if next_task:
27
+ next_task.previous_tasks.append(task.name)
28
+
29
+ # Find start task
30
+ start_task = None
31
+ for task_id, task in self.tasks.items():
32
+ if task.is_start:
33
+ start_task = task
34
+ break
35
+
36
+ if not start_task:
37
+ start_task = list(self.tasks.values())[0]
38
+ logging.info("No start task marked, using first task")
39
+
40
+ current_task = start_task
41
+ visited_tasks = set()
42
+ loop_data = {} # Store loop-specific data
43
+
44
+ while current_task and current_task.id not in visited_tasks:
45
+ task_id = current_task.id
46
+ logging.info(f"Executing workflow task: {current_task.name if current_task.name else task_id}")
47
+
48
+ # Add context from previous tasks to description
49
+ if current_task.previous_tasks or current_task.context:
50
+ context = "\nInput data from previous tasks:"
51
+
52
+ # Add data from previous tasks in workflow
53
+ for prev_name in current_task.previous_tasks:
54
+ prev_task = next((t for t in self.tasks.values() if t.name == prev_name), None)
55
+ if prev_task and prev_task.result:
56
+ # Handle loop data
57
+ if current_task.task_type == "loop":
58
+ # create a loop manager Agent
59
+ loop_manager = Agent(
60
+ name="Loop Manager",
61
+ role="Loop data processor",
62
+ goal="Process loop data and convert it to list format",
63
+ backstory="Expert at handling loop data and converting it to proper format",
64
+ llm=self.manager_llm,
65
+ verbose=self.verbose,
66
+ markdown=True
67
+ )
68
+
69
+ # get the loop data convert it to list using calling Agent class chat
70
+ loop_prompt = f"""
71
+ Process this data into a list format:
72
+ {prev_task.result.raw}
73
+
74
+ Return a JSON object with an 'items' array containing the items to process.
75
+ """
76
+ if current_task.async_execution:
77
+ loop_data_str = await loop_manager.achat(
78
+ prompt=loop_prompt,
79
+ output_json=LoopItems
80
+ )
81
+ else:
82
+ loop_data_str = loop_manager.chat(
83
+ prompt=loop_prompt,
84
+ output_json=LoopItems
85
+ )
86
+
87
+ try:
88
+ # The response will already be parsed into LoopItems model
89
+ loop_data[f"loop_{current_task.name}"] = {
90
+ "items": loop_data_str.items,
91
+ "index": 0,
92
+ "remaining": len(loop_data_str.items)
93
+ }
94
+ context += f"\nCurrent loop item: {loop_data_str.items[0]}"
95
+ except Exception as e:
96
+ display_error(f"Failed to process loop data: {e}")
97
+ context += f"\n{prev_name}: {prev_task.result.raw}"
98
+ else:
99
+ context += f"\n{prev_name}: {prev_task.result.raw}"
100
+
101
+ # Add data from context tasks
102
+ if current_task.context:
103
+ for ctx_task in current_task.context:
104
+ if ctx_task.result and ctx_task.name != current_task.name:
105
+ context += f"\n{ctx_task.name}: {ctx_task.result.raw}"
106
+
107
+ # Update task description with context
108
+ current_task.description = current_task.description + context
109
+
110
+ # Execute task using existing run_task method
111
+ yield task_id
112
+ visited_tasks.add(task_id)
113
+
114
+ # Handle loop progression
115
+ if current_task.task_type == "loop":
116
+ loop_key = f"loop_{current_task.name}"
117
+ if loop_key in loop_data:
118
+ loop_info = loop_data[loop_key]
119
+ loop_info["index"] += 1
120
+ has_more = loop_info["remaining"] > 0
121
+
122
+ # Update result to trigger correct condition
123
+ if current_task.result:
124
+ result = current_task.result.raw
125
+ if has_more:
126
+ result += "\nmore"
127
+ else:
128
+ result += "\ndone"
129
+ current_task.result.raw = result
130
+
131
+ # Determine next task based on result
132
+ next_task = None
133
+ if current_task.result:
134
+ if current_task.task_type in ["decision", "loop"]:
135
+ result = current_task.result.raw.lower()
136
+ # Check conditions
137
+ for condition, tasks in current_task.condition.items():
138
+ if condition.lower() in result and tasks:
139
+ next_task_name = tasks[0]
140
+ next_task = next((t for t in self.tasks.values() if t.name == next_task_name), None)
141
+ # For loops, allow revisiting the same task
142
+ if next_task and next_task.id == current_task.id:
143
+ visited_tasks.discard(current_task.id)
144
+ break
145
+
146
+ if not next_task and current_task.next_tasks:
147
+ next_task_name = current_task.next_tasks[0]
148
+ next_task = next((t for t in self.tasks.values() if t.name == next_task_name), None)
149
+
150
+ current_task = next_task
151
+ if not current_task:
152
+ logging.info("Workflow execution completed")
153
+ break
154
+
155
+ async def asequential(self) -> AsyncGenerator[str, None]:
156
+ """Async version of sequential method"""
157
+ for task_id in self.tasks:
158
+ if self.tasks[task_id].status != "completed":
159
+ yield task_id
160
+
161
+ async def ahierarchical(self) -> AsyncGenerator[str, None]:
162
+ """Async version of hierarchical method"""
163
+ logging.debug(f"Starting hierarchical task execution with {len(self.tasks)} tasks")
164
+ manager_agent = Agent(
165
+ name="Manager",
166
+ role="Project manager",
167
+ goal="Manage the entire flow of tasks and delegate them to the right agent",
168
+ backstory="Expert project manager to coordinate tasks among agents",
169
+ llm=self.manager_llm,
170
+ verbose=self.verbose,
171
+ markdown=True,
172
+ self_reflect=False
173
+ )
174
+
175
+ class ManagerInstructions(BaseModel):
176
+ task_id: int
177
+ agent_name: str
178
+ action: str
179
+
180
+ manager_task = Task(
181
+ name="manager_task",
182
+ description="Decide the order of tasks and which agent executes them",
183
+ expected_output="All tasks completed successfully",
184
+ agent=manager_agent
185
+ )
186
+ manager_task_id = yield manager_task
187
+ logging.info(f"Created manager task with ID {manager_task_id}")
188
+
189
+ completed_count = 0
190
+ total_tasks = len(self.tasks) - 1
191
+ logging.info(f"Need to complete {total_tasks} tasks (excluding manager task)")
192
+
193
+ while completed_count < total_tasks:
194
+ tasks_summary = []
195
+ for tid, tk in self.tasks.items():
196
+ if tk.name == "manager_task":
197
+ continue
198
+ task_info = {
199
+ "task_id": tid,
200
+ "name": tk.name,
201
+ "description": tk.description,
202
+ "status": tk.status if tk.status else "not started",
203
+ "agent": tk.agent.name if tk.agent else "No agent"
204
+ }
205
+ tasks_summary.append(task_info)
206
+ logging.info(f"Task {tid} status: {task_info}")
207
+
208
+ manager_prompt = f"""
209
+ Here is the current status of all tasks except yours (manager_task):
210
+ {tasks_summary}
211
+
212
+ Provide a JSON with the structure:
213
+ {{
214
+ "task_id": <int>,
215
+ "agent_name": "<string>",
216
+ "action": "<execute or stop>"
217
+ }}
218
+ """
219
+
220
+ try:
221
+ logging.info("Requesting manager instructions...")
222
+ if manager_task.async_execution:
223
+ manager_response = await client.beta.chat.completions.parse(
224
+ model=self.manager_llm,
225
+ messages=[
226
+ {"role": "system", "content": manager_task.description},
227
+ {"role": "user", "content": manager_prompt}
228
+ ],
229
+ temperature=0.7,
230
+ response_format=ManagerInstructions
231
+ )
232
+ else:
233
+ manager_response = client.beta.chat.completions.parse(
234
+ model=self.manager_llm,
235
+ messages=[
236
+ {"role": "system", "content": manager_task.description},
237
+ {"role": "user", "content": manager_prompt}
238
+ ],
239
+ temperature=0.7,
240
+ response_format=ManagerInstructions
241
+ )
242
+ parsed_instructions = manager_response.choices[0].message.parsed
243
+ logging.info(f"Manager instructions: {parsed_instructions}")
244
+ except Exception as e:
245
+ display_error(f"Manager parse error: {e}")
246
+ logging.error(f"Manager parse error: {str(e)}", exc_info=True)
247
+ break
248
+
249
+ selected_task_id = parsed_instructions.task_id
250
+ selected_agent_name = parsed_instructions.agent_name
251
+ action = parsed_instructions.action
252
+
253
+ logging.info(f"Manager selected task_id={selected_task_id}, agent={selected_agent_name}, action={action}")
254
+
255
+ if action.lower() == "stop":
256
+ logging.info("Manager decided to stop task execution")
257
+ break
258
+
259
+ if selected_task_id not in self.tasks:
260
+ error_msg = f"Manager selected invalid task id {selected_task_id}"
261
+ display_error(error_msg)
262
+ logging.error(error_msg)
263
+ break
264
+
265
+ original_agent = self.tasks[selected_task_id].agent.name if self.tasks[selected_task_id].agent else "None"
266
+ for a in self.agents:
267
+ if a.name == selected_agent_name:
268
+ self.tasks[selected_task_id].agent = a
269
+ logging.info(f"Changed agent for task {selected_task_id} from {original_agent} to {selected_agent_name}")
270
+ break
271
+
272
+ if self.tasks[selected_task_id].status != "completed":
273
+ logging.info(f"Starting execution of task {selected_task_id}")
274
+ yield selected_task_id
275
+ logging.info(f"Finished execution of task {selected_task_id}, status: {self.tasks[selected_task_id].status}")
276
+
277
+ if self.tasks[selected_task_id].status == "completed":
278
+ completed_count += 1
279
+ logging.info(f"Task {selected_task_id} completed. Total completed: {completed_count}/{total_tasks}")
280
+
281
+ self.tasks[manager_task.id].status = "completed"
282
+ if self.verbose >= 1:
283
+ logging.info("All tasks completed under manager supervision.")
284
+ logging.info("Hierarchical task execution finished")
285
+
18
286
  def workflow(self):
287
+ """Synchronous version of workflow method"""
19
288
  # Build workflow relationships first
20
289
  for task in self.tasks.values():
21
290
  if task.next_tasks:
@@ -145,11 +414,13 @@ Return a JSON object with an 'items' array containing the items to process.
145
414
  break
146
415
 
147
416
  def sequential(self):
417
+ """Synchronous version of sequential method"""
148
418
  for task_id in self.tasks:
149
419
  if self.tasks[task_id].status != "completed":
150
420
  yield task_id
151
421
 
152
422
  def hierarchical(self):
423
+ """Synchronous version of hierarchical method"""
153
424
  logging.debug(f"Starting hierarchical task execution with {len(self.tasks)} tasks")
154
425
  manager_agent = Agent(
155
426
  name="Manager",
@@ -1,5 +1,6 @@
1
1
  import logging
2
- from typing import List, Optional, Dict, Any, Type, Callable, Union
2
+ import asyncio
3
+ from typing import List, Optional, Dict, Any, Type, Callable, Union, Coroutine
3
4
  from pydantic import BaseModel
4
5
  from ..main import TaskOutput
5
6
  from ..agent.agent import Agent
@@ -19,7 +20,7 @@ class Task:
19
20
  output_file: Optional[str] = None,
20
21
  output_json: Optional[Type[BaseModel]] = None,
21
22
  output_pydantic: Optional[Type[BaseModel]] = None,
22
- callback: Optional[Any] = None,
23
+ callback: Optional[Union[Callable[[TaskOutput], Any], Callable[[TaskOutput], Coroutine[Any, Any, Any]]]] = None,
23
24
  status: str = "not started",
24
25
  result: Optional[TaskOutput] = None,
25
26
  create_directory: Optional[bool] = False,
@@ -61,4 +62,13 @@ class Task:
61
62
  self.previous_tasks = []
62
63
 
63
64
  def __str__(self):
64
- return f"Task(name='{self.name if self.name else 'None'}', description='{self.description}', agent='{self.agent.name if self.agent else 'None'}', status='{self.status}')"
65
+ return f"Task(name='{self.name if self.name else 'None'}', description='{self.description}', agent='{self.agent.name if self.agent else 'None'}', status='{self.status}')"
66
+
67
+ async def execute_callback(self, task_output: TaskOutput) -> None:
68
+ """Execute the callback function, handling both sync and async callbacks"""
69
+ if self.callback:
70
+ if asyncio.iscoroutinefunction(self.callback):
71
+ await self.callback(task_output)
72
+ else:
73
+ loop = asyncio.get_event_loop()
74
+ await loop.run_in_executor(None, self.callback, task_output)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: praisonaiagents
3
- Version: 0.0.16
3
+ Version: 0.0.18
4
4
  Summary: Praison AI agents for completing complex tasks with Self Reflection Agents
5
5
  Author: Mervin Praison
6
6
  Requires-Dist: pydantic
@@ -1,9 +1,9 @@
1
- praisonaiagents/__init__.py,sha256=gI8vEabBTRPsE_E8GA5sBMi4sTtJI-YokPrH2Nor-k0,741
2
- praisonaiagents/main.py,sha256=K2OxVKPmo4dNJbSWIsXDi_hm9CRx5O4km_74UGcszhk,5744
1
+ praisonaiagents/__init__.py,sha256=bXQwi56S1iZOXs_TyXs3doxVxAeRAyqKVyZEIoBOipM,853
2
+ praisonaiagents/main.py,sha256=QP45NxcrVs4PboVWcVkvLcsqiU7EzgE1ukH13jcYKfo,6985
3
3
  praisonaiagents/agent/__init__.py,sha256=sKO8wGEXvtCrvV1e834r1Okv0XAqAxqZCqz6hKLiTvA,79
4
- praisonaiagents/agent/agent.py,sha256=zTYcDpJ5DzzBnefwLvhrtBlGQoRI4ZZAioDu5nKTPSs,24042
4
+ praisonaiagents/agent/agent.py,sha256=_UmUWGbZjd3tApPX2T6RPB5Pll3Gos97XBhhg_zmfn8,30662
5
5
  praisonaiagents/agents/__init__.py,sha256=7RDeQNSqZg5uBjD4M_0p_F6YgfWuDuxPFydPU50kDYc,120
6
- praisonaiagents/agents/agents.py,sha256=-1Z-jIN2fkLM9P6nAq-OtKPTF8HIjA-a_Zznv8TfpFY,12964
6
+ praisonaiagents/agents/agents.py,sha256=P8JJ1849-djMDkMuP0kNhPwtg97L8gO60jYXzXFcPc0,21762
7
7
  praisonaiagents/build/lib/praisonaiagents/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
8
8
  praisonaiagents/build/lib/praisonaiagents/main.py,sha256=zDhN5KKtKbfruolDNxlyJkcFlkSt4KQkQTDRfQVAhxc,3960
9
9
  praisonaiagents/build/lib/praisonaiagents/agent/__init__.py,sha256=sKO8wGEXvtCrvV1e834r1Okv0XAqAxqZCqz6hKLiTvA,79
@@ -13,10 +13,10 @@ praisonaiagents/build/lib/praisonaiagents/agents/agents.py,sha256=P2FAtlfD3kPib5
13
13
  praisonaiagents/build/lib/praisonaiagents/task/__init__.py,sha256=VL5hXVmyGjINb34AalxpBMl-YW9m5EDcRkMTKkSSl7c,80
14
14
  praisonaiagents/build/lib/praisonaiagents/task/task.py,sha256=4Y1qX8OeEFcid2yhAiPYylvHpuDmWORsyNL16_BiVvI,1831
15
15
  praisonaiagents/process/__init__.py,sha256=lkYbL7Hn5a0ldvJtkdH23vfIIZLIcanK-65C0MwaorY,52
16
- praisonaiagents/process/process.py,sha256=HN_xNeG02T5QwcvcuSw8OiABVYbKIYJ-J--3H8Hd89Q,11722
16
+ praisonaiagents/process/process.py,sha256=4qXdrCDQPH5MtvHvdJVURXKNgSl6ae3OYTiqAF_A2ZU,24295
17
17
  praisonaiagents/task/__init__.py,sha256=VL5hXVmyGjINb34AalxpBMl-YW9m5EDcRkMTKkSSl7c,80
18
- praisonaiagents/task/task.py,sha256=oMC5Zz1dMj0Ceice69aBS1KQQXMLqphc8wNOQ9zcu0Q,2570
19
- praisonaiagents-0.0.16.dist-info/METADATA,sha256=Nb_ZUqzMHYl2CzLFiDFAclzAAx4G0LWN6IWADmYu0LY,233
20
- praisonaiagents-0.0.16.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
21
- praisonaiagents-0.0.16.dist-info/top_level.txt,sha256=_HsRddrJ23iDx5TTqVUVvXG2HeHBL5voshncAMDGjtA,16
22
- praisonaiagents-0.0.16.dist-info/RECORD,,
18
+ praisonaiagents/task/task.py,sha256=UiiWgLDOdX_w0opP8h8-u-leVZlq1CkpGUmf7L2qyJs,3110
19
+ praisonaiagents-0.0.18.dist-info/METADATA,sha256=JtO_vQUfxUb2EM2x_PSt1HqTf3biR65fyz4tg0_PF0Q,233
20
+ praisonaiagents-0.0.18.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
21
+ praisonaiagents-0.0.18.dist-info/top_level.txt,sha256=_HsRddrJ23iDx5TTqVUVvXG2HeHBL5voshncAMDGjtA,16
22
+ praisonaiagents-0.0.18.dist-info/RECORD,,