devorch 0.1.2__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.
- cli/commands/ask.py +0 -0
- cli/commands/edit.py +0 -0
- cli/commands/run.py +0 -0
- cli/main.py +1527 -0
- config/permissions.py +285 -0
- config/profiles.py +0 -0
- config/settings.py +185 -0
- core/agent.py +433 -0
- core/context.py +0 -0
- core/executor.py +213 -0
- core/modes.py +141 -0
- core/planner.py +16 -0
- core/sessions.py +271 -0
- core/tasks.py +142 -0
- devorch-0.1.2.dist-info/METADATA +528 -0
- devorch-0.1.2.dist-info/RECORD +50 -0
- devorch-0.1.2.dist-info/WHEEL +4 -0
- devorch-0.1.2.dist-info/entry_points.txt +2 -0
- providers/__init__.py +130 -0
- providers/anthropic.py +127 -0
- providers/base.py +51 -0
- providers/custom.py +186 -0
- providers/deepseek.py +170 -0
- providers/gemini.py +192 -0
- providers/github_copilot.py +196 -0
- providers/groq.py +158 -0
- providers/kimi.py +181 -0
- providers/lmstudio.py +147 -0
- providers/local.py +214 -0
- providers/mistral.py +161 -0
- providers/openai.py +114 -0
- providers/openrouter.py +195 -0
- providers/together.py +159 -0
- schemas/message.py +32 -0
- schemas/task.py +115 -0
- schemas/tool.py +0 -0
- tools/__init__.py +30 -0
- tools/base.py +40 -0
- tools/edit.py +236 -0
- tools/filesystem.py +224 -0
- tools/git.py +0 -0
- tools/grep.py +280 -0
- tools/search.py +150 -0
- tools/shell.py +55 -0
- tools/task.py +91 -0
- tools/terminal.py +123 -0
- tools/terminal_session.py +298 -0
- tools/tests.py +0 -0
- tools/websearch.py +166 -0
- utils/logger.py +52 -0
providers/gemini.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from google import genai
|
|
4
|
+
from google.genai import types
|
|
5
|
+
|
|
6
|
+
from providers.base import LLMProvider, ModelInfo
|
|
7
|
+
from schemas.message import LLMResponse, Message, ToolCall
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GeminiProvider(LLMProvider):
|
|
11
|
+
"""Google Gemini provider with function calling support using the new google.genai SDK."""
|
|
12
|
+
|
|
13
|
+
name = "gemini"
|
|
14
|
+
|
|
15
|
+
DEFAULT_MODELS = [
|
|
16
|
+
"gemini-2.0-flash",
|
|
17
|
+
"gemini-1.5-pro",
|
|
18
|
+
"gemini-1.5-flash",
|
|
19
|
+
"gemini-1.5-flash-8b",
|
|
20
|
+
"gemini-pro",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
def __init__(self, model: str = "gemini-2.0-flash", api_key: str | None = None):
|
|
24
|
+
self.model_name = model
|
|
25
|
+
self.model = model
|
|
26
|
+
self.client = genai.Client(api_key=api_key)
|
|
27
|
+
|
|
28
|
+
def list_models(self) -> list[ModelInfo]:
|
|
29
|
+
"""Fetch available models from Gemini API."""
|
|
30
|
+
try:
|
|
31
|
+
models = []
|
|
32
|
+
for m in self.client.models.list():
|
|
33
|
+
# Filter for models that support content generation
|
|
34
|
+
if hasattr(m, "supported_actions") and "generateContent" in m.supported_actions:
|
|
35
|
+
model_id = (
|
|
36
|
+
m.name.replace("models/", "") if m.name.startswith("models/") else m.name
|
|
37
|
+
)
|
|
38
|
+
models.append(
|
|
39
|
+
ModelInfo(
|
|
40
|
+
id=model_id,
|
|
41
|
+
name=getattr(m, "display_name", model_id),
|
|
42
|
+
description=getattr(m, "description", ""),
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
return models if models else [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
46
|
+
except Exception:
|
|
47
|
+
return [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
48
|
+
|
|
49
|
+
def generate(
|
|
50
|
+
self,
|
|
51
|
+
messages: list[Message],
|
|
52
|
+
tools: list | None = None,
|
|
53
|
+
stream: bool = False,
|
|
54
|
+
) -> LLMResponse:
|
|
55
|
+
# Build contents list for the API
|
|
56
|
+
contents = []
|
|
57
|
+
system_instruction = None
|
|
58
|
+
|
|
59
|
+
for msg in messages:
|
|
60
|
+
if msg.role == "system":
|
|
61
|
+
system_instruction = msg.content
|
|
62
|
+
elif msg.role == "user":
|
|
63
|
+
contents.append(
|
|
64
|
+
types.Content(role="user", parts=[types.Part.from_text(text=msg.content)])
|
|
65
|
+
)
|
|
66
|
+
elif msg.role == "assistant":
|
|
67
|
+
# Check for function call metadata
|
|
68
|
+
if msg.metadata and msg.metadata.get("function_calls"):
|
|
69
|
+
parts = []
|
|
70
|
+
if msg.content and msg.content != "Calling tool...":
|
|
71
|
+
parts.append(types.Part.from_text(text=msg.content))
|
|
72
|
+
for fc in msg.metadata["function_calls"]:
|
|
73
|
+
parts.append(
|
|
74
|
+
types.Part.from_function_call(name=fc["name"], args=fc["args"])
|
|
75
|
+
)
|
|
76
|
+
contents.append(types.Content(role="model", parts=parts))
|
|
77
|
+
else:
|
|
78
|
+
contents.append(
|
|
79
|
+
types.Content(role="model", parts=[types.Part.from_text(text=msg.content)])
|
|
80
|
+
)
|
|
81
|
+
elif msg.role == "tool":
|
|
82
|
+
# Tool results use function_response
|
|
83
|
+
contents.append(
|
|
84
|
+
types.Content(
|
|
85
|
+
role="user",
|
|
86
|
+
parts=[
|
|
87
|
+
types.Part.from_function_response(
|
|
88
|
+
name=msg.name, response={"result": msg.content}
|
|
89
|
+
)
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Build tools configuration
|
|
95
|
+
gemini_tools = None
|
|
96
|
+
if tools:
|
|
97
|
+
function_declarations = []
|
|
98
|
+
for tool in tools:
|
|
99
|
+
params = tool.get("parameters", {"type": "object", "properties": {}})
|
|
100
|
+
gemini_params = self._convert_params(params)
|
|
101
|
+
function_declarations.append(
|
|
102
|
+
types.FunctionDeclaration(
|
|
103
|
+
name=tool["name"], description=tool["description"], parameters=gemini_params
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
gemini_tools = [types.Tool(function_declarations=function_declarations)]
|
|
107
|
+
|
|
108
|
+
# Build config
|
|
109
|
+
config_kwargs = {}
|
|
110
|
+
if system_instruction:
|
|
111
|
+
config_kwargs["system_instruction"] = system_instruction
|
|
112
|
+
if gemini_tools:
|
|
113
|
+
config_kwargs["tools"] = gemini_tools
|
|
114
|
+
# Disable automatic function calling - we handle it ourselves
|
|
115
|
+
config_kwargs["automatic_function_calling"] = types.AutomaticFunctionCallingConfig(
|
|
116
|
+
disable=True
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
config = types.GenerateContentConfig(**config_kwargs) if config_kwargs else None
|
|
120
|
+
|
|
121
|
+
# Generate response
|
|
122
|
+
try:
|
|
123
|
+
response = self.client.models.generate_content(
|
|
124
|
+
model=self.model_name, contents=contents, config=config
|
|
125
|
+
)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
# If tools fail, try without them
|
|
128
|
+
if gemini_tools and "tool" in str(e).lower():
|
|
129
|
+
config_no_tools = (
|
|
130
|
+
types.GenerateContentConfig(system_instruction=system_instruction)
|
|
131
|
+
if system_instruction
|
|
132
|
+
else None
|
|
133
|
+
)
|
|
134
|
+
response = self.client.models.generate_content(
|
|
135
|
+
model=self.model_name, contents=contents, config=config_no_tools
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
# Parse response
|
|
141
|
+
tool_calls = []
|
|
142
|
+
text_content = ""
|
|
143
|
+
function_call_metadata = []
|
|
144
|
+
|
|
145
|
+
if response.candidates and response.candidates[0].content:
|
|
146
|
+
for part in response.candidates[0].content.parts:
|
|
147
|
+
if hasattr(part, "text") and part.text:
|
|
148
|
+
text_content += part.text
|
|
149
|
+
if hasattr(part, "function_call") and part.function_call:
|
|
150
|
+
fc = part.function_call
|
|
151
|
+
# Convert args to dict
|
|
152
|
+
args = dict(fc.args) if fc.args else {}
|
|
153
|
+
tool_call = ToolCall(
|
|
154
|
+
name=fc.name, arguments=args, id=f"gemini_{fc.name}_{len(tool_calls)}"
|
|
155
|
+
)
|
|
156
|
+
tool_calls.append(tool_call)
|
|
157
|
+
function_call_metadata.append({"name": fc.name, "args": args})
|
|
158
|
+
|
|
159
|
+
content = text_content if text_content else ("Calling tool..." if tool_calls else "")
|
|
160
|
+
|
|
161
|
+
# Store function calls in metadata for history reconstruction
|
|
162
|
+
metadata = {"function_calls": function_call_metadata} if function_call_metadata else None
|
|
163
|
+
|
|
164
|
+
return LLMResponse(
|
|
165
|
+
message=Message(role="assistant", content=content, metadata=metadata),
|
|
166
|
+
tool_calls=tool_calls if tool_calls else None,
|
|
167
|
+
raw=response,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _convert_params(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
171
|
+
"""Convert JSON Schema parameters to Gemini format."""
|
|
172
|
+
result = {}
|
|
173
|
+
|
|
174
|
+
if "type" in params:
|
|
175
|
+
result["type"] = params["type"].upper()
|
|
176
|
+
|
|
177
|
+
if "properties" in params:
|
|
178
|
+
result["properties"] = {}
|
|
179
|
+
for key, value in params["properties"].items():
|
|
180
|
+
prop = {}
|
|
181
|
+
if "type" in value:
|
|
182
|
+
prop["type"] = value["type"].upper()
|
|
183
|
+
if "description" in value:
|
|
184
|
+
prop["description"] = value["description"]
|
|
185
|
+
if "enum" in value:
|
|
186
|
+
prop["enum"] = value["enum"]
|
|
187
|
+
result["properties"][key] = prop
|
|
188
|
+
|
|
189
|
+
if "required" in params:
|
|
190
|
+
result["required"] = params["required"]
|
|
191
|
+
|
|
192
|
+
return result
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub Copilot provider using the GitHub Copilot API.
|
|
3
|
+
|
|
4
|
+
Requires GitHub Copilot subscription and authentication.
|
|
5
|
+
Uses OpenAI-compatible API with GitHub's models.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from openai import OpenAI
|
|
12
|
+
|
|
13
|
+
from providers.base import LLMProvider, ModelInfo
|
|
14
|
+
from schemas.message import LLMResponse, Message, ToolCall
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GitHubCopilotProvider(LLMProvider):
|
|
18
|
+
name = "github_copilot"
|
|
19
|
+
|
|
20
|
+
DEFAULT_MODELS = [
|
|
21
|
+
"gpt-4o",
|
|
22
|
+
"gpt-4o-mini",
|
|
23
|
+
"claude-3.5-sonnet",
|
|
24
|
+
"o1-preview",
|
|
25
|
+
"o1-mini",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
model: str = "gpt-4o",
|
|
31
|
+
api_key: str | None = None,
|
|
32
|
+
base_url: str | None = None,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Initialize GitHub Copilot provider.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
model: Model to use (gpt-4o, claude-3.5-sonnet, etc.)
|
|
39
|
+
api_key: GitHub token (defaults to GITHUB_TOKEN env var)
|
|
40
|
+
base_url: Base URL for the API (defaults to GitHub Copilot API)
|
|
41
|
+
"""
|
|
42
|
+
# GitHub Copilot uses GitHub tokens, not OpenAI keys
|
|
43
|
+
token = api_key or os.getenv("GITHUB_TOKEN")
|
|
44
|
+
if not token:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"GitHub token required. Set GITHUB_TOKEN env var or pass api_key parameter."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# GitHub Copilot API endpoint
|
|
50
|
+
base = base_url or "https://api.githubcopilot.com"
|
|
51
|
+
|
|
52
|
+
self.client = OpenAI(
|
|
53
|
+
api_key=token,
|
|
54
|
+
base_url=base,
|
|
55
|
+
)
|
|
56
|
+
self.model = model
|
|
57
|
+
|
|
58
|
+
def list_models(self) -> list[ModelInfo]:
|
|
59
|
+
"""Fetch available models from GitHub Copilot API."""
|
|
60
|
+
try:
|
|
61
|
+
response = self.client.models.list()
|
|
62
|
+
models = []
|
|
63
|
+
for model in response.data:
|
|
64
|
+
models.append(
|
|
65
|
+
ModelInfo(
|
|
66
|
+
id=model.id,
|
|
67
|
+
name=model.id,
|
|
68
|
+
description=getattr(model, "description", None),
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
return models if models else self._get_default_models()
|
|
72
|
+
except Exception:
|
|
73
|
+
# GitHub Copilot API might not support model listing
|
|
74
|
+
# Fall back to known available models
|
|
75
|
+
return self._get_default_models()
|
|
76
|
+
|
|
77
|
+
def _get_default_models(self) -> list[ModelInfo]:
|
|
78
|
+
"""Get hardcoded default models as fallback."""
|
|
79
|
+
return [
|
|
80
|
+
ModelInfo(
|
|
81
|
+
id="gpt-4o",
|
|
82
|
+
name="GPT-4o",
|
|
83
|
+
description="OpenAI's GPT-4o via GitHub Copilot",
|
|
84
|
+
),
|
|
85
|
+
ModelInfo(
|
|
86
|
+
id="gpt-4o-mini",
|
|
87
|
+
name="GPT-4o Mini",
|
|
88
|
+
description="Faster, cheaper GPT-4o variant",
|
|
89
|
+
),
|
|
90
|
+
ModelInfo(
|
|
91
|
+
id="claude-3.5-sonnet",
|
|
92
|
+
name="Claude 3.5 Sonnet",
|
|
93
|
+
description="Anthropic's Claude 3.5 Sonnet via GitHub Copilot",
|
|
94
|
+
),
|
|
95
|
+
ModelInfo(
|
|
96
|
+
id="o1-preview",
|
|
97
|
+
name="OpenAI o1 Preview",
|
|
98
|
+
description="OpenAI's o1 reasoning model (preview)",
|
|
99
|
+
),
|
|
100
|
+
ModelInfo(
|
|
101
|
+
id="o1-mini",
|
|
102
|
+
name="OpenAI o1 Mini",
|
|
103
|
+
description="Smaller o1 reasoning model",
|
|
104
|
+
),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
def generate(
|
|
108
|
+
self,
|
|
109
|
+
messages: list[Message],
|
|
110
|
+
tools: list | None = None,
|
|
111
|
+
stream: bool = False,
|
|
112
|
+
) -> LLMResponse:
|
|
113
|
+
# Format messages for OpenAI-compatible API
|
|
114
|
+
formatted_messages = []
|
|
115
|
+
for msg in messages:
|
|
116
|
+
if msg.role == "tool":
|
|
117
|
+
formatted_msg = {
|
|
118
|
+
"role": "tool",
|
|
119
|
+
"content": msg.content,
|
|
120
|
+
"tool_call_id": msg.tool_call_id or msg.name,
|
|
121
|
+
}
|
|
122
|
+
elif msg.role == "assistant" and msg.metadata and msg.metadata.get("tool_calls"):
|
|
123
|
+
# Preserve tool_calls in assistant messages
|
|
124
|
+
formatted_msg = {
|
|
125
|
+
"role": "assistant",
|
|
126
|
+
"content": msg.content or "",
|
|
127
|
+
"tool_calls": msg.metadata["tool_calls"],
|
|
128
|
+
}
|
|
129
|
+
else:
|
|
130
|
+
formatted_msg = {"role": msg.role, "content": msg.content}
|
|
131
|
+
formatted_messages.append(formatted_msg)
|
|
132
|
+
|
|
133
|
+
# Format tools for OpenAI-compatible API
|
|
134
|
+
formatted_tools = None
|
|
135
|
+
if tools:
|
|
136
|
+
formatted_tools = []
|
|
137
|
+
for tool in tools:
|
|
138
|
+
formatted_tools.append(
|
|
139
|
+
{
|
|
140
|
+
"type": "function",
|
|
141
|
+
"function": {
|
|
142
|
+
"name": tool["name"],
|
|
143
|
+
"description": tool["description"],
|
|
144
|
+
"parameters": tool.get(
|
|
145
|
+
"parameters", {"type": "object", "properties": {}}
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
response = self.client.chat.completions.create(
|
|
152
|
+
model=self.model,
|
|
153
|
+
messages=formatted_messages,
|
|
154
|
+
tools=formatted_tools,
|
|
155
|
+
temperature=0.0,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
choice = response.choices[0]
|
|
159
|
+
message = choice.message
|
|
160
|
+
|
|
161
|
+
tool_calls = []
|
|
162
|
+
if message.tool_calls:
|
|
163
|
+
for tc in message.tool_calls:
|
|
164
|
+
tool_calls.append(
|
|
165
|
+
ToolCall(
|
|
166
|
+
id=tc.id,
|
|
167
|
+
name=tc.function.name,
|
|
168
|
+
arguments=json.loads(tc.function.arguments),
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return LLMResponse(
|
|
173
|
+
content=message.content or "",
|
|
174
|
+
tool_calls=tool_calls,
|
|
175
|
+
metadata={
|
|
176
|
+
"model": response.model,
|
|
177
|
+
"usage": {
|
|
178
|
+
"prompt_tokens": response.usage.prompt_tokens if response.usage else 0,
|
|
179
|
+
"completion_tokens": (
|
|
180
|
+
response.usage.completion_tokens if response.usage else 0
|
|
181
|
+
),
|
|
182
|
+
"total_tokens": response.usage.total_tokens if response.usage else 0,
|
|
183
|
+
},
|
|
184
|
+
"tool_calls": [
|
|
185
|
+
{
|
|
186
|
+
"id": tc.id,
|
|
187
|
+
"type": "function",
|
|
188
|
+
"function": {
|
|
189
|
+
"name": tc.function.name,
|
|
190
|
+
"arguments": tc.function.arguments,
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
for tc in (message.tool_calls or [])
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
)
|
providers/groq.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Groq Provider - Fast AI inference
|
|
3
|
+
https://groq.com/
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from providers.base import LLMProvider, ModelInfo
|
|
11
|
+
from schemas.message import LLMResponse, Message, ToolCall
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GroqProvider(LLMProvider):
|
|
15
|
+
"""
|
|
16
|
+
Groq provider for ultra-fast inference.
|
|
17
|
+
Supports Llama, Mixtral, and Gemma models.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name = "groq"
|
|
21
|
+
|
|
22
|
+
DEFAULT_MODELS = [
|
|
23
|
+
"llama-3.1-70b-versatile",
|
|
24
|
+
"llama-3.1-8b-instant",
|
|
25
|
+
"llama3-70b-8192",
|
|
26
|
+
"llama3-8b-8192",
|
|
27
|
+
"mixtral-8x7b-32768",
|
|
28
|
+
"gemma2-9b-it",
|
|
29
|
+
"gemma-7b-it",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
BASE_URL = "https://api.groq.com/openai/v1"
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
model: str = "llama-3.1-70b-versatile",
|
|
37
|
+
api_key: str | None = None,
|
|
38
|
+
):
|
|
39
|
+
self.model = model
|
|
40
|
+
self.api_key = api_key
|
|
41
|
+
self.client = httpx.Client(timeout=120.0)
|
|
42
|
+
|
|
43
|
+
def _get_headers(self) -> dict:
|
|
44
|
+
return {
|
|
45
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def list_models(self) -> list[ModelInfo]:
|
|
50
|
+
"""Fetch available models from Groq API."""
|
|
51
|
+
try:
|
|
52
|
+
response = self.client.get(f"{self.BASE_URL}/models", headers=self._get_headers())
|
|
53
|
+
response.raise_for_status()
|
|
54
|
+
data = response.json()
|
|
55
|
+
|
|
56
|
+
models = []
|
|
57
|
+
for model in data.get("data", []):
|
|
58
|
+
# Filter to active models
|
|
59
|
+
if model.get("active", True):
|
|
60
|
+
models.append(
|
|
61
|
+
ModelInfo(
|
|
62
|
+
id=model.get("id"),
|
|
63
|
+
name=model.get("id"),
|
|
64
|
+
context_length=model.get("context_window"),
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
models.sort(key=lambda m: m.id)
|
|
69
|
+
return models if models else [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
70
|
+
|
|
71
|
+
except Exception:
|
|
72
|
+
return [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
73
|
+
|
|
74
|
+
def generate(
|
|
75
|
+
self,
|
|
76
|
+
messages: list[Message],
|
|
77
|
+
tools: list | None = None,
|
|
78
|
+
stream: bool = False,
|
|
79
|
+
) -> LLMResponse:
|
|
80
|
+
# Format messages for Groq API
|
|
81
|
+
formatted_messages = []
|
|
82
|
+
for msg in messages:
|
|
83
|
+
if msg.role == "tool":
|
|
84
|
+
formatted_msg = {
|
|
85
|
+
"role": "tool",
|
|
86
|
+
"content": msg.content,
|
|
87
|
+
"tool_call_id": msg.tool_call_id or msg.name,
|
|
88
|
+
}
|
|
89
|
+
elif msg.role == "assistant" and msg.metadata and msg.metadata.get("tool_calls"):
|
|
90
|
+
# Preserve tool_calls in assistant messages
|
|
91
|
+
formatted_msg = {
|
|
92
|
+
"role": "assistant",
|
|
93
|
+
"content": msg.content or "",
|
|
94
|
+
"tool_calls": msg.metadata["tool_calls"],
|
|
95
|
+
}
|
|
96
|
+
else:
|
|
97
|
+
formatted_msg = {"role": msg.role, "content": msg.content}
|
|
98
|
+
formatted_messages.append(formatted_msg)
|
|
99
|
+
|
|
100
|
+
# Format tools
|
|
101
|
+
formatted_tools = None
|
|
102
|
+
if tools:
|
|
103
|
+
formatted_tools = []
|
|
104
|
+
for tool in tools:
|
|
105
|
+
formatted_tools.append(
|
|
106
|
+
{
|
|
107
|
+
"type": "function",
|
|
108
|
+
"function": {
|
|
109
|
+
"name": tool["name"],
|
|
110
|
+
"description": tool["description"],
|
|
111
|
+
"parameters": tool.get(
|
|
112
|
+
"parameters", {"type": "object", "properties": {}}
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
payload = {
|
|
119
|
+
"model": self.model,
|
|
120
|
+
"messages": formatted_messages,
|
|
121
|
+
"temperature": 0.0,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if formatted_tools:
|
|
125
|
+
payload["tools"] = formatted_tools
|
|
126
|
+
payload["tool_choice"] = "auto"
|
|
127
|
+
|
|
128
|
+
response = self.client.post(
|
|
129
|
+
f"{self.BASE_URL}/chat/completions", headers=self._get_headers(), json=payload
|
|
130
|
+
)
|
|
131
|
+
response.raise_for_status()
|
|
132
|
+
data = response.json()
|
|
133
|
+
|
|
134
|
+
choice = data["choices"][0]
|
|
135
|
+
message = choice["message"]
|
|
136
|
+
|
|
137
|
+
tool_calls = []
|
|
138
|
+
if message.get("tool_calls"):
|
|
139
|
+
for tc in message["tool_calls"]:
|
|
140
|
+
args = (
|
|
141
|
+
json.loads(tc["function"]["arguments"])
|
|
142
|
+
if tc["function"].get("arguments")
|
|
143
|
+
else {}
|
|
144
|
+
)
|
|
145
|
+
tool_call = ToolCall(
|
|
146
|
+
name=tc["function"]["name"],
|
|
147
|
+
arguments=args,
|
|
148
|
+
id=tc.get("id", tc["function"]["name"]),
|
|
149
|
+
)
|
|
150
|
+
tool_calls.append(tool_call)
|
|
151
|
+
|
|
152
|
+
content = message.get("content") or "Calling tool..."
|
|
153
|
+
|
|
154
|
+
return LLMResponse(
|
|
155
|
+
message=Message(role="assistant", content=content),
|
|
156
|
+
tool_calls=tool_calls if tool_calls else None,
|
|
157
|
+
raw=data,
|
|
158
|
+
)
|