gemini-agent-framework 0.1.0__tar.gz

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.
@@ -0,0 +1,5 @@
1
+ __pycache__
2
+ llama.log
3
+ localllm.exe
4
+ .env
5
+ llm_test
@@ -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,54 @@
1
+ # Gemini Agent Framework
2
+
3
+ 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.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install gemini-agent-framework
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from gemini_agent import Agent
15
+ from dotenv import load_dotenv
16
+
17
+ load_dotenv()
18
+
19
+ # Define your tools
20
+ @Agent.description("Multiplies two numbers.")
21
+ @Agent.parameters({
22
+ 'a': {'type': int, 'description': 'The first number'},
23
+ 'b': {'type': int, 'description': 'The second number'}
24
+ })
25
+ def multiply(a: int, b: int) -> int:
26
+ return a * b
27
+
28
+ # Create an agent instance
29
+ agent = Agent(api_key="your-api-key", tools=[multiply])
30
+
31
+ # Use the agent
32
+ response = agent.prompt("Multiply 3 and 7")
33
+ print(response) # Should output 21
34
+ ```
35
+
36
+ ## Features
37
+
38
+ - Easy tool definition using decorators
39
+ - Automatic sequential task breakdown
40
+ - Support for structured responses
41
+ - Intermediate result handling
42
+ - Error handling and recovery
43
+
44
+ ## Documentation
45
+
46
+ For more detailed documentation, please visit the [documentation page](https://github.com/yourusername/gemini-agent-framework#readme).
47
+
48
+ ## Contributing
49
+
50
+ Contributions are welcome! Please feel free to submit a Pull Request.
51
+
52
+ ## License
53
+
54
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "gemini-agent-framework"
7
+ version = "0.1.0"
8
+ description = "A framework for building agents that use Gemini's function calling capabilities"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Mohamed Baathman", email = "mohamed.baathman2001@gmail.com" }
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.8",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ ]
25
+ dependencies = [
26
+ "requests>=2.31.0",
27
+ "python-dotenv>=1.0.0",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/m7mdony/gemini-agent-framework"
32
+ Documentation = "https://github.com/m7mdony/gemini-agent-framework#readme"
33
+ Repository = "https://github.com/m7mdony/gemini-agent-framework.git"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/gemini_agent"]
@@ -0,0 +1,2 @@
1
+ python-dotenv>=1.0.0
2
+ requests>=2.31.0
@@ -0,0 +1,4 @@
1
+ from .agent import Agent
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["Agent"]
@@ -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,48 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from src.gemini_agent import Agent
4
+
5
+ load_dotenv()
6
+
7
+ def test_basic_operations():
8
+ # Define some basic math operations
9
+ @Agent.description("Multiplies two numbers.")
10
+ @Agent.parameters({
11
+ 'a': {'type': int, 'description': 'The first number'},
12
+ 'b': {'type': int, 'description': 'The second number'}
13
+ })
14
+ def multiply(a: int, b: int) -> int:
15
+ return a * b
16
+
17
+ @Agent.description("Adds two numbers.")
18
+ @Agent.parameters({
19
+ 'a': {'type': int, 'description': 'The first number'},
20
+ 'b': {'type': int, 'description': 'The second number'}
21
+ })
22
+ def add(a: int, b: int) -> int:
23
+ return a + b
24
+
25
+ # Create an agent with the math tools
26
+ agent = Agent(
27
+ api_key=os.getenv("GEMINI_API_KEY"),
28
+ tools=[multiply, add]
29
+ )
30
+
31
+ # Test a simple multiplication
32
+ response = agent.prompt("Multiply 3 and 7")
33
+ print(f"Multiplication result: {response}") # Should be 21
34
+
35
+ # Test a complex operation
36
+ response = agent.prompt(
37
+ "Multiply 3 and 7, then add 4 to the result",
38
+ response_structure={
39
+ "type": "object",
40
+ "properties": {
41
+ "result": {"type": "number"}
42
+ }
43
+ }
44
+ )
45
+ print(f"Complex operation result: {response}") # Should be {"result": 25}
46
+
47
+ if __name__ == "__main__":
48
+ test_basic_operations()