quantalogic 0.2.8__py3-none-any.whl → 0.2.12__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.
quantalogic/agent.py CHANGED
@@ -1,7 +1,5 @@
1
1
  """Enhanced QuantaLogic agent implementing the ReAct framework."""
2
2
 
3
- import os
4
- import sys
5
3
  from collections.abc import Callable
6
4
  from datetime import datetime
7
5
  from typing import Any
@@ -64,6 +62,7 @@ class Agent(BaseModel):
64
62
  event_emitter: EventEmitter = EventEmitter()
65
63
  config: AgentConfig
66
64
  task_to_solve: str
65
+ task_to_solve_summary: str = ""
67
66
  ask_for_user_validation: Callable[[str], bool] = console_ask_for_user_validation
68
67
  last_tool_call: dict[str, Any] = {} # Stores the last tool call information
69
68
  total_tokens: int = 0 # Total tokens in the conversation
@@ -137,6 +136,9 @@ class Agent(BaseModel):
137
136
  logger.debug(f"Solving task... {task}")
138
137
  self._reset_session(task_to_solve=task, max_iterations=max_iterations)
139
138
 
139
+ # Generate task summary
140
+ self.task_to_solve_summary = self._generate_task_summary(task)
141
+
140
142
  # Add system prompt to memory
141
143
  self.memory.add(Message(role="system", content=self.config.system_prompt))
142
144
 
@@ -166,7 +168,7 @@ class Agent(BaseModel):
166
168
  self._update_total_tokens(message_history=self.memory.memory, prompt=current_prompt)
167
169
 
168
170
  # Emit event: Task Think Start after updating total tokens
169
- self._emit_event("task_think_start")
171
+ self._emit_event("task_think_start", {"prompt": current_prompt})
170
172
 
171
173
  self._compact_memory_if_needed(current_prompt)
172
174
 
@@ -443,6 +445,10 @@ class Agent(BaseModel):
443
445
  "You must analyze this answer and evaluate what to do next to solve the task.\n"
444
446
  "If the step failed, take a step back and rethink your approach.\n"
445
447
  "\n"
448
+ "--- Task to solve summary ---\n"
449
+ "\n"
450
+ f"{self.task_to_solve_summary}"
451
+ "\n"
446
452
  "--- Format ---\n"
447
453
  "\n"
448
454
  "You MUST respond with exactly two XML blocks formatted in markdown:\n"
@@ -546,6 +552,7 @@ class Agent(BaseModel):
546
552
  "\n### Tools:\n"
547
553
  "-----------------------------\n"
548
554
  f"{self._get_tools_names_prompt()}\n"
555
+ "\n"
549
556
  "### Variables:\n"
550
557
  "-----------------------------\n"
551
558
  f"{self._get_variable_prompt()}\n"
@@ -575,6 +582,8 @@ class Agent(BaseModel):
575
582
  "Available variables:\n"
576
583
  "\n"
577
584
  f"{', '.join(self.variable_store.keys())}\n"
585
+ if len(self.variable_store.keys()) > 0
586
+ else "None\n"
578
587
  )
579
588
  return prompt_use_variables
580
589
 
@@ -619,6 +628,28 @@ class Agent(BaseModel):
619
628
  self.memory.memory = memory_copy
620
629
  return summary.response
621
630
 
631
+ def _generate_task_summary(self, content: str) -> str:
632
+ """Generate a concise summary of the given content using the generative model.
633
+
634
+ Args:
635
+ content (str): The content to summarize
636
+
637
+ Returns:
638
+ str: Generated summary
639
+ """
640
+ try:
641
+ prompt = (
642
+ "Rewrite this task in a precise, dense, and concise manner:\n"
643
+ f"{content}\n"
644
+ "Summary should be 2-3 sentences maximum. No extra comments should be added.\n"
645
+ )
646
+ result = self.model.generate(prompt=prompt)
647
+ logger.debug(f"Generated summary: {result.response}")
648
+ return result.response
649
+ except Exception as e:
650
+ logger.error(f"Error generating summary: {str(e)}")
651
+ return f"Summary generation failed: {str(e)}"
652
+
622
653
  def _update_session_memory(self, user_content: str, assistant_content: str) -> None:
623
654
  """
624
655
  Log session messages to memory and emit events.
@@ -13,6 +13,7 @@ from quantalogic.tools import (
13
13
  InputQuestionTool,
14
14
  ListDirectoryTool,
15
15
  LLMTool,
16
+ LLMVisionTool,
16
17
  MarkitdownTool,
17
18
  NodeJsTool,
18
19
  PythonTool,
@@ -28,109 +29,141 @@ from quantalogic.tools import (
28
29
  MODEL_NAME = "deepseek/deepseek-chat"
29
30
 
30
31
 
31
- def create_agent(model_name) -> Agent:
32
+ def create_agent(model_name: str, vision_model_name: str | None) -> Agent:
32
33
  """Create an agent with the specified model and tools.
33
34
 
34
35
  Args:
35
36
  model_name (str): Name of the model to use
37
+ vision_model_name (str | None): Name of the vision model to use
38
+
39
+ Returns:
40
+ Agent: An agent with the specified model and tools
36
41
  """
42
+ tools = [
43
+ TaskCompleteTool(),
44
+ ReadFileTool(),
45
+ ReadFileBlockTool(),
46
+ WriteFileTool(),
47
+ EditWholeContentTool(),
48
+ InputQuestionTool(),
49
+ ListDirectoryTool(),
50
+ ExecuteBashCommandTool(),
51
+ ReplaceInFileTool(),
52
+ RipgrepTool(),
53
+ SearchDefinitionNames(),
54
+ MarkitdownTool(),
55
+ LLMTool(model_name=model_name),
56
+ DownloadHttpFileTool(),
57
+ ]
58
+
59
+ if vision_model_name:
60
+ tools.append(LLMVisionTool(model_name=vision_model_name))
61
+
37
62
  return Agent(
38
63
  model_name=model_name,
39
- tools=[
40
- TaskCompleteTool(),
41
- ReadFileTool(),
42
- ReadFileBlockTool(),
43
- WriteFileTool(),
44
- EditWholeContentTool(),
45
- InputQuestionTool(),
46
- ListDirectoryTool(),
47
- ExecuteBashCommandTool(),
48
- ReplaceInFileTool(),
49
- RipgrepTool(),
50
- SearchDefinitionNames(),
51
- MarkitdownTool(),
52
- LLMTool(model_name=model_name),
53
- DownloadHttpFileTool(),
54
- ],
64
+ tools=tools,
55
65
  )
56
66
 
57
67
 
58
- def create_interpreter_agent(model_name: str) -> Agent:
68
+ def create_interpreter_agent(model_name: str, vision_model_name: str | None) -> Agent:
59
69
  """Create an interpreter agent with the specified model and tools.
60
70
 
61
71
  Args:
62
72
  model_name (str): Name of the model to use
63
- """
64
- return Agent(
65
- model_name=model_name,
66
- tools=[
67
- TaskCompleteTool(),
68
- ReadFileTool(),
69
- ReadFileBlockTool(),
70
- WriteFileTool(),
71
- EditWholeContentTool(),
72
- InputQuestionTool(),
73
- ListDirectoryTool(),
74
- ExecuteBashCommandTool(),
75
- ReplaceInFileTool(),
76
- RipgrepTool(),
77
- PythonTool(),
78
- NodeJsTool(),
79
- SearchDefinitionNames(),
80
- DownloadHttpFileTool(),
81
- ],
82
- )
83
-
73
+ vision_model_name (str | None): Name of the vision model to use
84
74
 
85
- def create_full_agent(model_name: str) -> Agent:
75
+ Returns:
76
+ Agent: An interpreter agent with the specified model and tools
77
+ """
78
+ tools = [
79
+ TaskCompleteTool(),
80
+ ReadFileTool(),
81
+ ReadFileBlockTool(),
82
+ WriteFileTool(),
83
+ EditWholeContentTool(),
84
+ InputQuestionTool(),
85
+ ListDirectoryTool(),
86
+ ExecuteBashCommandTool(),
87
+ ReplaceInFileTool(),
88
+ RipgrepTool(),
89
+ PythonTool(),
90
+ NodeJsTool(),
91
+ SearchDefinitionNames(),
92
+ MarkitdownTool(),
93
+ LLMTool(model_name=model_name),
94
+ DownloadHttpFileTool(),
95
+ ]
96
+ return Agent(model_name=model_name, tools=tools)
97
+
98
+
99
+ def create_full_agent(model_name: str, vision_model_name: str | None) -> Agent:
86
100
  """Create an agent with the specified model and many tools.
87
101
 
88
102
  Args:
89
103
  model_name (str): Name of the model to use
104
+ vision_model_name (str | None): Name of the vision model to use
105
+
106
+ Returns:
107
+ Agent: An agent with the specified model and tools
108
+
90
109
  """
110
+ tools = [
111
+ TaskCompleteTool(),
112
+ ReadFileTool(),
113
+ ReadFileBlockTool(),
114
+ WriteFileTool(),
115
+ EditWholeContentTool(),
116
+ InputQuestionTool(),
117
+ ListDirectoryTool(),
118
+ ExecuteBashCommandTool(),
119
+ ReplaceInFileTool(),
120
+ RipgrepTool(),
121
+ PythonTool(),
122
+ NodeJsTool(),
123
+ SearchDefinitionNames(),
124
+ MarkitdownTool(),
125
+ LLMTool(model_name=model_name),
126
+ DownloadHttpFileTool(),
127
+ ]
128
+
129
+ if vision_model_name:
130
+ tools.append(LLMVisionTool(model_name=vision_model_name))
131
+
91
132
  return Agent(
92
133
  model_name=model_name,
93
- tools=[
94
- TaskCompleteTool(),
95
- ReadFileTool(),
96
- ReadFileBlockTool(),
97
- WriteFileTool(),
98
- EditWholeContentTool(),
99
- InputQuestionTool(),
100
- ListDirectoryTool(),
101
- ExecuteBashCommandTool(),
102
- ReplaceInFileTool(),
103
- RipgrepTool(),
104
- PythonTool(),
105
- NodeJsTool(),
106
- SearchDefinitionNames(),
107
- MarkitdownTool(),
108
- LLMTool(model_name=model_name),
109
- DownloadHttpFileTool(),
110
- ],
134
+ tools=tools,
111
135
  )
112
136
 
113
137
 
114
- def create_orchestrator_agent(model_name: str) -> Agent:
138
+ def create_orchestrator_agent(model_name: str, vision_model_name: str | None = None) -> Agent:
115
139
  """Create an agent with the specified model and tools.
116
140
 
117
141
  Args:
118
142
  model_name (str): Name of the model to use
143
+ vision_model_name (str | None): Name of the vision model to use
144
+
145
+ Returns:
146
+ Agent: An agent with the specified model and tools
119
147
  """
120
148
  # Rebuild AgentTool to resolve forward references
121
149
  AgentTool.model_rebuild()
122
150
 
123
151
  coding_agent_instance = create_coding_agent(model_name)
124
152
 
153
+ tools = [
154
+ TaskCompleteTool(),
155
+ ListDirectoryTool(),
156
+ ReadFileBlockTool(),
157
+ RipgrepTool(),
158
+ SearchDefinitionNames(),
159
+ LLMTool(model_name=MODEL_NAME),
160
+ AgentTool(agent=coding_agent_instance, agent_role="software expert", name="coder_agent_tool"),
161
+ ]
162
+
163
+ if vision_model_name:
164
+ tools.append(LLMVisionTool(model_name=vision_model_name))
165
+
125
166
  return Agent(
126
167
  model_name=model_name,
127
- tools=[
128
- TaskCompleteTool(),
129
- ListDirectoryTool(),
130
- ReadFileBlockTool(),
131
- RipgrepTool(),
132
- SearchDefinitionNames(),
133
- LLMTool(model_name=MODEL_NAME),
134
- AgentTool(agent=coding_agent_instance, agent_role="software expert", name="coder_agent_tool"),
135
- ],
168
+ tools=tools,
136
169
  )
@@ -5,6 +5,7 @@ from quantalogic.tools import (
5
5
  InputQuestionTool,
6
6
  ListDirectoryTool,
7
7
  LLMTool,
8
+ LLMVisionTool,
8
9
  ReadFileBlockTool,
9
10
  ReadFileTool,
10
11
  ReplaceInFileTool,
@@ -17,11 +18,12 @@ from quantalogic.utils import get_coding_environment
17
18
  from quantalogic.utils.get_quantalogic_rules_content import get_quantalogic_rules_file_content
18
19
 
19
20
 
20
- def create_coding_agent(model_name: str, basic: bool = False) -> Agent:
21
+ def create_coding_agent(model_name: str, vision_model_name: str | None = None, basic: bool = False) -> Agent:
21
22
  """Creates and configures a coding agent with a comprehensive set of tools.
22
23
 
23
24
  Args:
24
25
  model_name (str): Name of the language model to use for the agent's core capabilities
26
+ vision_model_name (str | None): Name of the vision model to use for the agent's core capabilities
25
27
  basic (bool, optional): If True, the agent will be configured with a basic set of tools.
26
28
 
27
29
  Returns:
@@ -59,12 +61,15 @@ def create_coding_agent(model_name: str, basic: bool = False) -> Agent:
59
61
  InputQuestionTool(),
60
62
  ]
61
63
 
64
+ if vision_model_name:
65
+ tools.append(LLMVisionTool(model_name=vision_model_name))
66
+
62
67
  if not basic:
63
68
  tools.append(
64
69
  LLMTool(
65
70
  model_name=model_name,
66
71
  system_prompt="You are a software expert, your role is to answer coding questions.",
67
- name="coding_consultant", # Handles implementation-level coding questions
72
+ name="coding_consultant", # Handles implementation-level coding questions
68
73
  )
69
74
  )
70
75
  tools.append(
@@ -5,16 +5,17 @@ from litellm import completion, exceptions, get_max_tokens, get_model_info, toke
5
5
  from loguru import logger
6
6
  from pydantic import BaseModel, Field, field_validator
7
7
 
8
- MIN_RETRIES = 3
8
+ MIN_RETRIES = 1
9
9
 
10
10
 
11
11
  class Message(BaseModel):
12
12
  """Represents a message in a conversation with a specific role and content."""
13
13
 
14
14
  role: str = Field(..., min_length=1)
15
- content: str = Field(..., min_length=1)
15
+ content: str | dict = Field(..., min_length=1)
16
+ image_url: str | None = Field(default=None, pattern=r"^https?://")
16
17
 
17
- @field_validator("role", "content")
18
+ @field_validator("role")
18
19
  @classmethod
19
20
  def validate_not_empty(cls, v: str) -> str:
20
21
  """Validate that the field is not empty or whitespace-only."""
@@ -22,6 +23,26 @@ class Message(BaseModel):
22
23
  raise ValueError("Field cannot be empty or whitespace-only")
23
24
  return v
24
25
 
26
+ @field_validator("content")
27
+ @classmethod
28
+ def validate_content(cls, v: str | dict) -> str | dict:
29
+ """Validate content based on its type."""
30
+ if isinstance(v, str):
31
+ if not v or not v.strip():
32
+ raise ValueError("Text content cannot be empty or whitespace-only")
33
+ elif isinstance(v, dict):
34
+ if not v.get("text") or not v.get("image_url"):
35
+ raise ValueError("Multimodal content must have both text and image_url")
36
+ return v
37
+
38
+ @field_validator("image_url")
39
+ @classmethod
40
+ def validate_image_url(cls, v: str | None) -> str | None:
41
+ """Validate image URL format if present."""
42
+ if v and not v.startswith(("http://", "https://")):
43
+ raise ValueError("Image URL must start with http:// or https://")
44
+ return v
45
+
25
46
 
26
47
  class TokenUsage(BaseModel):
27
48
  """Represents token usage statistics for a language model."""
@@ -59,6 +80,7 @@ class GenerativeModel:
59
80
  temperature: Sampling temperature between 0 and 1.
60
81
  Defaults to 0.7.
61
82
  """
83
+ logger.debug(f"Initializing GenerativeModel with model={model}, temperature={temperature}")
62
84
  self.model = model
63
85
  self.temperature = temperature
64
86
 
@@ -85,15 +107,18 @@ class GenerativeModel:
85
107
  )
86
108
 
87
109
  # Retry on specific retriable exceptions
88
- def generate_with_history(self, messages_history: list[Message], prompt: str) -> ResponseStats:
89
- """Generate a response with conversation history.
110
+ def generate_with_history(
111
+ self, messages_history: list[Message], prompt: str, image_url: str | None = None
112
+ ) -> ResponseStats:
113
+ """Generate a response with conversation history and optional image.
90
114
 
91
- Generates a response based on previous conversation messages
92
- and a new user prompt.
115
+ Generates a response based on previous conversation messages,
116
+ a new user prompt, and an optional image URL.
93
117
 
94
118
  Args:
95
119
  messages_history: Previous conversation messages.
96
120
  prompt: Current user prompt.
121
+ image_url: Optional image URL for visual queries.
97
122
 
98
123
  Returns:
99
124
  Detailed response statistics.
@@ -105,7 +130,19 @@ class GenerativeModel:
105
130
  Exception: For other unexpected errors.
106
131
  """
107
132
  messages = [{"role": msg.role, "content": str(msg.content)} for msg in messages_history]
108
- messages.append({"role": "user", "content": str(prompt)})
133
+
134
+ if image_url:
135
+ messages.append(
136
+ {
137
+ "role": "user",
138
+ "content": [
139
+ {"type": "text", "text": str(prompt)},
140
+ {"type": "image_url", "image_url": {"url": image_url}},
141
+ ],
142
+ }
143
+ )
144
+ else:
145
+ messages.append({"role": "user", "content": str(prompt)})
109
146
 
110
147
  try:
111
148
  logger.debug(f"Generating response for prompt: {prompt}")
@@ -140,9 +177,12 @@ class GenerativeModel:
140
177
  }
141
178
 
142
179
  logger.error("LLM Generation Error: {}", error_details)
180
+ logger.debug(f"Error details: {error_details}")
181
+ logger.debug(f"Model: {self.model}, Temperature: {self.temperature}")
143
182
 
144
183
  # Handle authentication and permission errors
145
184
  if isinstance(e, self.AUTH_EXCEPTIONS):
185
+ logger.debug("Authentication error occurred")
146
186
  raise openai.AuthenticationError(
147
187
  f"Authentication failed with provider {error_details['provider']}"
148
188
  ) from e
@@ -162,7 +202,7 @@ class GenerativeModel:
162
202
  # Wrap unknown errors in APIError
163
203
  raise openai.APIError(f"Unexpected error during generation: {str(e)}") from e
164
204
 
165
- def generate(self, prompt: str) -> ResponseStats:
205
+ def generate(self, prompt: str, image_url: str | None = None) -> ResponseStats:
166
206
  """Generate a response without conversation history.
167
207
 
168
208
  Generates a response for a single user prompt without
@@ -170,11 +210,12 @@ class GenerativeModel:
170
210
 
171
211
  Args:
172
212
  prompt: User prompt.
213
+ image_url: Optional image URL for visual queries.
173
214
 
174
215
  Returns:
175
216
  Detailed response statistics.
176
217
  """
177
- return self.generate_with_history([], prompt)
218
+ return self.generate_with_history([], prompt, image_url)
178
219
 
179
220
  def get_max_tokens(self) -> int:
180
221
  """Get the maximum number of tokens that can be generated by the model."""
@@ -182,8 +223,11 @@ class GenerativeModel:
182
223
 
183
224
  def token_counter(self, messages: list[Message]) -> int:
184
225
  """Count the number of tokens in a list of messages."""
226
+ logger.debug(f"Counting tokens for {len(messages)} messages using model {self.model}")
185
227
  litellm_messages = [{"role": msg.role, "content": str(msg.content)} for msg in messages]
186
- return token_counter(model=self.model, messages=litellm_messages)
228
+ token_count = token_counter(model=self.model, messages=litellm_messages)
229
+ logger.debug(f"Token count: {token_count}")
230
+ return token_count
187
231
 
188
232
  def token_counter_with_history(self, messages_history: list[Message], prompt: str) -> int:
189
233
  """Count the number of tokens in a list of messages and a prompt."""
@@ -193,12 +237,18 @@ class GenerativeModel:
193
237
 
194
238
  def get_model_info(self) -> dict | None:
195
239
  """Get information about the model."""
240
+ logger.debug(f"Retrieving model info for {self.model}")
196
241
  model_info = get_model_info(self.model)
197
242
 
198
243
  if not model_info:
199
- # Search without prefix "openrouter/"
244
+ logger.debug("Model info not found, trying without openrouter/ prefix")
200
245
  model_info = get_model_info(self.model.replace("openrouter/", ""))
201
246
 
247
+ if model_info:
248
+ logger.debug(f"Model info retrieved: {model_info.keys()}")
249
+ else:
250
+ logger.debug("No model info available")
251
+
202
252
  return model_info
203
253
 
204
254
  def get_model_max_input_tokens(self) -> int: