quantalogic 0.2.0__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.
Files changed (68) hide show
  1. quantalogic/__init__.py +20 -0
  2. quantalogic/agent.py +638 -0
  3. quantalogic/agent_config.py +138 -0
  4. quantalogic/coding_agent.py +83 -0
  5. quantalogic/event_emitter.py +223 -0
  6. quantalogic/generative_model.py +226 -0
  7. quantalogic/interactive_text_editor.py +190 -0
  8. quantalogic/main.py +185 -0
  9. quantalogic/memory.py +217 -0
  10. quantalogic/model_names.py +19 -0
  11. quantalogic/print_event.py +66 -0
  12. quantalogic/prompts.py +99 -0
  13. quantalogic/server/__init__.py +3 -0
  14. quantalogic/server/agent_server.py +633 -0
  15. quantalogic/server/models.py +60 -0
  16. quantalogic/server/routes.py +117 -0
  17. quantalogic/server/state.py +199 -0
  18. quantalogic/server/static/js/event_visualizer.js +430 -0
  19. quantalogic/server/static/js/quantalogic.js +571 -0
  20. quantalogic/server/templates/index.html +134 -0
  21. quantalogic/tool_manager.py +68 -0
  22. quantalogic/tools/__init__.py +46 -0
  23. quantalogic/tools/agent_tool.py +88 -0
  24. quantalogic/tools/download_http_file_tool.py +64 -0
  25. quantalogic/tools/edit_whole_content_tool.py +70 -0
  26. quantalogic/tools/elixir_tool.py +240 -0
  27. quantalogic/tools/execute_bash_command_tool.py +116 -0
  28. quantalogic/tools/input_question_tool.py +57 -0
  29. quantalogic/tools/language_handlers/__init__.py +21 -0
  30. quantalogic/tools/language_handlers/c_handler.py +33 -0
  31. quantalogic/tools/language_handlers/cpp_handler.py +33 -0
  32. quantalogic/tools/language_handlers/go_handler.py +33 -0
  33. quantalogic/tools/language_handlers/java_handler.py +37 -0
  34. quantalogic/tools/language_handlers/javascript_handler.py +42 -0
  35. quantalogic/tools/language_handlers/python_handler.py +29 -0
  36. quantalogic/tools/language_handlers/rust_handler.py +33 -0
  37. quantalogic/tools/language_handlers/scala_handler.py +33 -0
  38. quantalogic/tools/language_handlers/typescript_handler.py +42 -0
  39. quantalogic/tools/list_directory_tool.py +123 -0
  40. quantalogic/tools/llm_tool.py +119 -0
  41. quantalogic/tools/markitdown_tool.py +105 -0
  42. quantalogic/tools/nodejs_tool.py +515 -0
  43. quantalogic/tools/python_tool.py +469 -0
  44. quantalogic/tools/read_file_block_tool.py +140 -0
  45. quantalogic/tools/read_file_tool.py +79 -0
  46. quantalogic/tools/replace_in_file_tool.py +300 -0
  47. quantalogic/tools/ripgrep_tool.py +353 -0
  48. quantalogic/tools/search_definition_names.py +419 -0
  49. quantalogic/tools/task_complete_tool.py +35 -0
  50. quantalogic/tools/tool.py +146 -0
  51. quantalogic/tools/unified_diff_tool.py +387 -0
  52. quantalogic/tools/write_file_tool.py +97 -0
  53. quantalogic/utils/__init__.py +17 -0
  54. quantalogic/utils/ask_user_validation.py +12 -0
  55. quantalogic/utils/download_http_file.py +77 -0
  56. quantalogic/utils/get_coding_environment.py +15 -0
  57. quantalogic/utils/get_environment.py +26 -0
  58. quantalogic/utils/get_quantalogic_rules_content.py +19 -0
  59. quantalogic/utils/git_ls.py +121 -0
  60. quantalogic/utils/read_file.py +54 -0
  61. quantalogic/utils/read_http_text_content.py +101 -0
  62. quantalogic/xml_parser.py +242 -0
  63. quantalogic/xml_tool_parser.py +99 -0
  64. quantalogic-0.2.0.dist-info/LICENSE +201 -0
  65. quantalogic-0.2.0.dist-info/METADATA +1034 -0
  66. quantalogic-0.2.0.dist-info/RECORD +68 -0
  67. quantalogic-0.2.0.dist-info/WHEEL +4 -0
  68. quantalogic-0.2.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,138 @@
1
+ """Module for agent configuration and creation."""
2
+
3
+ # Standard library imports
4
+
5
+ # Local application imports
6
+ from quantalogic.agent import Agent
7
+ from quantalogic.coding_agent import create_coding_agent
8
+ from quantalogic.tools import (
9
+ AgentTool,
10
+ DownloadHttpFileTool,
11
+ EditWholeContentTool,
12
+ ExecuteBashCommandTool,
13
+ InputQuestionTool,
14
+ ListDirectoryTool,
15
+ LLMTool,
16
+ MarkitdownTool,
17
+ NodeJsTool,
18
+ PythonTool,
19
+ ReadFileBlockTool,
20
+ ReadFileTool,
21
+ ReplaceInFileTool,
22
+ RipgrepTool,
23
+ SearchDefinitionNames,
24
+ TaskCompleteTool,
25
+ WriteFileTool,
26
+ )
27
+
28
+ MODEL_NAME = "deepseek/deepseek-chat"
29
+
30
+
31
+ def create_agent(model_name) -> Agent:
32
+ """Create an agent with the specified model and tools.
33
+
34
+ Args:
35
+ model_name (str): Name of the model to use
36
+ """
37
+ return Agent(
38
+ 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
+ ],
55
+ )
56
+
57
+
58
+ def create_interpreter_agent(model_name: str) -> Agent:
59
+ """Create an interpreter agent with the specified model and tools.
60
+
61
+ Args:
62
+ 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
+
84
+
85
+ def create_full_agent(model_name: str) -> Agent:
86
+ """Create an agent with the specified model and many tools.
87
+
88
+ Args:
89
+ model_name (str): Name of the model to use
90
+ """
91
+ return Agent(
92
+ 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
+ ],
111
+ )
112
+
113
+
114
+ def create_orchestrator_agent(model_name: str) -> Agent:
115
+ """Create an agent with the specified model and tools.
116
+
117
+ Args:
118
+ model_name (str): Name of the model to use
119
+ """
120
+ # Rebuild AgentTool to resolve forward references
121
+ AgentTool.model_rebuild()
122
+
123
+ coding_agent_instance = create_coding_agent(model_name)
124
+
125
+ return Agent(
126
+ 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
+ ],
136
+ )
137
+
138
+
@@ -0,0 +1,83 @@
1
+ from quantalogic.agent import Agent
2
+ from quantalogic.tools import (
3
+ EditWholeContentTool,
4
+ ExecuteBashCommandTool,
5
+ InputQuestionTool,
6
+ ListDirectoryTool,
7
+ LLMTool,
8
+ ReadFileBlockTool,
9
+ ReadFileTool,
10
+ ReplaceInFileTool,
11
+ RipgrepTool,
12
+ SearchDefinitionNames,
13
+ TaskCompleteTool,
14
+ WriteFileTool,
15
+ )
16
+ from quantalogic.utils import get_coding_environment
17
+ from quantalogic.utils.get_quantalogic_rules_content import get_quantalogic_rules_file_content
18
+
19
+
20
+ def create_coding_agent(model_name: str, basic: bool = False) -> Agent:
21
+ """Creates and configures a coding agent with a comprehensive set of tools.
22
+
23
+ Args:
24
+ model_name (str): Name of the language model to use for the agent's core capabilities
25
+ basic (bool, optional): If True, the agent will be configured with a basic set of tools.
26
+
27
+ Returns:
28
+ Agent: A fully configured coding agent instance with:
29
+ - File manipulation tools
30
+ - Code search capabilities
31
+ - Specialized language model tools for coding and architecture
32
+ """
33
+ specific_expertise = (
34
+ "Software expert focused on pragmatic solutions."
35
+ "Validates codebase pre/post changes."
36
+ "Employs SearchDefinitionNames for code search; ReplaceInFileTool for updates."
37
+ "Exercise caution with the surrounding context during search/replace operations."
38
+ "For refactoring tasks, take the time to develop a comprehensive plan for implementing the proposed changes."
39
+ )
40
+ quantalogic_rules_file_content = get_quantalogic_rules_file_content()
41
+
42
+ if quantalogic_rules_file_content:
43
+ specific_expertise += "\n\n" "<coding_rules>\n" f"{quantalogic_rules_file_content}" "\n</coding_rules>\n"
44
+
45
+ tools = [
46
+ # Core file manipulation tools
47
+ TaskCompleteTool(), # Marks task completion
48
+ ReadFileBlockTool(), # Reads specific file sections
49
+ WriteFileTool(), # Creates new files
50
+ ReplaceInFileTool(), # Updates file sections
51
+ EditWholeContentTool(), # Modifies entire files
52
+ # Code navigation and search tools
53
+ ListDirectoryTool(), # Lists directory contents
54
+ RipgrepTool(), # Searches code with regex
55
+ SearchDefinitionNames(), # Finds code definitions
56
+ # Specialized language model tools
57
+ ReadFileTool(),
58
+ ExecuteBashCommandTool(),
59
+ InputQuestionTool(),
60
+ ]
61
+
62
+ if not basic:
63
+ tools.append(
64
+ LLMTool(
65
+ model_name=model_name,
66
+ system_prompt="You are a software expert, your role is to answer coding questions.",
67
+ name="coding_consultant", # Handles implementation-level coding questions
68
+ )
69
+ )
70
+ tools.append(
71
+ LLMTool(
72
+ model_name=model_name,
73
+ system_prompt="You are a software architect, your role is to answer software architecture questions.",
74
+ name="software_architect", # Handles system design and architecture questions
75
+ )
76
+ )
77
+
78
+ return Agent(
79
+ model_name=model_name,
80
+ tools=tools,
81
+ specific_expertise=specific_expertise,
82
+ get_environment=get_coding_environment,
83
+ )
@@ -0,0 +1,223 @@
1
+ import threading
2
+ from typing import Any, Callable
3
+
4
+
5
+ class EventEmitter:
6
+ """A thread-safe event emitter class for managing event listeners and emissions."""
7
+
8
+ def __init__(self) -> None:
9
+ """Initialize an empty EventEmitter instance.
10
+
11
+ Creates an empty dictionary to store event listeners,
12
+ where each event can have multiple callable listeners.
13
+ Also initializes a list for wildcard listeners that listen to all events.
14
+ """
15
+ self._listeners: dict[str, list[Callable[..., Any]]] = {}
16
+ self._wildcard_listeners: list[Callable[..., Any]] = []
17
+ self._lock = threading.RLock()
18
+
19
+ def on(self, event: str | list[str], listener: Callable[..., Any]) -> None:
20
+ """Register an event listener for one or more events.
21
+
22
+ If event is a list, the listener is registered for each event in the list.
23
+
24
+ Parameters:
25
+ - event (str | list[str]): The event name or a list of event names to listen to.
26
+ - listener (Callable): The function to call when the specified event(s) are emitted.
27
+ """
28
+ if isinstance(event, str):
29
+ events = [event]
30
+ elif isinstance(event, list):
31
+ events = event
32
+ else:
33
+ raise TypeError("Event must be a string or a list of strings.")
34
+
35
+ with self._lock:
36
+ for evt in events:
37
+ if evt == "*":
38
+ if listener not in self._wildcard_listeners:
39
+ self._wildcard_listeners.append(listener)
40
+ else:
41
+ if evt not in self._listeners:
42
+ self._listeners[evt] = []
43
+ if listener not in self._listeners[evt]:
44
+ self._listeners[evt].append(listener)
45
+
46
+ def once(self, event: str | list[str], listener: Callable[..., Any]) -> None:
47
+ """Register a one-time event listener for one or more events.
48
+
49
+ The listener is removed after it is invoked the first time the event is emitted.
50
+
51
+ Parameters:
52
+ - event (str | list[str]): The event name or a list of event names to listen to.
53
+ """
54
+
55
+ def wrapper(*args: Any, **kwargs: Any) -> None:
56
+ self.off(event, wrapper)
57
+ listener(*args, **kwargs)
58
+
59
+ self.on(event, wrapper)
60
+
61
+ def off(
62
+ self,
63
+ event: str | list[str] | None = None,
64
+ listener: Callable[..., Any] = None,
65
+ ) -> None:
66
+ """Unregister an event listener.
67
+
68
+ If event is None, removes the listener from all events.
69
+
70
+ Parameters:
71
+ - event (str | list[str] | None): The name of the event or a list of event names to stop listening to.
72
+ If None, removes the listener from all events.
73
+ - listener (Callable): The function to remove from the event listeners.
74
+ """
75
+ with self._lock:
76
+ if event is None:
77
+ # Remove from all specific events
78
+ for evt_list in self._listeners.values():
79
+ if listener in evt_list:
80
+ evt_list.remove(listener)
81
+ # Remove from wildcard listeners
82
+ if listener in self._wildcard_listeners:
83
+ self._wildcard_listeners.remove(listener)
84
+ else:
85
+ if isinstance(event, str):
86
+ events = [event]
87
+ elif isinstance(event, list):
88
+ events = event
89
+ else:
90
+ raise TypeError("Event must be a string, a list of strings, or None.")
91
+
92
+ for evt in events:
93
+ if evt == "*":
94
+ if listener in self._wildcard_listeners:
95
+ self._wildcard_listeners.remove(listener)
96
+ elif evt in self._listeners:
97
+ try:
98
+ self._listeners[evt].remove(listener)
99
+ except ValueError:
100
+ pass # Listener was not found for this event
101
+
102
+ def emit(self, event: str, *args: Any, **kwargs: Any) -> None:
103
+ """Emit an event to all registered listeners.
104
+
105
+ First, invokes wildcard listeners, then listeners registered to the specific event.
106
+
107
+ Parameters:
108
+ - event (str): The name of the event to emit.
109
+ - args: Positional arguments to pass to the listeners.
110
+ - kwargs: Keyword arguments to pass to the listeners.
111
+ """
112
+ with self._lock:
113
+ listeners = list(self._wildcard_listeners)
114
+ if event in self._listeners:
115
+ listeners.extend(self._listeners[event])
116
+
117
+ for listener in listeners:
118
+ try:
119
+ listener(event, *args, **kwargs)
120
+ except Exception as e:
121
+ # Log the exception or handle it as needed
122
+ print(f"Error in listener {listener}: {e}")
123
+
124
+ def clear(self, event: str) -> None:
125
+ """Clear all listeners for a specific event.
126
+
127
+ Parameters:
128
+ - event (str): The name of the event to clear listeners from.
129
+ """
130
+ with self._lock:
131
+ if event in self._listeners:
132
+ del self._listeners[event]
133
+
134
+ def clear_all(self) -> None:
135
+ """Clear all listeners for all events, including wildcard listeners."""
136
+ with self._lock:
137
+ self._listeners.clear()
138
+ self._wildcard_listeners.clear()
139
+
140
+ def listeners(self, event: str) -> list[Callable[..., Any]]:
141
+ """Retrieve all listeners registered for a specific event, including wildcard listeners.
142
+
143
+ Parameters:
144
+ - event (str): The name of the event.
145
+
146
+ Returns:
147
+ - List of callables registered for the event.
148
+ """
149
+ with self._lock:
150
+ listeners = list(self._wildcard_listeners)
151
+ if event in self._listeners:
152
+ listeners.extend(self._listeners[event])
153
+ return listeners
154
+
155
+ def has_listener(self, event: str | None, listener: Callable[..., Any]) -> bool:
156
+ """Check if a specific listener is registered for an event.
157
+
158
+ Parameters:
159
+ - event (str | None): The name of the event. If None, checks in wildcard listeners.
160
+ - listener (Callable): The listener to check.
161
+
162
+ Returns:
163
+ - True if the listener is registered for the event, False otherwise.
164
+ """
165
+ with self._lock:
166
+ if event is None:
167
+ return listener in self._wildcard_listeners
168
+ elif event == "*":
169
+ return listener in self._wildcard_listeners
170
+ else:
171
+ return listener in self._listeners.get(event, [])
172
+
173
+
174
+ if __name__ == "__main__":
175
+
176
+ def on_data_received(data):
177
+ print(f"Data received: {data}")
178
+
179
+ def on_any_event(event, data):
180
+ print(f"Event '{event}' emitted with data: {data}")
181
+
182
+ emitter = EventEmitter()
183
+
184
+ # Register specific event listener
185
+ emitter.on("data", on_data_received)
186
+
187
+ # Register wildcard listener
188
+ emitter.on("*", on_any_event)
189
+
190
+ # Emit 'data' event
191
+ emitter.emit("data", "Sample Data")
192
+
193
+ # Output:
194
+ # Event 'data' emitted with data: Sample Data
195
+ # Data received: Sample Data
196
+
197
+ # Emit 'update' event
198
+ emitter.emit("update", "Update Data")
199
+
200
+ # Output:
201
+ # Event 'update' emitted with data: Update Data
202
+
203
+ # Register a one-time listener
204
+ def once_listener(data):
205
+ print(f"Once listener received: {data}")
206
+
207
+ emitter.once("data", once_listener)
208
+
209
+ # Emit 'data' event
210
+ emitter.emit("data", "First Call")
211
+
212
+ # Output:
213
+ # Event 'data' emitted with data: First Call
214
+ # Data received: First Call
215
+ # Once listener received: First Call
216
+
217
+ # Emit 'data' event again
218
+ emitter.emit("data", "Second Call")
219
+
220
+ # Output:
221
+ # Event 'data' emitted with data: Second Call
222
+ # Data received: Second Call
223
+ # (Once listener is not called again)
@@ -0,0 +1,226 @@
1
+ """Generative model module for AI-powered text generation."""
2
+
3
+ import openai
4
+ from litellm import completion, exceptions, get_max_tokens, get_model_info, token_counter
5
+ from loguru import logger
6
+ from pydantic import BaseModel, Field, field_validator
7
+
8
+ MIN_RETRIES = 3
9
+
10
+
11
+ class Message(BaseModel):
12
+ """Represents a message in a conversation with a specific role and content."""
13
+
14
+ role: str = Field(..., min_length=1)
15
+ content: str = Field(..., min_length=1)
16
+
17
+ @field_validator("role", "content")
18
+ @classmethod
19
+ def validate_not_empty(cls, v: str) -> str:
20
+ """Validate that the field is not empty or whitespace-only."""
21
+ if not v or not v.strip():
22
+ raise ValueError("Field cannot be empty or whitespace-only")
23
+ return v
24
+
25
+
26
+ class TokenUsage(BaseModel):
27
+ """Represents token usage statistics for a language model."""
28
+
29
+ prompt_tokens: int
30
+ completion_tokens: int
31
+ total_tokens: int
32
+
33
+
34
+ class ResponseStats(BaseModel):
35
+ """Represents detailed statistics for a model response."""
36
+
37
+ response: str
38
+ usage: TokenUsage
39
+ model: str
40
+ finish_reason: str | None = None
41
+
42
+
43
+ class GenerativeModel:
44
+ """Generative model for AI-powered text generation with configurable parameters."""
45
+
46
+ def __init__(
47
+ self,
48
+ model: str = "ollama/qwen2.5-coder:14b",
49
+ temperature: float = 0.7,
50
+ ) -> None:
51
+ """Initialize a generative model with configurable parameters.
52
+
53
+ Configure the generative model with specified model,
54
+ temperature, and maximum token settings.
55
+
56
+ Args:
57
+ model: Model identifier.
58
+ Defaults to "ollama/qwen2.5-coder:14b".
59
+ temperature: Sampling temperature between 0 and 1.
60
+ Defaults to 0.7.
61
+ """
62
+ self.model = model
63
+ self.temperature = temperature
64
+
65
+ # Define retriable exceptions based on LiteLLM's exception mapping
66
+ RETRIABLE_EXCEPTIONS = (
67
+ exceptions.RateLimitError, # Rate limits - should retry
68
+ exceptions.APIConnectionError, # Connection issues - should retry
69
+ exceptions.ServiceUnavailableError, # Service issues - should retry
70
+ exceptions.Timeout, # Timeout - should retry
71
+ exceptions.APIError, # Generic API errors - should retry
72
+ )
73
+
74
+ # Non-retriable exceptions that need specific handling
75
+ CONTEXT_EXCEPTIONS = (
76
+ exceptions.ContextWindowExceededError,
77
+ exceptions.InvalidRequestError,
78
+ )
79
+
80
+ POLICY_EXCEPTIONS = (exceptions.ContentPolicyViolationError,)
81
+
82
+ AUTH_EXCEPTIONS = (
83
+ exceptions.AuthenticationError,
84
+ exceptions.PermissionDeniedError,
85
+ )
86
+
87
+ # 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.
90
+
91
+ Generates a response based on previous conversation messages
92
+ and a new user prompt.
93
+
94
+ Args:
95
+ messages_history: Previous conversation messages.
96
+ prompt: Current user prompt.
97
+
98
+ Returns:
99
+ Detailed response statistics.
100
+
101
+ Raises:
102
+ openai.AuthenticationError: If authentication fails.
103
+ openai.InvalidRequestError: If the request is invalid (e.g., context length exceeded).
104
+ openai.APIError: For content policy violations or other API errors.
105
+ Exception: For other unexpected errors.
106
+ """
107
+ messages = [{"role": msg.role, "content": str(msg.content)} for msg in messages_history]
108
+ messages.append({"role": "user", "content": str(prompt)})
109
+
110
+ try:
111
+ logger.debug(f"Generating response for prompt: {prompt}")
112
+
113
+ response = completion(
114
+ temperature=self.temperature,
115
+ model=self.model,
116
+ messages=messages,
117
+ num_retries=MIN_RETRIES,
118
+ )
119
+
120
+ token_usage = TokenUsage(
121
+ prompt_tokens=response.usage.prompt_tokens,
122
+ completion_tokens=response.usage.completion_tokens,
123
+ total_tokens=response.usage.total_tokens,
124
+ )
125
+
126
+ return ResponseStats(
127
+ response=response.choices[0].message.content,
128
+ usage=token_usage,
129
+ model=self.model,
130
+ finish_reason=response.choices[0].finish_reason,
131
+ )
132
+
133
+ except Exception as e:
134
+ error_details = {
135
+ "error_type": type(e).__name__,
136
+ "message": str(e),
137
+ "model": self.model,
138
+ "provider": getattr(e, "llm_provider", "unknown"),
139
+ "status_code": getattr(e, "status_code", None),
140
+ }
141
+
142
+ logger.error("LLM Generation Error: {}", error_details)
143
+
144
+ # Handle authentication and permission errors
145
+ if isinstance(e, self.AUTH_EXCEPTIONS):
146
+ raise openai.AuthenticationError(
147
+ f"Authentication failed with provider {error_details['provider']}"
148
+ ) from e
149
+
150
+ # Handle context window errors
151
+ if isinstance(e, self.CONTEXT_EXCEPTIONS):
152
+ raise openai.InvalidRequestError(f"Context window exceeded or invalid request: {str(e)}") from e
153
+
154
+ # Handle content policy violations
155
+ if isinstance(e, self.POLICY_EXCEPTIONS):
156
+ raise openai.APIError(f"Content policy violation: {str(e)}") from e
157
+
158
+ # For other exceptions, preserve the original error type if it's from OpenAI
159
+ if isinstance(e, openai.OpenAIError):
160
+ raise
161
+
162
+ # Wrap unknown errors in APIError
163
+ raise openai.APIError(f"Unexpected error during generation: {str(e)}") from e
164
+
165
+ def generate(self, prompt: str) -> ResponseStats:
166
+ """Generate a response without conversation history.
167
+
168
+ Generates a response for a single user prompt without
169
+ any previous conversation context.
170
+
171
+ Args:
172
+ prompt: User prompt.
173
+
174
+ Returns:
175
+ Detailed response statistics.
176
+ """
177
+ return self.generate_with_history([], prompt)
178
+
179
+ def get_max_tokens(self) -> int:
180
+ """Get the maximum number of tokens that can be generated by the model."""
181
+ return get_max_tokens(self.model)
182
+
183
+ def token_counter(self, messages: list[Message]) -> int:
184
+ """Count the number of tokens in a list of messages."""
185
+ litellm_messages = [{"role": msg.role, "content": str(msg.content)} for msg in messages]
186
+ return token_counter(model=self.model, messages=litellm_messages)
187
+
188
+ def token_counter_with_history(self, messages_history: list[Message], prompt: str) -> int:
189
+ """Count the number of tokens in a list of messages and a prompt."""
190
+ litellm_messages = [{"role": msg.role, "content": str(msg.content)} for msg in messages_history]
191
+ litellm_messages.append({"role": "user", "content": str(prompt)})
192
+ return token_counter(model=self.model, messages=litellm_messages)
193
+
194
+ def get_model_info(self) -> dict | None:
195
+ """Get information about the model."""
196
+ model_info = get_model_info(self.model)
197
+
198
+ if not model_info:
199
+ # Search without prefix "openrouter/"
200
+ model_info = get_model_info(self.model.replace("openrouter/", ""))
201
+
202
+ return model_info
203
+
204
+ def get_model_max_input_tokens(self) -> int:
205
+ """Get the maximum number of input tokens for the model."""
206
+ try:
207
+ model_info = self.get_model_info()
208
+ max_tokens = model_info.get("max_input_tokens") if model_info else None
209
+ return max_tokens
210
+ except Exception as e:
211
+ logger.error(f"Error getting max input tokens for {self.model}: {e}")
212
+ return None
213
+
214
+ def get_model_max_output_tokens(self) -> int | None:
215
+ """Get the maximum number of output tokens for the model."""
216
+ try:
217
+ model_info = self.get_model_info()
218
+ if model_info:
219
+ return model_info.get("max_output_tokens")
220
+
221
+ # Fallback for unmapped models
222
+ logger.warning(f"No max output tokens found for {self.model}. Using default.")
223
+ return 4096 # A reasonable default for many chat models
224
+ except Exception as e:
225
+ logger.error(f"Error getting max output tokens for {self.model}: {e}")
226
+ return None