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.
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
+ )