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.
- quantalogic/__init__.py +20 -0
- quantalogic/agent.py +638 -0
- quantalogic/agent_config.py +138 -0
- quantalogic/coding_agent.py +83 -0
- quantalogic/event_emitter.py +223 -0
- quantalogic/generative_model.py +226 -0
- quantalogic/interactive_text_editor.py +190 -0
- quantalogic/main.py +185 -0
- quantalogic/memory.py +217 -0
- quantalogic/model_names.py +19 -0
- quantalogic/print_event.py +66 -0
- quantalogic/prompts.py +99 -0
- quantalogic/server/__init__.py +3 -0
- quantalogic/server/agent_server.py +633 -0
- quantalogic/server/models.py +60 -0
- quantalogic/server/routes.py +117 -0
- quantalogic/server/state.py +199 -0
- quantalogic/server/static/js/event_visualizer.js +430 -0
- quantalogic/server/static/js/quantalogic.js +571 -0
- quantalogic/server/templates/index.html +134 -0
- quantalogic/tool_manager.py +68 -0
- quantalogic/tools/__init__.py +46 -0
- quantalogic/tools/agent_tool.py +88 -0
- quantalogic/tools/download_http_file_tool.py +64 -0
- quantalogic/tools/edit_whole_content_tool.py +70 -0
- quantalogic/tools/elixir_tool.py +240 -0
- quantalogic/tools/execute_bash_command_tool.py +116 -0
- quantalogic/tools/input_question_tool.py +57 -0
- quantalogic/tools/language_handlers/__init__.py +21 -0
- quantalogic/tools/language_handlers/c_handler.py +33 -0
- quantalogic/tools/language_handlers/cpp_handler.py +33 -0
- quantalogic/tools/language_handlers/go_handler.py +33 -0
- quantalogic/tools/language_handlers/java_handler.py +37 -0
- quantalogic/tools/language_handlers/javascript_handler.py +42 -0
- quantalogic/tools/language_handlers/python_handler.py +29 -0
- quantalogic/tools/language_handlers/rust_handler.py +33 -0
- quantalogic/tools/language_handlers/scala_handler.py +33 -0
- quantalogic/tools/language_handlers/typescript_handler.py +42 -0
- quantalogic/tools/list_directory_tool.py +123 -0
- quantalogic/tools/llm_tool.py +119 -0
- quantalogic/tools/markitdown_tool.py +105 -0
- quantalogic/tools/nodejs_tool.py +515 -0
- quantalogic/tools/python_tool.py +469 -0
- quantalogic/tools/read_file_block_tool.py +140 -0
- quantalogic/tools/read_file_tool.py +79 -0
- quantalogic/tools/replace_in_file_tool.py +300 -0
- quantalogic/tools/ripgrep_tool.py +353 -0
- quantalogic/tools/search_definition_names.py +419 -0
- quantalogic/tools/task_complete_tool.py +35 -0
- quantalogic/tools/tool.py +146 -0
- quantalogic/tools/unified_diff_tool.py +387 -0
- quantalogic/tools/write_file_tool.py +97 -0
- quantalogic/utils/__init__.py +17 -0
- quantalogic/utils/ask_user_validation.py +12 -0
- quantalogic/utils/download_http_file.py +77 -0
- quantalogic/utils/get_coding_environment.py +15 -0
- quantalogic/utils/get_environment.py +26 -0
- quantalogic/utils/get_quantalogic_rules_content.py +19 -0
- quantalogic/utils/git_ls.py +121 -0
- quantalogic/utils/read_file.py +54 -0
- quantalogic/utils/read_http_text_content.py +101 -0
- quantalogic/xml_parser.py +242 -0
- quantalogic/xml_tool_parser.py +99 -0
- quantalogic-0.2.0.dist-info/LICENSE +201 -0
- quantalogic-0.2.0.dist-info/METADATA +1034 -0
- quantalogic-0.2.0.dist-info/RECORD +68 -0
- quantalogic-0.2.0.dist-info/WHEEL +4 -0
- 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
|