praisonaiagents 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- {agents → praisonaiagents}/__init__.py +2 -2
- praisonaiagents/agents/__init__.py +4 -0
- praisonaiagents/agents/agents.py +318 -0
- praisonaiagents/build/lib/praisonaiagents/__init__.py +1 -0
- praisonaiagents/build/lib/praisonaiagents/agent/__init__.py +4 -0
- praisonaiagents/build/lib/praisonaiagents/agent/agent.py +350 -0
- praisonaiagents/main.py +112 -0
- praisonaiagents/task/__init__.py +4 -0
- praisonaiagents/task/task.py +48 -0
- {praisonaiagents-0.0.2.dist-info → praisonaiagents-0.0.4.dist-info}/METADATA +2 -2
- praisonaiagents-0.0.4.dist-info/RECORD +20 -0
- praisonaiagents-0.0.4.dist-info/top_level.txt +1 -0
- praisonaiagents-0.0.2.dist-info/RECORD +0 -12
- praisonaiagents-0.0.2.dist-info/top_level.txt +0 -1
- {agents → praisonaiagents}/agent/__init__.py +0 -0
- {agents → praisonaiagents}/agent/agent.py +0 -0
- {agents → praisonaiagents/build/lib/praisonaiagents}/agents/__init__.py +0 -0
- {agents → praisonaiagents/build/lib/praisonaiagents}/agents/agents.py +0 -0
- {agents → praisonaiagents/build/lib/praisonaiagents}/main.py +0 -0
- {agents → praisonaiagents/build/lib/praisonaiagents}/task/__init__.py +0 -0
- {agents → praisonaiagents/build/lib/praisonaiagents}/task/task.py +0 -0
- {praisonaiagents-0.0.2.dist-info → praisonaiagents-0.0.4.dist-info}/WHEEL +0 -0
@@ -3,7 +3,7 @@ Praison AI Agents - A package for hierarchical AI agent task execution
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from .agent.agent import Agent
|
6
|
-
from .agents.agents import
|
6
|
+
from .agents.agents import PraisonAIAgents
|
7
7
|
from .task.task import Task
|
8
8
|
from .main import (
|
9
9
|
TaskOutput,
|
@@ -20,7 +20,7 @@ from .main import (
|
|
20
20
|
|
21
21
|
__all__ = [
|
22
22
|
'Agent',
|
23
|
-
'
|
23
|
+
'PraisonAIAgents',
|
24
24
|
'Task',
|
25
25
|
'TaskOutput',
|
26
26
|
'ReflectionOutput',
|
@@ -0,0 +1,318 @@
|
|
1
|
+
import os
|
2
|
+
import time
|
3
|
+
import json
|
4
|
+
import logging
|
5
|
+
from typing import Any, Dict, Optional
|
6
|
+
from pydantic import BaseModel
|
7
|
+
from rich.text import Text
|
8
|
+
from rich.panel import Panel
|
9
|
+
from rich.console import Console
|
10
|
+
from ..main import display_error, TaskOutput, error_logs, client
|
11
|
+
from ..agent.agent import Agent
|
12
|
+
from ..task.task import Task
|
13
|
+
|
14
|
+
class PraisonAIAgents:
|
15
|
+
def __init__(self, agents, tasks, verbose=0, completion_checker=None, max_retries=5, process="sequential", manager_llm=None):
|
16
|
+
self.agents = agents
|
17
|
+
self.tasks = {}
|
18
|
+
if max_retries < 3:
|
19
|
+
max_retries = 3
|
20
|
+
self.completion_checker = completion_checker if completion_checker else self.default_completion_checker
|
21
|
+
self.task_id_counter = 0
|
22
|
+
self.verbose = verbose
|
23
|
+
self.max_retries = max_retries
|
24
|
+
self.process = process
|
25
|
+
self.manager_llm = manager_llm
|
26
|
+
for task in tasks:
|
27
|
+
self.add_task(task)
|
28
|
+
task.status = "not started"
|
29
|
+
|
30
|
+
def add_task(self, task):
|
31
|
+
task_id = self.task_id_counter
|
32
|
+
task.id = task_id
|
33
|
+
self.tasks[task_id] = task
|
34
|
+
self.task_id_counter += 1
|
35
|
+
return task_id
|
36
|
+
|
37
|
+
def clean_json_output(self, output: str) -> str:
|
38
|
+
cleaned = output.strip()
|
39
|
+
if cleaned.startswith("```json"):
|
40
|
+
cleaned = cleaned[len("```json"):].strip()
|
41
|
+
if cleaned.startswith("```"):
|
42
|
+
cleaned = cleaned[len("```"):].strip()
|
43
|
+
if cleaned.endswith("```"):
|
44
|
+
cleaned = cleaned[:-3].strip()
|
45
|
+
return cleaned
|
46
|
+
|
47
|
+
def default_completion_checker(self, task, agent_output):
|
48
|
+
if task.output_json and task.result and task.result.json_dict:
|
49
|
+
return True
|
50
|
+
if task.output_pydantic and task.result and task.result.pydantic:
|
51
|
+
return True
|
52
|
+
return len(agent_output.strip()) > 0
|
53
|
+
|
54
|
+
def execute_task(self, task_id):
|
55
|
+
if task_id not in self.tasks:
|
56
|
+
display_error(f"Error: Task with ID {task_id} does not exist")
|
57
|
+
return
|
58
|
+
task = self.tasks[task_id]
|
59
|
+
if task.status == "not started":
|
60
|
+
task.status = "in progress"
|
61
|
+
|
62
|
+
executor_agent = task.agent
|
63
|
+
|
64
|
+
task_prompt = f"""
|
65
|
+
You need to do the following task: {task.description}.
|
66
|
+
Expected Output: {task.expected_output}.
|
67
|
+
"""
|
68
|
+
if task.context:
|
69
|
+
context_results = ""
|
70
|
+
for context_task in task.context:
|
71
|
+
if context_task.result:
|
72
|
+
context_results += f"Result of previous task {context_task.name if context_task.name else context_task.description}: {context_task.result.raw}\n"
|
73
|
+
else:
|
74
|
+
context_results += f"Previous task {context_task.name if context_task.name else context_task.description} had no result.\n"
|
75
|
+
task_prompt += f"""
|
76
|
+
Here are the results of previous tasks that might be useful:\n
|
77
|
+
{context_results}
|
78
|
+
"""
|
79
|
+
task_prompt += "Please provide only the final result of your work. Do not add any conversation or extra explanation."
|
80
|
+
|
81
|
+
if self.verbose >= 2:
|
82
|
+
logging.info(f"Executing task {task_id}: {task.description} using {executor_agent.name}")
|
83
|
+
logging.debug(f"Starting execution of task {task_id} with prompt:\n{task_prompt}")
|
84
|
+
agent_output = executor_agent.chat(task_prompt, tools=task.tools)
|
85
|
+
if agent_output:
|
86
|
+
task_output = TaskOutput(
|
87
|
+
description=task.description,
|
88
|
+
summary=task.description[:10],
|
89
|
+
raw=agent_output,
|
90
|
+
agent=executor_agent.name,
|
91
|
+
output_format="RAW"
|
92
|
+
)
|
93
|
+
|
94
|
+
if task.output_json:
|
95
|
+
cleaned = self.clean_json_output(agent_output)
|
96
|
+
try:
|
97
|
+
parsed = json.loads(cleaned)
|
98
|
+
task_output.json_dict = parsed
|
99
|
+
task_output.output_format = "JSON"
|
100
|
+
except:
|
101
|
+
logging.warning(f"Warning: Could not parse output of task {task_id} as JSON")
|
102
|
+
logging.debug(f"Output that failed JSON parsing: {agent_output}")
|
103
|
+
|
104
|
+
if task.output_pydantic:
|
105
|
+
cleaned = self.clean_json_output(agent_output)
|
106
|
+
try:
|
107
|
+
parsed = json.loads(cleaned)
|
108
|
+
pyd_obj = task.output_pydantic(**parsed)
|
109
|
+
task_output.pydantic = pyd_obj
|
110
|
+
task_output.output_format = "Pydantic"
|
111
|
+
except:
|
112
|
+
logging.warning(f"Warning: Could not parse output of task {task_id} as Pydantic Model")
|
113
|
+
logging.debug(f"Output that failed Pydantic parsing: {agent_output}")
|
114
|
+
|
115
|
+
task.result = task_output
|
116
|
+
return task_output
|
117
|
+
else:
|
118
|
+
task.status = "failed"
|
119
|
+
return None
|
120
|
+
|
121
|
+
def save_output_to_file(self, task, task_output):
|
122
|
+
if task.output_file:
|
123
|
+
try:
|
124
|
+
if task.create_directory:
|
125
|
+
os.makedirs(os.path.dirname(task.output_file), exist_ok=True)
|
126
|
+
with open(task.output_file, "w") as f:
|
127
|
+
f.write(str(task_output))
|
128
|
+
if self.verbose >= 1:
|
129
|
+
logging.info(f"Task output saved to {task.output_file}")
|
130
|
+
except Exception as e:
|
131
|
+
display_error(f"Error saving task output to file: {e}")
|
132
|
+
|
133
|
+
def run_task(self, task_id):
|
134
|
+
if task_id not in self.tasks:
|
135
|
+
display_error(f"Error: Task with ID {task_id} does not exist")
|
136
|
+
return
|
137
|
+
task = self.tasks[task_id]
|
138
|
+
if task.status == "completed":
|
139
|
+
logging.info(f"Task with ID {task_id} is already completed")
|
140
|
+
return
|
141
|
+
|
142
|
+
retries = 0
|
143
|
+
while task.status != "completed" and retries < self.max_retries:
|
144
|
+
logging.debug(f"Attempt {retries+1} for task {task_id}")
|
145
|
+
if task.status in ["not started", "in progress"]:
|
146
|
+
task_output = self.execute_task(task_id)
|
147
|
+
if task_output and self.completion_checker(task, task_output.raw):
|
148
|
+
task.status = "completed"
|
149
|
+
if task.callback:
|
150
|
+
task.callback(task_output)
|
151
|
+
self.save_output_to_file(task, task_output)
|
152
|
+
if self.verbose >= 1:
|
153
|
+
logging.info(f"Task {task_id} completed successfully.")
|
154
|
+
else:
|
155
|
+
task.status = "in progress"
|
156
|
+
if self.verbose >= 1:
|
157
|
+
logging.info(f"Task {task_id} not completed, retrying")
|
158
|
+
time.sleep(1)
|
159
|
+
retries += 1
|
160
|
+
else:
|
161
|
+
if task.status == "failed":
|
162
|
+
logging.info("Task is failed, resetting to in-progress for another try...")
|
163
|
+
task.status = "in progress"
|
164
|
+
else:
|
165
|
+
logging.info("Invalid Task status")
|
166
|
+
break
|
167
|
+
|
168
|
+
if retries == self.max_retries and task.status != "completed":
|
169
|
+
logging.info(f"Task {task_id} failed after {self.max_retries} retries.")
|
170
|
+
|
171
|
+
def run_all_tasks(self):
|
172
|
+
if self.process == "sequential":
|
173
|
+
for task_id in self.tasks:
|
174
|
+
if self.tasks[task_id].status != "completed":
|
175
|
+
self.run_task(task_id)
|
176
|
+
elif self.process == "hierarchical":
|
177
|
+
logging.debug(f"Starting hierarchical task execution with {len(self.tasks)} tasks")
|
178
|
+
manager_agent = Agent(
|
179
|
+
name="Manager",
|
180
|
+
role="Project manager",
|
181
|
+
goal="Manage the entire flow of tasks and delegate them to the right agent",
|
182
|
+
backstory="Expert project manager to coordinate tasks among agents",
|
183
|
+
llm=self.manager_llm,
|
184
|
+
verbose=self.verbose,
|
185
|
+
markdown=True,
|
186
|
+
self_reflect=False
|
187
|
+
)
|
188
|
+
|
189
|
+
class ManagerInstructions(BaseModel):
|
190
|
+
task_id: int
|
191
|
+
agent_name: str
|
192
|
+
action: str
|
193
|
+
|
194
|
+
manager_task = Task(
|
195
|
+
name="manager_task",
|
196
|
+
description="Decide the order of tasks and which agent executes them",
|
197
|
+
expected_output="All tasks completed successfully",
|
198
|
+
agent=manager_agent
|
199
|
+
)
|
200
|
+
manager_task_id = self.add_task(manager_task)
|
201
|
+
logging.info(f"Created manager task with ID {manager_task_id}")
|
202
|
+
|
203
|
+
completed_count = 0
|
204
|
+
total_tasks = len(self.tasks) - 1
|
205
|
+
logging.info(f"Need to complete {total_tasks} tasks (excluding manager task)")
|
206
|
+
|
207
|
+
while completed_count < total_tasks:
|
208
|
+
tasks_summary = []
|
209
|
+
for tid, tk in self.tasks.items():
|
210
|
+
if tk.name == "manager_task":
|
211
|
+
continue
|
212
|
+
task_info = {
|
213
|
+
"task_id": tid,
|
214
|
+
"name": tk.name,
|
215
|
+
"description": tk.description,
|
216
|
+
"status": tk.status if tk.status else "not started",
|
217
|
+
"agent": tk.agent.name if tk.agent else "No agent"
|
218
|
+
}
|
219
|
+
tasks_summary.append(task_info)
|
220
|
+
logging.info(f"Task {tid} status: {task_info}")
|
221
|
+
|
222
|
+
manager_prompt = f"""
|
223
|
+
Here is the current status of all tasks except yours (manager_task):
|
224
|
+
{tasks_summary}
|
225
|
+
|
226
|
+
Provide a JSON with the structure:
|
227
|
+
{{
|
228
|
+
"task_id": <int>,
|
229
|
+
"agent_name": "<string>",
|
230
|
+
"action": "<execute or stop>"
|
231
|
+
}}
|
232
|
+
"""
|
233
|
+
|
234
|
+
try:
|
235
|
+
logging.info("Requesting manager instructions...")
|
236
|
+
manager_response = client.beta.chat.completions.parse(
|
237
|
+
model=self.manager_llm,
|
238
|
+
messages=[
|
239
|
+
{"role": "system", "content": manager_task.description},
|
240
|
+
{"role": "user", "content": manager_prompt}
|
241
|
+
],
|
242
|
+
temperature=0.7,
|
243
|
+
response_format=ManagerInstructions
|
244
|
+
)
|
245
|
+
parsed_instructions = manager_response.choices[0].message.parsed
|
246
|
+
logging.info(f"Manager instructions: {parsed_instructions}")
|
247
|
+
except Exception as e:
|
248
|
+
display_error(f"Manager parse error: {e}")
|
249
|
+
logging.error(f"Manager parse error: {str(e)}", exc_info=True)
|
250
|
+
break
|
251
|
+
|
252
|
+
selected_task_id = parsed_instructions.task_id
|
253
|
+
selected_agent_name = parsed_instructions.agent_name
|
254
|
+
action = parsed_instructions.action
|
255
|
+
|
256
|
+
logging.info(f"Manager selected task_id={selected_task_id}, agent={selected_agent_name}, action={action}")
|
257
|
+
|
258
|
+
if action.lower() == "stop":
|
259
|
+
logging.info("Manager decided to stop task execution")
|
260
|
+
break
|
261
|
+
|
262
|
+
if selected_task_id not in self.tasks:
|
263
|
+
error_msg = f"Manager selected invalid task id {selected_task_id}"
|
264
|
+
display_error(error_msg)
|
265
|
+
logging.error(error_msg)
|
266
|
+
break
|
267
|
+
|
268
|
+
original_agent = self.tasks[selected_task_id].agent.name if self.tasks[selected_task_id].agent else "None"
|
269
|
+
for a in self.agents:
|
270
|
+
if a.name == selected_agent_name:
|
271
|
+
self.tasks[selected_task_id].agent = a
|
272
|
+
logging.info(f"Changed agent for task {selected_task_id} from {original_agent} to {selected_agent_name}")
|
273
|
+
break
|
274
|
+
|
275
|
+
if self.tasks[selected_task_id].status != "completed":
|
276
|
+
logging.info(f"Starting execution of task {selected_task_id}")
|
277
|
+
self.run_task(selected_task_id)
|
278
|
+
logging.info(f"Finished execution of task {selected_task_id}, status: {self.tasks[selected_task_id].status}")
|
279
|
+
|
280
|
+
if self.tasks[selected_task_id].status == "completed":
|
281
|
+
completed_count += 1
|
282
|
+
logging.info(f"Task {selected_task_id} completed. Total completed: {completed_count}/{total_tasks}")
|
283
|
+
|
284
|
+
self.tasks[manager_task.id].status = "completed"
|
285
|
+
if self.verbose >= 1:
|
286
|
+
logging.info("All tasks completed under manager supervision.")
|
287
|
+
logging.info("Hierarchical task execution finished")
|
288
|
+
|
289
|
+
def get_task_status(self, task_id):
|
290
|
+
if task_id in self.tasks:
|
291
|
+
return self.tasks[task_id].status
|
292
|
+
return None
|
293
|
+
|
294
|
+
def get_all_tasks_status(self):
|
295
|
+
return {task_id: self.tasks[task_id].status for task_id in self.tasks}
|
296
|
+
|
297
|
+
def get_task_result(self, task_id):
|
298
|
+
if task_id in self.tasks:
|
299
|
+
return self.tasks[task_id].result
|
300
|
+
return None
|
301
|
+
|
302
|
+
def get_task_details(self, task_id):
|
303
|
+
if task_id in self.tasks:
|
304
|
+
return str(self.tasks[task_id])
|
305
|
+
return None
|
306
|
+
|
307
|
+
def get_agent_details(self, agent_name):
|
308
|
+
agent = [task.agent for task in self.tasks.values() if task.agent and task.agent.name == agent_name]
|
309
|
+
if agent:
|
310
|
+
return str(agent[0])
|
311
|
+
return None
|
312
|
+
|
313
|
+
def start(self):
|
314
|
+
self.run_all_tasks()
|
315
|
+
return {
|
316
|
+
"task_status": self.get_all_tasks_status(),
|
317
|
+
"task_results": {task_id: self.get_task_result(task_id) for task_id in self.tasks}
|
318
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,350 @@
|
|
1
|
+
import logging
|
2
|
+
import json
|
3
|
+
import time
|
4
|
+
from typing import List, Optional, Any, Dict, Union, Literal
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.live import Live
|
7
|
+
from ..main import (
|
8
|
+
display_error,
|
9
|
+
display_tool_call,
|
10
|
+
display_instruction,
|
11
|
+
display_interaction,
|
12
|
+
display_generating,
|
13
|
+
ReflectionOutput,
|
14
|
+
client,
|
15
|
+
error_logs
|
16
|
+
)
|
17
|
+
|
18
|
+
class Agent:
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
name: str,
|
22
|
+
role: str,
|
23
|
+
goal: str,
|
24
|
+
backstory: str,
|
25
|
+
llm: Optional[Union[str, Any]] = "gpt-4o-mini",
|
26
|
+
tools: Optional[List[Any]] = None,
|
27
|
+
function_calling_llm: Optional[Any] = None,
|
28
|
+
max_iter: int = 20,
|
29
|
+
max_rpm: Optional[int] = None,
|
30
|
+
max_execution_time: Optional[int] = None,
|
31
|
+
memory: bool = True,
|
32
|
+
verbose: bool = False,
|
33
|
+
allow_delegation: bool = False,
|
34
|
+
step_callback: Optional[Any] = None,
|
35
|
+
cache: bool = True,
|
36
|
+
system_template: Optional[str] = None,
|
37
|
+
prompt_template: Optional[str] = None,
|
38
|
+
response_template: Optional[str] = None,
|
39
|
+
allow_code_execution: Optional[bool] = False,
|
40
|
+
max_retry_limit: int = 2,
|
41
|
+
respect_context_window: bool = True,
|
42
|
+
code_execution_mode: Literal["safe", "unsafe"] = "safe",
|
43
|
+
embedder_config: Optional[Dict[str, Any]] = None,
|
44
|
+
knowledge_sources: Optional[List[Any]] = None,
|
45
|
+
use_system_prompt: Optional[bool] = True,
|
46
|
+
markdown: bool = True,
|
47
|
+
self_reflect: bool = True,
|
48
|
+
max_reflection_iter: int = 3
|
49
|
+
):
|
50
|
+
self.name = name
|
51
|
+
self.role = role
|
52
|
+
self.goal = goal
|
53
|
+
self.backstory = backstory
|
54
|
+
self.llm = llm
|
55
|
+
self.tools = tools if tools else []
|
56
|
+
self.function_calling_llm = function_calling_llm
|
57
|
+
self.max_iter = max_iter
|
58
|
+
self.max_rpm = max_rpm
|
59
|
+
self.max_execution_time = max_execution_time
|
60
|
+
self.memory = memory
|
61
|
+
self.verbose = verbose
|
62
|
+
self.allow_delegation = allow_delegation
|
63
|
+
self.step_callback = step_callback
|
64
|
+
self.cache = cache
|
65
|
+
self.system_template = system_template
|
66
|
+
self.prompt_template = prompt_template
|
67
|
+
self.response_template = response_template
|
68
|
+
self.allow_code_execution = allow_code_execution
|
69
|
+
self.max_retry_limit = max_retry_limit
|
70
|
+
self.respect_context_window = respect_context_window
|
71
|
+
self.code_execution_mode = code_execution_mode
|
72
|
+
self.embedder_config = embedder_config
|
73
|
+
self.knowledge_sources = knowledge_sources
|
74
|
+
self.use_system_prompt = use_system_prompt
|
75
|
+
self.chat_history = []
|
76
|
+
self.markdown = markdown
|
77
|
+
self.self_reflect = self_reflect
|
78
|
+
self.max_reflection_iter = max_reflection_iter
|
79
|
+
|
80
|
+
def execute_tool(self, function_name, arguments):
|
81
|
+
logging.debug(f"{self.name} executing tool {function_name} with arguments: {arguments}")
|
82
|
+
if function_name == "get_weather":
|
83
|
+
location = arguments.get("location", "Unknown Location")
|
84
|
+
return {"temperature": "25C", "condition": "Sunny", "location": location}
|
85
|
+
elif function_name == "search_tool":
|
86
|
+
query = arguments.get("query", "AI trends in 2024")
|
87
|
+
return {"results": [
|
88
|
+
{"title": "AI advancements in 2024", "link": "url1", "summary": "Lots of advancements"},
|
89
|
+
{"title": "New trends in AI", "link": "url2", "summary": "New trends being found"}
|
90
|
+
]}
|
91
|
+
else:
|
92
|
+
return f"Tool '{function_name}' is not recognized"
|
93
|
+
|
94
|
+
def clear_history(self):
|
95
|
+
self.chat_history = []
|
96
|
+
|
97
|
+
def __str__(self):
|
98
|
+
return f"Agent(name='{self.name}', role='{self.role}', goal='{self.goal}')"
|
99
|
+
|
100
|
+
def _chat_completion(self, messages, temperature=0.2, tools=None, stream=True):
|
101
|
+
console = Console()
|
102
|
+
start_time = time.time()
|
103
|
+
logging.debug(f"{self.name} sending messages to LLM: {messages}")
|
104
|
+
|
105
|
+
formatted_tools = []
|
106
|
+
if tools:
|
107
|
+
for tool in tools:
|
108
|
+
if isinstance(tool, dict):
|
109
|
+
formatted_tools.append(tool)
|
110
|
+
elif hasattr(tool, "to_openai_tool"):
|
111
|
+
formatted_tools.append(tool.to_openai_tool())
|
112
|
+
elif isinstance(tool, str):
|
113
|
+
formatted_tools.append({
|
114
|
+
"type": "function",
|
115
|
+
"function": {
|
116
|
+
"name": tool,
|
117
|
+
"description": f"This is a tool called {tool}",
|
118
|
+
"parameters": {
|
119
|
+
"type": "object",
|
120
|
+
"properties": {},
|
121
|
+
},
|
122
|
+
}
|
123
|
+
})
|
124
|
+
else:
|
125
|
+
display_error(f"Warning: Tool {tool} not recognized")
|
126
|
+
|
127
|
+
try:
|
128
|
+
initial_response = client.chat.completions.create(
|
129
|
+
model=self.llm,
|
130
|
+
messages=messages,
|
131
|
+
temperature=temperature,
|
132
|
+
tools=formatted_tools if formatted_tools else None,
|
133
|
+
stream=False
|
134
|
+
)
|
135
|
+
|
136
|
+
tool_calls = getattr(initial_response.choices[0].message, 'tool_calls', None)
|
137
|
+
|
138
|
+
if tool_calls:
|
139
|
+
messages.append({
|
140
|
+
"role": "assistant",
|
141
|
+
"content": initial_response.choices[0].message.content,
|
142
|
+
"tool_calls": tool_calls
|
143
|
+
})
|
144
|
+
|
145
|
+
for tool_call in tool_calls:
|
146
|
+
function_name = tool_call.function.name
|
147
|
+
arguments = json.loads(tool_call.function.arguments)
|
148
|
+
|
149
|
+
if self.verbose:
|
150
|
+
display_tool_call(f"Agent {self.name} is calling function '{function_name}' with arguments: {arguments}")
|
151
|
+
|
152
|
+
tool_result = self.execute_tool(function_name, arguments)
|
153
|
+
results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
|
154
|
+
|
155
|
+
if self.verbose:
|
156
|
+
display_tool_call(f"Function '{function_name}' returned: {results_str}")
|
157
|
+
|
158
|
+
messages.append({
|
159
|
+
"role": "tool",
|
160
|
+
"tool_call_id": tool_call.id,
|
161
|
+
"content": results_str
|
162
|
+
})
|
163
|
+
|
164
|
+
if stream:
|
165
|
+
response_stream = client.chat.completions.create(
|
166
|
+
model=self.llm,
|
167
|
+
messages=messages,
|
168
|
+
temperature=temperature,
|
169
|
+
stream=True
|
170
|
+
)
|
171
|
+
full_response_text = ""
|
172
|
+
with Live(display_generating("", start_time), refresh_per_second=4) as live:
|
173
|
+
for chunk in response_stream:
|
174
|
+
if chunk.choices[0].delta.content:
|
175
|
+
full_response_text += chunk.choices[0].delta.content
|
176
|
+
live.update(display_generating(full_response_text, start_time))
|
177
|
+
|
178
|
+
final_response = client.chat.completions.create(
|
179
|
+
model=self.llm,
|
180
|
+
messages=messages,
|
181
|
+
temperature=temperature,
|
182
|
+
stream=False
|
183
|
+
)
|
184
|
+
return final_response
|
185
|
+
else:
|
186
|
+
if tool_calls:
|
187
|
+
final_response = client.chat.completions.create(
|
188
|
+
model=self.llm,
|
189
|
+
messages=messages,
|
190
|
+
temperature=temperature,
|
191
|
+
stream=False
|
192
|
+
)
|
193
|
+
return final_response
|
194
|
+
else:
|
195
|
+
return initial_response
|
196
|
+
|
197
|
+
except Exception as e:
|
198
|
+
display_error(f"Error in chat completion: {e}")
|
199
|
+
return None
|
200
|
+
|
201
|
+
def chat(self, prompt, temperature=0.2, tools=None, output_json=None):
|
202
|
+
if self.use_system_prompt:
|
203
|
+
system_prompt = f"""{self.backstory}\n
|
204
|
+
Your Role: {self.role}\n
|
205
|
+
Your Goal: {self.goal}
|
206
|
+
"""
|
207
|
+
else:
|
208
|
+
system_prompt = None
|
209
|
+
|
210
|
+
messages = []
|
211
|
+
if system_prompt:
|
212
|
+
messages.append({"role": "system", "content": system_prompt})
|
213
|
+
messages.extend(self.chat_history)
|
214
|
+
messages.append({"role": "user", "content": prompt})
|
215
|
+
|
216
|
+
final_response_text = None
|
217
|
+
reflection_count = 0
|
218
|
+
start_time = time.time()
|
219
|
+
|
220
|
+
while True:
|
221
|
+
try:
|
222
|
+
if self.verbose:
|
223
|
+
display_instruction(f"Agent {self.name} is processing prompt: {prompt}")
|
224
|
+
|
225
|
+
formatted_tools = []
|
226
|
+
if tools:
|
227
|
+
for tool in tools:
|
228
|
+
if isinstance(tool, dict):
|
229
|
+
formatted_tools.append(tool)
|
230
|
+
elif hasattr(tool, "to_openai_tool"):
|
231
|
+
formatted_tools.append(tool.to_openai_tool())
|
232
|
+
elif isinstance(tool, str):
|
233
|
+
formatted_tools.append({
|
234
|
+
"type": "function",
|
235
|
+
"function": {
|
236
|
+
"name": tool,
|
237
|
+
"description": f"This is a tool called {tool}",
|
238
|
+
"parameters": {
|
239
|
+
"type": "object",
|
240
|
+
"properties": {},
|
241
|
+
},
|
242
|
+
}
|
243
|
+
})
|
244
|
+
else:
|
245
|
+
display_error(f"Warning: Tool {tool} not recognized")
|
246
|
+
|
247
|
+
response = self._chat_completion(messages, temperature=temperature, tools=formatted_tools if formatted_tools else None)
|
248
|
+
if not response:
|
249
|
+
return None
|
250
|
+
|
251
|
+
tool_calls = getattr(response.choices[0].message, 'tool_calls', None)
|
252
|
+
|
253
|
+
if tool_calls:
|
254
|
+
messages.append({
|
255
|
+
"role": "assistant",
|
256
|
+
"content": response.choices[0].message.content,
|
257
|
+
"tool_calls": tool_calls
|
258
|
+
})
|
259
|
+
|
260
|
+
for tool_call in tool_calls:
|
261
|
+
function_name = tool_call.function.name
|
262
|
+
arguments = json.loads(tool_call.function.arguments)
|
263
|
+
|
264
|
+
if self.verbose:
|
265
|
+
display_tool_call(f"Agent {self.name} is calling function '{function_name}' with arguments: {arguments}")
|
266
|
+
|
267
|
+
tool_result = self.execute_tool(function_name, arguments)
|
268
|
+
|
269
|
+
if tool_result:
|
270
|
+
if self.verbose:
|
271
|
+
display_tool_call(f"Function '{function_name}' returned: {tool_result}")
|
272
|
+
messages.append({
|
273
|
+
"role": "tool",
|
274
|
+
"tool_call_id": tool_call.id,
|
275
|
+
"content": json.dumps(tool_result)
|
276
|
+
})
|
277
|
+
else:
|
278
|
+
messages.append({
|
279
|
+
"role": "tool",
|
280
|
+
"tool_call_id": tool_call.id,
|
281
|
+
"content": "Function returned an empty output"
|
282
|
+
})
|
283
|
+
|
284
|
+
response = self._chat_completion(messages, temperature=temperature)
|
285
|
+
if not response:
|
286
|
+
return None
|
287
|
+
response_text = response.choices[0].message.content.strip()
|
288
|
+
else:
|
289
|
+
response_text = response.choices[0].message.content.strip()
|
290
|
+
|
291
|
+
if not self.self_reflect:
|
292
|
+
self.chat_history.append({"role": "user", "content": prompt})
|
293
|
+
self.chat_history.append({"role": "assistant", "content": response_text})
|
294
|
+
if self.verbose:
|
295
|
+
logging.info(f"Agent {self.name} final response: {response_text}")
|
296
|
+
display_interaction(prompt, response_text, markdown=self.markdown, generation_time=time.time() - start_time)
|
297
|
+
return response_text
|
298
|
+
|
299
|
+
reflection_prompt = f"""
|
300
|
+
Reflect on your previous response: '{response_text}'.
|
301
|
+
Identify any flaws, improvements, or actions.
|
302
|
+
Provide a "satisfactory" status ('yes' or 'no').
|
303
|
+
Output MUST be JSON with 'reflection' and 'satisfactory'.
|
304
|
+
"""
|
305
|
+
logging.debug(f"{self.name} reflection attempt {reflection_count+1}, sending prompt: {reflection_prompt}")
|
306
|
+
messages.append({"role": "user", "content": reflection_prompt})
|
307
|
+
|
308
|
+
try:
|
309
|
+
reflection_response = client.beta.chat.completions.parse(
|
310
|
+
model=self.llm,
|
311
|
+
messages=messages,
|
312
|
+
temperature=temperature,
|
313
|
+
response_format=ReflectionOutput
|
314
|
+
)
|
315
|
+
|
316
|
+
reflection_output = reflection_response.choices[0].message.parsed
|
317
|
+
|
318
|
+
if self.verbose:
|
319
|
+
display_self_reflection(f"Agent {self.name} self reflection: reflection='{reflection_output.reflection}' satisfactory='{reflection_output.satisfactory}'")
|
320
|
+
|
321
|
+
messages.append({"role": "assistant", "content": f"Self Reflection: {reflection_output.reflection} Satisfactory?: {reflection_output.satisfactory}"})
|
322
|
+
|
323
|
+
if reflection_output.satisfactory == "yes":
|
324
|
+
if self.verbose:
|
325
|
+
display_self_reflection("Agent marked the response as satisfactory")
|
326
|
+
self.chat_history.append({"role": "assistant", "content": response_text})
|
327
|
+
display_interaction(prompt, response_text, markdown=self.markdown, generation_time=time.time() - start_time)
|
328
|
+
return response_text
|
329
|
+
|
330
|
+
logging.debug(f"{self.name} reflection not satisfactory, requesting regeneration.")
|
331
|
+
messages.append({"role": "user", "content": "Now regenerate your response using the reflection you made"})
|
332
|
+
response = self._chat_completion(messages, temperature=temperature, tools=None, stream=True)
|
333
|
+
response_text = response.choices[0].message.content.strip()
|
334
|
+
except Exception as e:
|
335
|
+
display_error(f"Error in parsing self-reflection json {e}. Retrying")
|
336
|
+
logging.error("Reflection parsing failed.", exc_info=True)
|
337
|
+
messages.append({"role": "assistant", "content": f"Self Reflection failed."})
|
338
|
+
|
339
|
+
reflection_count += 1
|
340
|
+
|
341
|
+
self.chat_history.append({"role": "user", "content": prompt})
|
342
|
+
self.chat_history.append({"role": "assistant", "content": response_text})
|
343
|
+
|
344
|
+
if self.verbose:
|
345
|
+
logging.info(f"Agent {self.name} final response: {response_text}")
|
346
|
+
display_interaction(prompt, response_text, markdown=self.markdown, generation_time=time.time() - start_time)
|
347
|
+
return response_text
|
348
|
+
except Exception as e:
|
349
|
+
display_error(f"Error in chat: {e}")
|
350
|
+
return None
|
praisonaiagents/main.py
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
import os
|
2
|
+
import time
|
3
|
+
import json
|
4
|
+
import logging
|
5
|
+
from typing import List, Optional, Dict, Any, Union, Literal, Type
|
6
|
+
from openai import OpenAI
|
7
|
+
from pydantic import BaseModel
|
8
|
+
from rich import print
|
9
|
+
from rich.console import Console
|
10
|
+
from rich.panel import Panel
|
11
|
+
from rich.text import Text
|
12
|
+
from rich.markdown import Markdown
|
13
|
+
from rich.logging import RichHandler
|
14
|
+
from rich.live import Live
|
15
|
+
|
16
|
+
LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO').upper()
|
17
|
+
|
18
|
+
logging.basicConfig(
|
19
|
+
level=getattr(logging, LOGLEVEL, logging.INFO),
|
20
|
+
format="%(asctime)s %(filename)s:%(lineno)d %(levelname)s %(message)s",
|
21
|
+
datefmt="[%X]",
|
22
|
+
handlers=[RichHandler(rich_tracebacks=True)]
|
23
|
+
)
|
24
|
+
|
25
|
+
# Global list to store error logs
|
26
|
+
error_logs = []
|
27
|
+
|
28
|
+
def display_interaction(message: str, response: str, markdown: bool = True, generation_time: Optional[float] = None):
|
29
|
+
console = Console()
|
30
|
+
if generation_time is not None:
|
31
|
+
console.print(Text(f"Response generated in {generation_time:.1f}s", style="dim"))
|
32
|
+
else:
|
33
|
+
console.print(Text("Response Generation Complete", style="dim"))
|
34
|
+
|
35
|
+
if markdown:
|
36
|
+
console.print(Panel.fit(Markdown(message), title="Message", border_style="cyan"))
|
37
|
+
console.print(Panel.fit(Markdown(response), title="Response", border_style="cyan"))
|
38
|
+
else:
|
39
|
+
console.print(Panel.fit(Text(message, style="bold green"), title="Message", border_style="cyan"))
|
40
|
+
console.print(Panel.fit(Text(response, style="bold white"), title="Response", border_style="cyan"))
|
41
|
+
|
42
|
+
def display_self_reflection(message: str):
|
43
|
+
console = Console()
|
44
|
+
console.print(Panel.fit(Text(message, style="bold yellow"), title="Self Reflection", border_style="magenta"))
|
45
|
+
|
46
|
+
def display_instruction(message: str):
|
47
|
+
console = Console()
|
48
|
+
console.print(Panel.fit(Text(message, style="bold blue"), title="Instruction", border_style="cyan"))
|
49
|
+
|
50
|
+
def display_tool_call(message: str):
|
51
|
+
console = Console()
|
52
|
+
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
|
53
|
+
|
54
|
+
def display_error(message: str):
|
55
|
+
console = Console()
|
56
|
+
console.print(Panel.fit(Text(message, style="bold red"), title="Error", border_style="red"))
|
57
|
+
# Store errors
|
58
|
+
error_logs.append(message)
|
59
|
+
|
60
|
+
def display_generating(content: str = "", start_time: Optional[float] = None):
|
61
|
+
elapsed_str = ""
|
62
|
+
if start_time is not None:
|
63
|
+
elapsed = time.time() - start_time
|
64
|
+
elapsed_str = f" {elapsed:.1f}s"
|
65
|
+
return Panel(Markdown(content), title=f"Generating...{elapsed_str}", border_style="green")
|
66
|
+
|
67
|
+
def clean_triple_backticks(text: str) -> str:
|
68
|
+
"""Remove triple backticks and surrounding json fences from a string."""
|
69
|
+
cleaned = text.strip()
|
70
|
+
if cleaned.startswith("```json"):
|
71
|
+
cleaned = cleaned[len("```json"):].strip()
|
72
|
+
if cleaned.startswith("```"):
|
73
|
+
cleaned = cleaned[len("```"):].strip()
|
74
|
+
if cleaned.endswith("```"):
|
75
|
+
cleaned = cleaned[:-3].strip()
|
76
|
+
return cleaned
|
77
|
+
|
78
|
+
class ReflectionOutput(BaseModel):
|
79
|
+
reflection: str
|
80
|
+
satisfactory: Literal["yes", "no"]
|
81
|
+
|
82
|
+
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
|
83
|
+
|
84
|
+
class TaskOutput(BaseModel):
|
85
|
+
description: str
|
86
|
+
summary: Optional[str] = None
|
87
|
+
raw: str
|
88
|
+
pydantic: Optional[BaseModel] = None
|
89
|
+
json_dict: Optional[Dict[str, Any]] = None
|
90
|
+
agent: str
|
91
|
+
output_format: Literal["RAW", "JSON", "Pydantic"] = "RAW"
|
92
|
+
|
93
|
+
def json(self) -> Optional[str]:
|
94
|
+
if self.output_format == "JSON" and self.json_dict:
|
95
|
+
return json.dumps(self.json_dict)
|
96
|
+
return None
|
97
|
+
|
98
|
+
def to_dict(self) -> dict:
|
99
|
+
output_dict = {}
|
100
|
+
if self.json_dict:
|
101
|
+
output_dict.update(self.json_dict)
|
102
|
+
if self.pydantic:
|
103
|
+
output_dict.update(self.pydantic.model_dump())
|
104
|
+
return output_dict
|
105
|
+
|
106
|
+
def __str__(self):
|
107
|
+
if self.pydantic:
|
108
|
+
return str(self.pydantic)
|
109
|
+
elif self.json_dict:
|
110
|
+
return json.dumps(self.json_dict)
|
111
|
+
else:
|
112
|
+
return self.raw
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import List, Optional, Dict, Any, Type
|
3
|
+
from pydantic import BaseModel
|
4
|
+
from ..main import TaskOutput
|
5
|
+
from ..agent.agent import Agent
|
6
|
+
|
7
|
+
class Task:
|
8
|
+
def __init__(
|
9
|
+
self,
|
10
|
+
description: str,
|
11
|
+
expected_output: str,
|
12
|
+
agent: Optional[Agent] = None,
|
13
|
+
name: Optional[str] = None,
|
14
|
+
tools: Optional[List[Any]] = None,
|
15
|
+
context: Optional[List["Task"]] = None,
|
16
|
+
async_execution: Optional[bool] = False,
|
17
|
+
config: Optional[Dict[str, Any]] = None,
|
18
|
+
output_file: Optional[str] = None,
|
19
|
+
output_json: Optional[Type[BaseModel]] = None,
|
20
|
+
output_pydantic: Optional[Type[BaseModel]] = None,
|
21
|
+
callback: Optional[Any] = None,
|
22
|
+
status: str = "not started",
|
23
|
+
result: Optional[TaskOutput] = None,
|
24
|
+
create_directory: Optional[bool] = False,
|
25
|
+
id: Optional[int] = None
|
26
|
+
):
|
27
|
+
self.description = description
|
28
|
+
self.expected_output = expected_output
|
29
|
+
self.name = name
|
30
|
+
self.agent = agent
|
31
|
+
self.tools = tools if tools else []
|
32
|
+
self.context = context if context else []
|
33
|
+
self.async_execution = async_execution
|
34
|
+
self.config = config if config else {}
|
35
|
+
self.output_file = output_file
|
36
|
+
self.output_json = output_json
|
37
|
+
self.output_pydantic = output_pydantic
|
38
|
+
self.callback = callback
|
39
|
+
self.status = status
|
40
|
+
self.result = result
|
41
|
+
self.create_directory = create_directory
|
42
|
+
self.id = id
|
43
|
+
|
44
|
+
if self.output_json and self.output_pydantic:
|
45
|
+
raise ValueError("Only one output type can be defined")
|
46
|
+
|
47
|
+
def __str__(self):
|
48
|
+
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}')"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: praisonaiagents
|
3
|
-
Version: 0.0.
|
4
|
-
Summary:
|
3
|
+
Version: 0.0.4
|
4
|
+
Summary: Praison AI agents for completing complex tasks with Self Reflection Agents
|
5
5
|
Author: Mervin Praison
|
6
6
|
Requires-Dist: pydantic
|
7
7
|
Requires-Dist: rich
|
@@ -0,0 +1,20 @@
|
|
1
|
+
praisonaiagents/__init__.py,sha256=gI8vEabBTRPsE_E8GA5sBMi4sTtJI-YokPrH2Nor-k0,741
|
2
|
+
praisonaiagents/main.py,sha256=zDhN5KKtKbfruolDNxlyJkcFlkSt4KQkQTDRfQVAhxc,3960
|
3
|
+
praisonaiagents/agent/__init__.py,sha256=sKO8wGEXvtCrvV1e834r1Okv0XAqAxqZCqz6hKLiTvA,79
|
4
|
+
praisonaiagents/agent/agent.py,sha256=CCCjv-qtr6hSB-BG7C8l3z-pXQpnTkX9bW6me36YiaU,15512
|
5
|
+
praisonaiagents/agents/__init__.py,sha256=7RDeQNSqZg5uBjD4M_0p_F6YgfWuDuxPFydPU50kDYc,120
|
6
|
+
praisonaiagents/agents/agents.py,sha256=kgzesSMIOHB_ig_qJmCY5jzllaIcuy56jsJnFvpEAyk,13482
|
7
|
+
praisonaiagents/build/lib/praisonaiagents/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
|
8
|
+
praisonaiagents/build/lib/praisonaiagents/main.py,sha256=zDhN5KKtKbfruolDNxlyJkcFlkSt4KQkQTDRfQVAhxc,3960
|
9
|
+
praisonaiagents/build/lib/praisonaiagents/agent/__init__.py,sha256=sKO8wGEXvtCrvV1e834r1Okv0XAqAxqZCqz6hKLiTvA,79
|
10
|
+
praisonaiagents/build/lib/praisonaiagents/agent/agent.py,sha256=PwbeW6v4Ldcl10JQr9_7TBfg4_FskQh-mGoFUdGxg8w,15483
|
11
|
+
praisonaiagents/build/lib/praisonaiagents/agents/__init__.py,sha256=cgCLFLFcLp9SizmFSHUkH5aX-1seAAsRtQbtIHBBso4,101
|
12
|
+
praisonaiagents/build/lib/praisonaiagents/agents/agents.py,sha256=P2FAtlfD3kPib5a1oLVYanxlU6e4-GhBMQ0YDY5MHY4,13473
|
13
|
+
praisonaiagents/build/lib/praisonaiagents/task/__init__.py,sha256=VL5hXVmyGjINb34AalxpBMl-YW9m5EDcRkMTKkSSl7c,80
|
14
|
+
praisonaiagents/build/lib/praisonaiagents/task/task.py,sha256=4Y1qX8OeEFcid2yhAiPYylvHpuDmWORsyNL16_BiVvI,1831
|
15
|
+
praisonaiagents/task/__init__.py,sha256=VL5hXVmyGjINb34AalxpBMl-YW9m5EDcRkMTKkSSl7c,80
|
16
|
+
praisonaiagents/task/task.py,sha256=4Y1qX8OeEFcid2yhAiPYylvHpuDmWORsyNL16_BiVvI,1831
|
17
|
+
praisonaiagents-0.0.4.dist-info/METADATA,sha256=l-buIBQrgYLY8_1_qAQ6-ZqKJ1OlBNtjejWEFMcy4gQ,232
|
18
|
+
praisonaiagents-0.0.4.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
19
|
+
praisonaiagents-0.0.4.dist-info/top_level.txt,sha256=_HsRddrJ23iDx5TTqVUVvXG2HeHBL5voshncAMDGjtA,16
|
20
|
+
praisonaiagents-0.0.4.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
praisonaiagents
|
@@ -1,12 +0,0 @@
|
|
1
|
-
agents/__init__.py,sha256=479EdtuXm27prpz83bfm6DCoUfx-W0u-LNIphAlqXDc,723
|
2
|
-
agents/main.py,sha256=zDhN5KKtKbfruolDNxlyJkcFlkSt4KQkQTDRfQVAhxc,3960
|
3
|
-
agents/agent/__init__.py,sha256=sKO8wGEXvtCrvV1e834r1Okv0XAqAxqZCqz6hKLiTvA,79
|
4
|
-
agents/agent/agent.py,sha256=CCCjv-qtr6hSB-BG7C8l3z-pXQpnTkX9bW6me36YiaU,15512
|
5
|
-
agents/agents/__init__.py,sha256=cgCLFLFcLp9SizmFSHUkH5aX-1seAAsRtQbtIHBBso4,101
|
6
|
-
agents/agents/agents.py,sha256=P2FAtlfD3kPib5a1oLVYanxlU6e4-GhBMQ0YDY5MHY4,13473
|
7
|
-
agents/task/__init__.py,sha256=VL5hXVmyGjINb34AalxpBMl-YW9m5EDcRkMTKkSSl7c,80
|
8
|
-
agents/task/task.py,sha256=4Y1qX8OeEFcid2yhAiPYylvHpuDmWORsyNL16_BiVvI,1831
|
9
|
-
praisonaiagents-0.0.2.dist-info/METADATA,sha256=hCCZISknDJwJREyP1L92Dot8Hk0fjE4XMhFAkeknOdo,199
|
10
|
-
praisonaiagents-0.0.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
11
|
-
praisonaiagents-0.0.2.dist-info/top_level.txt,sha256=OHAN-tVxGXbi966rMQE_BK7YyDEYoe-JL20jvf6URgI,7
|
12
|
-
praisonaiagents-0.0.2.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
agents
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|