gemini-agent-framework 0.1.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.
gemini_agent/__init__.py
ADDED
gemini_agent/agent.py
ADDED
@@ -0,0 +1,372 @@
|
|
1
|
+
import requests
|
2
|
+
import json
|
3
|
+
import inspect
|
4
|
+
from functools import wraps
|
5
|
+
from typing import List, Callable, Dict, Any, Optional
|
6
|
+
from dotenv import load_dotenv
|
7
|
+
|
8
|
+
load_dotenv()
|
9
|
+
|
10
|
+
class Agent:
|
11
|
+
PYTHON_TO_GEMINI_TYPE_MAP = {
|
12
|
+
str: "STRING",
|
13
|
+
int: "INTEGER",
|
14
|
+
float: "NUMBER",
|
15
|
+
bool: "BOOLEAN",
|
16
|
+
list: "ARRAY",
|
17
|
+
dict: "OBJECT",
|
18
|
+
}
|
19
|
+
_tools_registry: Dict[str, Dict[str, Any]] = {} # Class-level registry
|
20
|
+
|
21
|
+
def get_gemini_type(self, py_type: type) -> str:
|
22
|
+
"""Maps Python types to Gemini JSON schema types."""
|
23
|
+
return self.PYTHON_TO_GEMINI_TYPE_MAP.get(py_type, "STRING") # Default to STRING if unknown
|
24
|
+
|
25
|
+
@staticmethod
|
26
|
+
def description(desc: str):
|
27
|
+
"""Decorator to add a description to a tool function."""
|
28
|
+
def decorator(func):
|
29
|
+
if func.__name__ not in Agent._tools_registry:
|
30
|
+
Agent._tools_registry[func.__name__] = {}
|
31
|
+
Agent._tools_registry[func.__name__]['description'] = desc
|
32
|
+
Agent._tools_registry[func.__name__]['signature'] = inspect.signature(func)
|
33
|
+
Agent._tools_registry[func.__name__]['function_ref'] = func
|
34
|
+
@wraps(func)
|
35
|
+
def wrapper(*args, **kwargs):
|
36
|
+
return func(*args, **kwargs)
|
37
|
+
return wrapper
|
38
|
+
return decorator
|
39
|
+
|
40
|
+
@staticmethod
|
41
|
+
def parameters(params: Dict[str, Dict[str, Any]]):
|
42
|
+
"""Decorator to define parameters for a tool function."""
|
43
|
+
def decorator(func):
|
44
|
+
if func.__name__ not in Agent._tools_registry:
|
45
|
+
Agent._tools_registry[func.__name__] = {}
|
46
|
+
Agent._tools_registry[func.__name__]['parameters_def'] = params
|
47
|
+
if 'signature' not in Agent._tools_registry[func.__name__]:
|
48
|
+
Agent._tools_registry[func.__name__]['signature'] = inspect.signature(func)
|
49
|
+
if 'function_ref' not in Agent._tools_registry[func.__name__]:
|
50
|
+
Agent._tools_registry[func.__name__]['function_ref'] = func
|
51
|
+
@wraps(func)
|
52
|
+
def wrapper(*args, **kwargs):
|
53
|
+
return func(*args, **kwargs)
|
54
|
+
return wrapper
|
55
|
+
return decorator
|
56
|
+
|
57
|
+
def __init__(self, api_key: str, tools: List[Callable] = None, model_name: str = "gemini-1.5-flash"):
|
58
|
+
"""
|
59
|
+
Initializes the Agent using REST API calls.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
api_key: Your Google Generative AI API key.
|
63
|
+
tools: A list of Python functions decorated as tools.
|
64
|
+
model_name: The name of the Gemini model to use.
|
65
|
+
"""
|
66
|
+
if not api_key:
|
67
|
+
raise ValueError("API key is required.")
|
68
|
+
self.api_key = api_key
|
69
|
+
self.model_name = model_name
|
70
|
+
self.base_url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model_name}"
|
71
|
+
self.headers = {'Content-Type': 'application/json'}
|
72
|
+
|
73
|
+
self._registered_tools_json: List[Dict[str, Any]] = [] # Store JSON representation
|
74
|
+
self._tool_functions: Dict[str, Callable] = {} # Map name to actual function
|
75
|
+
self._intermediate_results: Dict[str, Any] = {} # Store intermediate results
|
76
|
+
|
77
|
+
if tools:
|
78
|
+
self._process_tools(tools)
|
79
|
+
|
80
|
+
def _process_tools(self, tools: List[Callable]):
|
81
|
+
"""Converts decorated Python functions into the JSON format for the REST API."""
|
82
|
+
for func in tools:
|
83
|
+
tool_name = func.__name__
|
84
|
+
if tool_name not in Agent._tools_registry:
|
85
|
+
print(f"Warning: Function '{tool_name}' was passed but has no @Agent decorators. Skipping.")
|
86
|
+
continue
|
87
|
+
|
88
|
+
metadata = Agent._tools_registry[tool_name]
|
89
|
+
if 'description' not in metadata:
|
90
|
+
print(f"Warning: Function '{tool_name}' is missing @Agent.description. Skipping.")
|
91
|
+
continue
|
92
|
+
|
93
|
+
# Build the parameters schema JSON
|
94
|
+
gemini_params_schema = {
|
95
|
+
"type": "OBJECT",
|
96
|
+
"properties": {},
|
97
|
+
"required": []
|
98
|
+
}
|
99
|
+
params_def = metadata.get('parameters_def', {})
|
100
|
+
signature = metadata.get('signature') # inspect.signature object
|
101
|
+
|
102
|
+
if not params_def and signature:
|
103
|
+
params_def = {}
|
104
|
+
for name, param in signature.parameters.items():
|
105
|
+
py_type = param.annotation if param.annotation != inspect.Parameter.empty else str
|
106
|
+
params_def[name] = {'type': py_type, 'description': f'Parameter {name}'}
|
107
|
+
|
108
|
+
for name, definition in params_def.items():
|
109
|
+
py_type = definition.get('type', str)
|
110
|
+
gemini_type = self.get_gemini_type(py_type)
|
111
|
+
gemini_params_schema["properties"][name] = {
|
112
|
+
"type": gemini_type,
|
113
|
+
"description": definition.get('description', '')
|
114
|
+
}
|
115
|
+
if signature and signature.parameters[name].default == inspect.Parameter.empty:
|
116
|
+
gemini_params_schema["required"].append(name)
|
117
|
+
|
118
|
+
# Create the Function Declaration JSON dictionary
|
119
|
+
declaration_json = {
|
120
|
+
"name": tool_name,
|
121
|
+
"description": metadata['description'],
|
122
|
+
"parameters": gemini_params_schema if gemini_params_schema["properties"] else None
|
123
|
+
}
|
124
|
+
if declaration_json["parameters"] is None:
|
125
|
+
del declaration_json["parameters"]
|
126
|
+
|
127
|
+
self._registered_tools_json.append(declaration_json)
|
128
|
+
self._tool_functions[tool_name] = metadata['function_ref']
|
129
|
+
|
130
|
+
def _get_system_prompt(self) -> str:
|
131
|
+
"""Returns a system prompt that guides the model in breaking down complex operations."""
|
132
|
+
return """You are an AI assistant that can break down complex tasks into sequential steps using available tools.
|
133
|
+
When faced with a complex request:
|
134
|
+
1. Analyze the request to identify which tools can be used
|
135
|
+
2. Break down the request into sequential steps
|
136
|
+
3. For each step:
|
137
|
+
- Use the most appropriate tool
|
138
|
+
- Store the result for use in subsequent steps
|
139
|
+
4. Combine the results to provide a final answer
|
140
|
+
|
141
|
+
Available tools:
|
142
|
+
{tools_list}
|
143
|
+
|
144
|
+
Remember:
|
145
|
+
- Always perform one operation at a time
|
146
|
+
- Use intermediate results from previous steps
|
147
|
+
- If a step requires multiple tools, execute them sequentially
|
148
|
+
- If you're unsure about the next step, explain your reasoning
|
149
|
+
""".format(tools_list="\n".join([f"- {name}: {desc}" for name, desc in
|
150
|
+
[(tool['name'], tool['description']) for tool in self._registered_tools_json]]))
|
151
|
+
|
152
|
+
def _call_gemini_api(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
153
|
+
"""Makes a POST request to the Gemini generateContent endpoint."""
|
154
|
+
api_url = f"{self.base_url}:generateContent?key={self.api_key}"
|
155
|
+
try:
|
156
|
+
response = requests.post(api_url, headers=self.headers, json=payload)
|
157
|
+
response.raise_for_status()
|
158
|
+
return response.json()
|
159
|
+
except requests.exceptions.RequestException as e:
|
160
|
+
print(f"Error calling Gemini API: {e}")
|
161
|
+
error_message = f"API Request Error: {e}"
|
162
|
+
if hasattr(e, 'response') and e.response is not None:
|
163
|
+
try:
|
164
|
+
error_details = e.response.json()
|
165
|
+
error_message += f"\nDetails: {json.dumps(error_details)}"
|
166
|
+
except json.JSONDecodeError:
|
167
|
+
error_message += f"\nResponse Body: {e.response.text}"
|
168
|
+
return {"error": {"message": error_message}}
|
169
|
+
except json.JSONDecodeError as e:
|
170
|
+
print(f"Error decoding Gemini API JSON response: {e}")
|
171
|
+
return {"error": {"message": f"JSON Decode Error: {e}"}}
|
172
|
+
|
173
|
+
def prompt(self,
|
174
|
+
user_prompt: str,
|
175
|
+
system_prompt: Optional[str] = None,
|
176
|
+
response_structure: Optional[Dict[str, Any]] = None,
|
177
|
+
conversation_history: Optional[List[Dict[str, Any]]] = None
|
178
|
+
) -> Any:
|
179
|
+
"""
|
180
|
+
Sends a prompt via REST API, handles function calling, and respects response structure.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
user_prompt: The user's message.
|
184
|
+
system_prompt: An optional system instruction (prepended to history).
|
185
|
+
response_structure: An optional OpenAPI schema dict for desired output format.
|
186
|
+
conversation_history: Optional list of content dicts (e.g., [{'role':'user', 'parts':...}])
|
187
|
+
|
188
|
+
Returns:
|
189
|
+
The model's final response (string or dict/list if structured),
|
190
|
+
or a dictionary containing an 'error' key if something failed.
|
191
|
+
"""
|
192
|
+
self._intermediate_results = {}
|
193
|
+
|
194
|
+
if not system_prompt:
|
195
|
+
system_prompt = self._get_system_prompt()
|
196
|
+
|
197
|
+
current_contents = conversation_history if conversation_history else []
|
198
|
+
if system_prompt and not current_contents:
|
199
|
+
current_contents.append({'role': 'user', 'parts': [{'text': system_prompt}]})
|
200
|
+
current_contents.append({'role': 'model', 'parts': [{'text': "I understand I should break down complex tasks into sequential steps using the available tools."}]})
|
201
|
+
|
202
|
+
current_contents.append({'role': 'user', 'parts': [{'text': user_prompt}]})
|
203
|
+
payload: Dict[str, Any] = {"contents": current_contents}
|
204
|
+
|
205
|
+
if self._registered_tools_json:
|
206
|
+
payload["tools"] = [{"functionDeclarations": self._registered_tools_json}]
|
207
|
+
payload["toolConfig"] = {"functionCallingConfig": {"mode": "AUTO"}}
|
208
|
+
|
209
|
+
apply_structure_later = bool(response_structure) and bool(self._registered_tools_json)
|
210
|
+
final_response_schema = None
|
211
|
+
final_mime_type = None
|
212
|
+
|
213
|
+
if response_structure and not self._registered_tools_json:
|
214
|
+
apply_structure_later = False
|
215
|
+
payload["generationConfig"] = {
|
216
|
+
"response_mime_type": "application/json",
|
217
|
+
"response_schema": response_structure
|
218
|
+
}
|
219
|
+
final_mime_type = "application/json"
|
220
|
+
final_response_schema = response_structure
|
221
|
+
|
222
|
+
while True:
|
223
|
+
response_data = self._call_gemini_api(payload)
|
224
|
+
|
225
|
+
if "error" in response_data:
|
226
|
+
print(f"API call failed: {response_data['error'].get('message', 'Unknown API error')}")
|
227
|
+
return response_data
|
228
|
+
|
229
|
+
if not response_data.get("candidates"):
|
230
|
+
feedback = response_data.get("promptFeedback")
|
231
|
+
block_reason = feedback.get("blockReason") if feedback else "Unknown"
|
232
|
+
safety_ratings = feedback.get("safetyRatings") if feedback else []
|
233
|
+
error_msg = f"Request blocked by API. Reason: {block_reason}."
|
234
|
+
if safety_ratings:
|
235
|
+
error_msg += f" Details: {json.dumps(safety_ratings)}"
|
236
|
+
print(error_msg)
|
237
|
+
return {"error": {"message": error_msg, "details": feedback}}
|
238
|
+
|
239
|
+
try:
|
240
|
+
candidate = response_data["candidates"][0]
|
241
|
+
content = candidate["content"]
|
242
|
+
|
243
|
+
for part in content["parts"]:
|
244
|
+
payload["contents"].append({"role": "model", "parts": [part]})
|
245
|
+
|
246
|
+
if "functionCall" in part:
|
247
|
+
fc = part["functionCall"]
|
248
|
+
tool_name = fc["name"]
|
249
|
+
args = fc.get("args", {})
|
250
|
+
|
251
|
+
if tool_name not in self._tool_functions:
|
252
|
+
error_msg = f"Model attempted to call unknown function '{tool_name}'."
|
253
|
+
print(f"Error: {error_msg}")
|
254
|
+
error_response_part = {
|
255
|
+
"functionResponse": {
|
256
|
+
"name": tool_name,
|
257
|
+
"response": {"error": error_msg}
|
258
|
+
}
|
259
|
+
}
|
260
|
+
payload["contents"].append({"role": "user", "parts": [error_response_part]})
|
261
|
+
continue
|
262
|
+
|
263
|
+
try:
|
264
|
+
tool_function = self._tool_functions[tool_name]
|
265
|
+
print(f"--- Calling Function: {tool_name}({args}) ---")
|
266
|
+
|
267
|
+
for key, value in args.items():
|
268
|
+
if isinstance(value, str) and value.startswith('$'):
|
269
|
+
result_key = value[1:]
|
270
|
+
if result_key in self._intermediate_results:
|
271
|
+
args[key] = self._intermediate_results[result_key]
|
272
|
+
|
273
|
+
function_result = tool_function(**args)
|
274
|
+
print(f"--- Function Result: {function_result} ---")
|
275
|
+
|
276
|
+
result_key = f"result_{len(self._intermediate_results)}"
|
277
|
+
self._intermediate_results[result_key] = function_result
|
278
|
+
|
279
|
+
function_response_part = {
|
280
|
+
"functionResponse": {
|
281
|
+
"name": tool_name,
|
282
|
+
"response": {
|
283
|
+
"content": function_result,
|
284
|
+
"result_key": result_key
|
285
|
+
}
|
286
|
+
}
|
287
|
+
}
|
288
|
+
payload["contents"].append({"role": "user", "parts": [function_response_part]})
|
289
|
+
|
290
|
+
except Exception as e:
|
291
|
+
print(f"Error executing function {tool_name}: {e}")
|
292
|
+
error_msg = f"Error during execution of tool '{tool_name}': {e}"
|
293
|
+
error_response_part = {
|
294
|
+
"functionResponse": {
|
295
|
+
"name": tool_name,
|
296
|
+
"response": {"error": error_msg}
|
297
|
+
}
|
298
|
+
}
|
299
|
+
payload["contents"].append({"role": "user", "parts": [error_response_part]})
|
300
|
+
continue
|
301
|
+
|
302
|
+
elif "text" in part:
|
303
|
+
final_text = part["text"]
|
304
|
+
|
305
|
+
if final_mime_type == "application/json" and final_response_schema:
|
306
|
+
try:
|
307
|
+
structured_output = json.loads(final_text)
|
308
|
+
if not any("functionCall" in p for p in content["parts"][content["parts"].index(part) + 1:]):
|
309
|
+
return structured_output
|
310
|
+
except json.JSONDecodeError as e:
|
311
|
+
print(f"Warning: Failed to parse initially structured output: {e}. Continuing with raw text.")
|
312
|
+
|
313
|
+
elif apply_structure_later:
|
314
|
+
if not any("functionCall" in p for p in content["parts"][content["parts"].index(part) + 1:]):
|
315
|
+
print("--- Attempting final structuring call ---")
|
316
|
+
formatting_payload = {
|
317
|
+
"contents": [
|
318
|
+
{"role": "user", "parts": [{"text": f"Please format the following information according to the requested JSON structure:\n\n{final_text}" }]}
|
319
|
+
],
|
320
|
+
"generationConfig": {
|
321
|
+
"response_mime_type": "application/json",
|
322
|
+
"response_schema": response_structure
|
323
|
+
}
|
324
|
+
}
|
325
|
+
structured_response_data = self._call_gemini_api(formatting_payload)
|
326
|
+
|
327
|
+
if "error" in structured_response_data:
|
328
|
+
print(f"Structuring call failed: {structured_response_data['error']}. Returning intermediate text.")
|
329
|
+
return final_text
|
330
|
+
|
331
|
+
try:
|
332
|
+
structured_text = structured_response_data["candidates"][0]["content"]["parts"][0]["text"]
|
333
|
+
structured_output = json.loads(structured_text)
|
334
|
+
return structured_output
|
335
|
+
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
336
|
+
print(f"Warning: Failed to parse structured output after formatting call: {e}. Returning intermediate text.")
|
337
|
+
return final_text
|
338
|
+
|
339
|
+
elif not any("functionCall" in p for p in content["parts"][content["parts"].index(part) + 1:]):
|
340
|
+
if response_structure and not apply_structure_later:
|
341
|
+
print("--- Attempting final structuring call ---")
|
342
|
+
formatting_payload = {
|
343
|
+
"contents": [
|
344
|
+
{"role": "user", "parts": [{"text": f"Please format the following information according to the requested JSON structure:\n\n{final_text}" }]}
|
345
|
+
],
|
346
|
+
"generationConfig": {
|
347
|
+
"response_mime_type": "application/json",
|
348
|
+
"response_schema": response_structure
|
349
|
+
}
|
350
|
+
}
|
351
|
+
structured_response_data = self._call_gemini_api(formatting_payload)
|
352
|
+
|
353
|
+
if "error" in structured_response_data:
|
354
|
+
print(f"Structuring call failed: {structured_response_data['error']}. Returning intermediate text.")
|
355
|
+
return final_text
|
356
|
+
|
357
|
+
try:
|
358
|
+
structured_text = structured_response_data["candidates"][0]["content"]["parts"][0]["text"]
|
359
|
+
structured_output = json.loads(structured_text)
|
360
|
+
return structured_output
|
361
|
+
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
362
|
+
print(f"Warning: Failed to parse structured output after formatting call: {e}. Returning intermediate text.")
|
363
|
+
return final_text
|
364
|
+
return final_text
|
365
|
+
|
366
|
+
continue
|
367
|
+
|
368
|
+
except (KeyError, IndexError) as e:
|
369
|
+
print(f"Error parsing API response structure: {e}. Response: {response_data}")
|
370
|
+
return {"error": {"message": f"Error parsing API response: {e}", "details": response_data}}
|
371
|
+
|
372
|
+
return {"error": {"message": "Exited interaction loop unexpectedly."}}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: gemini-agent-framework
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A framework for building agents that use Gemini's function calling capabilities
|
5
|
+
Project-URL: Homepage, https://github.com/m7mdony/gemini-agent-framework
|
6
|
+
Project-URL: Documentation, https://github.com/m7mdony/gemini-agent-framework#readme
|
7
|
+
Project-URL: Repository, https://github.com/m7mdony/gemini-agent-framework.git
|
8
|
+
Author-email: Mohamed Baathman <mohamed.baathman2001@gmail.com>
|
9
|
+
License-Expression: MIT
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
11
|
+
Classifier: Intended Audience :: Developers
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
18
|
+
Requires-Python: >=3.8
|
19
|
+
Requires-Dist: python-dotenv>=1.0.0
|
20
|
+
Requires-Dist: requests>=2.31.0
|
21
|
+
Description-Content-Type: text/markdown
|
22
|
+
|
23
|
+
# Gemini Agent Framework
|
24
|
+
|
25
|
+
A Python framework for building agents that use Gemini's function calling capabilities. This framework allows you to easily create agents that can break down complex tasks into sequential steps using available tools.
|
26
|
+
|
27
|
+
## Installation
|
28
|
+
|
29
|
+
```bash
|
30
|
+
pip install gemini-agent-framework
|
31
|
+
```
|
32
|
+
|
33
|
+
## Quick Start
|
34
|
+
|
35
|
+
```python
|
36
|
+
from gemini_agent import Agent
|
37
|
+
from dotenv import load_dotenv
|
38
|
+
|
39
|
+
load_dotenv()
|
40
|
+
|
41
|
+
# Define your tools
|
42
|
+
@Agent.description("Multiplies two numbers.")
|
43
|
+
@Agent.parameters({
|
44
|
+
'a': {'type': int, 'description': 'The first number'},
|
45
|
+
'b': {'type': int, 'description': 'The second number'}
|
46
|
+
})
|
47
|
+
def multiply(a: int, b: int) -> int:
|
48
|
+
return a * b
|
49
|
+
|
50
|
+
# Create an agent instance
|
51
|
+
agent = Agent(api_key="your-api-key", tools=[multiply])
|
52
|
+
|
53
|
+
# Use the agent
|
54
|
+
response = agent.prompt("Multiply 3 and 7")
|
55
|
+
print(response) # Should output 21
|
56
|
+
```
|
57
|
+
|
58
|
+
## Features
|
59
|
+
|
60
|
+
- Easy tool definition using decorators
|
61
|
+
- Automatic sequential task breakdown
|
62
|
+
- Support for structured responses
|
63
|
+
- Intermediate result handling
|
64
|
+
- Error handling and recovery
|
65
|
+
|
66
|
+
## Documentation
|
67
|
+
|
68
|
+
For more detailed documentation, please visit the [documentation page](https://github.com/yourusername/gemini-agent-framework#readme).
|
69
|
+
|
70
|
+
## Contributing
|
71
|
+
|
72
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
73
|
+
|
74
|
+
## License
|
75
|
+
|
76
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
@@ -0,0 +1,5 @@
|
|
1
|
+
gemini_agent/__init__.py,sha256=LOMwiMfThpfN5KBMHrkQX9C7WvJIcUb0BUsLo55id8c,71
|
2
|
+
gemini_agent/agent.py,sha256=4gKLM5-wdXpFo6jq430-v7XbLDLJIJ4AUqqUUotP7fU,19466
|
3
|
+
gemini_agent_framework-0.1.0.dist-info/METADATA,sha256=UVDxC4SIYk6Vwf6tbXfkjrHuECZRxJlfEFU4GwgZrrI,2386
|
4
|
+
gemini_agent_framework-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
+
gemini_agent_framework-0.1.0.dist-info/RECORD,,
|