Explorar el Código

feat: support gemini agent

Talegorithm hace 1 mes
padre
commit
a76fce8770

+ 2 - 1
.gitignore

@@ -1,4 +1,5 @@
-# reson-agent
+# API-KEY
+.env
 
 
 # Python
 # Python
 __pycache__/
 __pycache__/

+ 6 - 6
agent/__init__.py

@@ -10,12 +10,12 @@ Reson Agent - 可扩展、可学习的 Agent 框架
 - TraceStore, MemoryStore: 存储接口
 - TraceStore, MemoryStore: 存储接口
 """
 """
 
 
-from reson_agent.runner import AgentRunner
-from reson_agent.events import AgentEvent
-from reson_agent.models.trace import Trace, Step, StepType
-from reson_agent.models.memory import Experience, Skill
-from reson_agent.tools import tool, ToolRegistry, get_tool_registry
-from reson_agent.storage.protocols import TraceStore, MemoryStore, StateStore
+from agent.runner import AgentRunner
+from agent.events import AgentEvent
+from agent.models.trace import Trace, Step, StepType
+from agent.models.memory import Experience, Skill
+from agent.tools import tool, ToolRegistry, get_tool_registry
+from agent.storage.protocols import TraceStore, MemoryStore, StateStore
 
 
 __version__ = "0.1.0"
 __version__ = "0.1.0"
 
 

+ 11 - 0
agent/llm/__init__.py

@@ -0,0 +1,11 @@
+"""
+LLM Provider 封装
+
+提供统一的接口支持多种 LLM provider
+"""
+
+from agent.llm.providers.gemini import create_gemini_llm_call
+
+__all__ = [
+    "create_gemini_llm_call",
+]

+ 5 - 0
agent/llm/providers/__init__.py

@@ -0,0 +1,5 @@
+"""
+LLM Providers
+
+各个 LLM 提供商的适配器
+"""

+ 317 - 0
agent/llm/providers/gemini.py

@@ -0,0 +1,317 @@
+"""
+Gemini Provider (HTTP API)
+
+使用 httpx 直接调用 Gemini REST API,避免 google-generativeai SDK 的兼容性问题
+
+参考:Resonote/llm/providers/gemini.py
+"""
+
+import os
+import json
+import httpx
+from typing import List, Dict, Any, Optional
+
+
+def _convert_messages_to_gemini(messages: List[Dict]) -> tuple[List[Dict], Optional[str]]:
+    """
+    将 OpenAI 格式消息转换为 Gemini 格式
+
+    Returns:
+        (gemini_contents, system_instruction)
+    """
+    contents = []
+    system_instruction = None
+    tool_parts_buffer = []
+
+    def flush_tool_buffer():
+        """合并连续的 tool 消息为单个 user 消息"""
+        if tool_parts_buffer:
+            contents.append({
+                "role": "user",
+                "parts": tool_parts_buffer.copy()
+            })
+            tool_parts_buffer.clear()
+
+    for msg in messages:
+        role = msg.get("role")
+
+        # System 消息 -> system_instruction
+        if role == "system":
+            system_instruction = msg.get("content", "")
+            continue
+
+        # Tool 消息 -> functionResponse
+        if role == "tool":
+            tool_name = msg.get("name")
+            content_text = msg.get("content", "")
+
+            if not tool_name:
+                print(f"[WARNING] Tool message missing 'name' field, skipping")
+                continue
+
+            # 尝试解析为 JSON
+            try:
+                parsed = json.loads(content_text) if content_text else {}
+                if isinstance(parsed, list):
+                    response_data = {"result": parsed}
+                else:
+                    response_data = parsed
+            except (json.JSONDecodeError, ValueError):
+                response_data = {"result": content_text}
+
+            # 添加到 buffer
+            tool_parts_buffer.append({
+                "functionResponse": {
+                    "name": tool_name,
+                    "response": response_data
+                }
+            })
+            continue
+
+        # 非 tool 消息:先 flush buffer
+        flush_tool_buffer()
+
+        content_text = msg.get("content", "")
+        tool_calls = msg.get("tool_calls")
+
+        # Assistant 消息 + tool_calls
+        if role == "assistant" and tool_calls:
+            parts = []
+            if content_text and content_text.strip():
+                parts.append({"text": content_text})
+
+            # 转换 tool_calls 为 functionCall
+            for tc in tool_calls:
+                func = tc.get("function", {})
+                func_name = func.get("name", "")
+                func_args_str = func.get("arguments", "{}")
+                try:
+                    func_args = json.loads(func_args_str) if isinstance(func_args_str, str) else func_args_str
+                except json.JSONDecodeError:
+                    func_args = {}
+
+                parts.append({
+                    "functionCall": {
+                        "name": func_name,
+                        "args": func_args
+                    }
+                })
+
+            if parts:
+                contents.append({
+                    "role": "model",
+                    "parts": parts
+                })
+            continue
+
+        # 跳过空消息
+        if not content_text or not content_text.strip():
+            continue
+
+        # 普通消息
+        gemini_role = "model" if role == "assistant" else "user"
+        contents.append({
+            "role": gemini_role,
+            "parts": [{"text": content_text}]
+        })
+
+    # Flush 剩余的 tool messages
+    flush_tool_buffer()
+
+    # 合并连续的 user 消息(Gemini 要求严格交替)
+    merged_contents = []
+    i = 0
+    while i < len(contents):
+        current = contents[i]
+
+        if current["role"] == "user":
+            merged_parts = current["parts"].copy()
+            j = i + 1
+            while j < len(contents) and contents[j]["role"] == "user":
+                merged_parts.extend(contents[j]["parts"])
+                j += 1
+
+            merged_contents.append({
+                "role": "user",
+                "parts": merged_parts
+            })
+            i = j
+        else:
+            merged_contents.append(current)
+            i += 1
+
+    return merged_contents, system_instruction
+
+
+def _convert_tools_to_gemini(tools: List[Dict]) -> List[Dict]:
+    """
+    将 OpenAI 工具格式转换为 Gemini REST API 格式
+
+    OpenAI: [{"type": "function", "function": {"name": "...", "parameters": {...}}}]
+    Gemini API: [{"functionDeclarations": [{"name": "...", "parameters": {...}}]}]
+    """
+    if not tools:
+        return []
+
+    function_declarations = []
+    for tool in tools:
+        if tool.get("type") == "function":
+            func = tool.get("function", {})
+
+            # 清理不支持的字段
+            parameters = func.get("parameters", {})
+            if "properties" in parameters:
+                cleaned_properties = {}
+                for prop_name, prop_def in parameters["properties"].items():
+                    # 移除 default 字段
+                    cleaned_prop = {k: v for k, v in prop_def.items() if k != "default"}
+                    cleaned_properties[prop_name] = cleaned_prop
+
+                # Gemini API 需要完整的 schema
+                cleaned_parameters = {
+                    "type": "object",
+                    "properties": cleaned_properties
+                }
+                if "required" in parameters:
+                    cleaned_parameters["required"] = parameters["required"]
+
+                parameters = cleaned_parameters
+
+            function_declarations.append({
+                "name": func.get("name"),
+                "description": func.get("description", ""),
+                "parameters": parameters
+            })
+
+    return [{"functionDeclarations": function_declarations}] if function_declarations else []
+
+
+def create_gemini_llm_call(
+    base_url: Optional[str] = None,
+    api_key: Optional[str] = None
+):
+    """
+    创建 Gemini LLM 调用函数(HTTP API)
+
+    Args:
+        base_url: Gemini API base URL(默认使用 Google 官方)
+        api_key: API key(默认从环境变量读取)
+
+    Returns:
+        async 函数
+    """
+    base_url = base_url or "https://generativelanguage.googleapis.com/v1beta"
+    api_key = api_key or os.getenv("GEMINI_API_KEY")
+
+    if not api_key:
+        raise ValueError("GEMINI_API_KEY not found")
+
+    # 创建 HTTP 客户端
+    client = httpx.AsyncClient(
+        headers={"x-goog-api-key": api_key},
+        timeout=httpx.Timeout(120.0, connect=10.0)
+    )
+
+    async def gemini_llm_call(
+        messages: List[Dict[str, Any]],
+        model: str = "gemini-2.5-pro",
+        tools: Optional[List[Dict]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        调用 Gemini REST API
+
+        Args:
+            messages: OpenAI 格式消息
+            model: 模型名称
+            tools: OpenAI 格式工具列表
+            **kwargs: 其他参数
+
+        Returns:
+            {
+                "content": str,
+                "tool_calls": List[Dict] | None,
+                "prompt_tokens": int,
+                "completion_tokens": int,
+                "cost": float
+            }
+        """
+        # 转换消息
+        contents, system_instruction = _convert_messages_to_gemini(messages)
+
+        print(f"\n[Gemini HTTP] Converted {len(contents)} messages: {[c['role'] for c in contents]}")
+
+        # 构建请求
+        endpoint = f"{base_url}/models/{model}:generateContent"
+        payload = {"contents": contents}
+
+        # 添加 system instruction
+        if system_instruction:
+            payload["systemInstruction"] = {"parts": [{"text": system_instruction}]}
+
+        # 添加工具
+        if tools:
+            gemini_tools = _convert_tools_to_gemini(tools)
+            if gemini_tools:
+                payload["tools"] = gemini_tools
+
+        # 调用 API
+        try:
+            response = await client.post(endpoint, json=payload)
+            response.raise_for_status()
+            gemini_resp = response.json()
+
+        except httpx.HTTPStatusError as e:
+            error_body = e.response.text
+            print(f"[Gemini HTTP] Error {e.response.status_code}: {error_body}")
+            raise
+        except Exception as e:
+            print(f"[Gemini HTTP] Request failed: {e}")
+            raise
+
+        # 解析响应
+        content = ""
+        tool_calls = None
+
+        candidates = gemini_resp.get("candidates", [])
+        if candidates:
+            parts = candidates[0].get("content", {}).get("parts", [])
+
+            # 提取文本
+            for part in parts:
+                if "text" in part:
+                    content += part.get("text", "")
+
+            # 提取 functionCall
+            for i, part in enumerate(parts):
+                if "functionCall" in part:
+                    if tool_calls is None:
+                        tool_calls = []
+
+                    fc = part["functionCall"]
+                    name = fc.get("name", "")
+                    args = fc.get("args", {})
+
+                    tool_calls.append({
+                        "id": f"call_{i}",
+                        "type": "function",
+                        "function": {
+                            "name": name,
+                            "arguments": json.dumps(args, ensure_ascii=False)
+                        }
+                    })
+
+        # 提取 usage
+        usage_meta = gemini_resp.get("usageMetadata", {})
+        prompt_tokens = usage_meta.get("promptTokenCount", 0)
+        completion_tokens = usage_meta.get("candidatesTokenCount", 0)
+
+        return {
+            "content": content,
+            "tool_calls": tool_calls,
+            "prompt_tokens": prompt_tokens,
+            "completion_tokens": completion_tokens,
+            "cost": 0.0
+        }
+
+    return gemini_llm_call

+ 2 - 2
agent/models/__init__.py

@@ -2,7 +2,7 @@
 Models 包
 Models 包
 """
 """
 
 
-from reson_agent.models.trace import Trace, Step, StepType
-from reson_agent.models.memory import Experience, Skill
+from agent.models.trace import Trace, Step, StepType
+from agent.models.memory import Experience, Skill
 
 
 __all__ = ["Trace", "Step", "StepType", "Experience", "Skill"]
 __all__ = ["Trace", "Step", "StepType", "Experience", "Skill"]

+ 7 - 6
agent/runner.py

@@ -13,11 +13,11 @@ from dataclasses import dataclass, field
 from datetime import datetime
 from datetime import datetime
 from typing import AsyncIterator, Optional, Dict, Any, List, Callable, Literal
 from typing import AsyncIterator, Optional, Dict, Any, List, Callable, Literal
 
 
-from reson_agent.events import AgentEvent
-from reson_agent.models.trace import Trace, Step
-from reson_agent.models.memory import Experience, Skill
-from reson_agent.storage.protocols import TraceStore, MemoryStore, StateStore
-from reson_agent.tools import ToolRegistry, get_tool_registry
+from agent.events import AgentEvent
+from agent.models.trace import Trace, Step
+from agent.models.memory import Experience, Skill
+from agent.storage.protocols import TraceStore, MemoryStore, StateStore
+from agent.tools import ToolRegistry, get_tool_registry
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -413,10 +413,11 @@ class AgentRunner:
                             "result": tool_result
                             "result": tool_result
                         })
                         })
 
 
-                        # 添加到消息
+                        # 添加到消息(Gemini 需要 name 字段!)
                         messages.append({
                         messages.append({
                             "role": "tool",
                             "role": "tool",
                             "tool_call_id": tc["id"],
                             "tool_call_id": tc["id"],
+                            "name": tool_name,
                             "content": tool_result
                             "content": tool_result
                         })
                         })
 
 

+ 2 - 2
agent/storage/__init__.py

@@ -2,8 +2,8 @@
 Storage 包 - 存储接口和实现
 Storage 包 - 存储接口和实现
 """
 """
 
 
-from reson_agent.storage.protocols import TraceStore, MemoryStore, StateStore
-from reson_agent.storage.memory_impl import MemoryTraceStore, MemoryMemoryStore, MemoryStateStore
+from agent.storage.protocols import TraceStore, MemoryStore, StateStore
+from agent.storage.memory_impl import MemoryTraceStore, MemoryMemoryStore, MemoryStateStore
 
 
 __all__ = [
 __all__ = [
     "TraceStore",
     "TraceStore",

+ 2 - 2
agent/storage/memory_impl.py

@@ -7,8 +7,8 @@ Memory Implementation - 内存存储实现
 from typing import Dict, List, Optional, Any
 from typing import Dict, List, Optional, Any
 from datetime import datetime
 from datetime import datetime
 
 
-from reson_agent.models.trace import Trace, Step
-from reson_agent.models.memory import Experience, Skill
+from agent.models.trace import Trace, Step
+from agent.models.memory import Experience, Skill
 
 
 
 
 class MemoryTraceStore:
 class MemoryTraceStore:

+ 2 - 2
agent/storage/protocols.py

@@ -6,8 +6,8 @@ Storage Protocols - 存储接口定义
 
 
 from typing import Protocol, List, Optional, Dict, Any, runtime_checkable
 from typing import Protocol, List, Optional, Dict, Any, runtime_checkable
 
 
-from reson_agent.models.trace import Trace, Step
-from reson_agent.models.memory import Experience, Skill
+from agent.models.trace import Trace, Step
+from agent.models.memory import Experience, Skill
 
 
 
 
 @runtime_checkable
 @runtime_checkable

+ 3 - 3
agent/tools/__init__.py

@@ -2,9 +2,9 @@
 Tools 包 - 工具注册和 Schema 生成
 Tools 包 - 工具注册和 Schema 生成
 """
 """
 
 
-from reson_agent.tools.registry import ToolRegistry, tool, get_tool_registry
-from reson_agent.tools.schema import SchemaGenerator
-from reson_agent.tools.models import ToolResult, ToolContext, ToolContextImpl
+from agent.tools.registry import ToolRegistry, tool, get_tool_registry
+from agent.tools.schema import SchemaGenerator
+from agent.tools.models import ToolResult, ToolContext, ToolContextImpl
 
 
 __all__ = [
 __all__ = [
 	"ToolRegistry",
 	"ToolRegistry",

+ 10 - 4
agent/tools/registry.py

@@ -16,7 +16,7 @@ import logging
 import time
 import time
 from typing import Any, Callable, Dict, List, Optional
 from typing import Any, Callable, Dict, List, Optional
 
 
-from reson_agent.tools.url_matcher import filter_by_url
+from agent.tools.url_matcher import filter_by_url
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -84,7 +84,7 @@ class ToolRegistry:
 		# 如果没有提供 Schema,自动生成
 		# 如果没有提供 Schema,自动生成
 		if schema is None:
 		if schema is None:
 			try:
 			try:
-				from reson_agent.tools.schema import SchemaGenerator
+				from agent.tools.schema import SchemaGenerator
 				schema = SchemaGenerator.generate(func)
 				schema = SchemaGenerator.generate(func)
 			except Exception as e:
 			except Exception as e:
 				logger.error(f"Failed to generate schema for {func_name}: {e}")
 				logger.error(f"Failed to generate schema for {func_name}: {e}")
@@ -207,7 +207,7 @@ class ToolRegistry:
 
 
 			# 处理敏感数据占位符
 			# 处理敏感数据占位符
 			if sensitive_data:
 			if sensitive_data:
-				from reson_agent.tools.sensitive import replace_sensitive_data
+				from agent.tools.sensitive import replace_sensitive_data
 				current_url = context.get("page_url") if context else None
 				current_url = context.get("page_url") if context else None
 				arguments = replace_sensitive_data(arguments, sensitive_data, current_url)
 				arguments = replace_sensitive_data(arguments, sensitive_data, current_url)
 
 
@@ -230,9 +230,15 @@ class ToolRegistry:
 			duration = time.time() - start_time
 			duration = time.time() - start_time
 			stats.total_duration += duration
 			stats.total_duration += duration
 
 
-			# 返回 JSON 字符串
+			# 返回 JSON 字符串或文本
 			if isinstance(result, str):
 			if isinstance(result, str):
 				return result
 				return result
+
+			# 处理 ToolResult 对象
+			from agent.tools.models import ToolResult
+			if isinstance(result, ToolResult):
+				return result.to_llm_message()
+
 			return json.dumps(result, ensure_ascii=False, indent=2)
 			return json.dumps(result, ensure_ascii=False, indent=2)
 
 
 		except Exception as e:
 		except Exception as e:

+ 1 - 1
agent/tools/sensitive.py

@@ -36,7 +36,7 @@ def match_domain(url: str, domain_pattern: str) -> bool:
 	Returns:
 	Returns:
 		是否匹配
 		是否匹配
 	"""
 	"""
-	from reson_agent.tools.url_matcher import match_url_with_pattern
+	from agent.tools.url_matcher import match_url_with_pattern
 	return match_url_with_pattern(url, domain_pattern)
 	return match_url_with_pattern(url, domain_pattern)
 
 
 
 

+ 215 - 0
docs/dependencies.md

@@ -0,0 +1,215 @@
+# Agent Framework 依赖管理
+
+## 📦 核心依赖
+
+### 必需依赖
+
+| 包名 | 版本 | 用途 |
+|------|------|------|
+| `httpx[socks]` | >=0.28.0 | HTTP 客户端,用于调用 Gemini REST API,支持 SOCKS 代理 |
+| `python-dotenv` | >=1.0.0 | 环境变量管理,用于加载 `.env` 配置文件 |
+
+### 可选依赖
+
+| 包名 | 版本 | 用途 | 安装建议 |
+|------|------|------|----------|
+| `docstring-parser` | >=0.15 | 工具文档解析,提供更好的类型提示和描述解析 | 推荐安装 |
+| `google-generativeai` | >=0.8.0 | Google Gemini SDK(不推荐使用) | ❌ 不推荐,使用 HTTP API 代替 |
+
+## 🚀 安装方式
+
+### 方式一:使用 requirements.txt(推荐)
+
+```bash
+pip install -r requirements.txt
+```
+
+### 方式二:手动安装
+
+```bash
+# 安装核心依赖
+pip install httpx[socks]>=0.28.0 python-dotenv>=1.0.0
+
+# 可选:安装 docstring-parser
+pip install docstring-parser>=0.15
+```
+
+### 方式三:使用 pip 直接安装
+
+```bash
+pip install httpx[socks] python-dotenv
+```
+
+## 📝 依赖说明
+
+### httpx[socks]
+- **用途**:异步 HTTP 客户端,用于调用 Gemini REST API
+- **为什么需要 `[socks]`**:支持 SOCKS 代理(很多用户需要代理访问 Google API)
+- **如果不需要代理**:可以只安装 `httpx`,但推荐安装完整版
+
+### python-dotenv
+- **用途**:从 `.env` 文件加载环境变量
+- **配置示例**:
+  ```bash
+  # .env
+  GEMINI_API_KEY=your_api_key_here
+  ```
+
+### docstring-parser(可选)
+- **用途**:解析工具函数的 docstring,提取参数类型和描述
+- **如果没安装**:框架会使用 fallback 解析器,功能稍弱但可用
+- **推荐理由**:提供更准确的工具 schema 生成
+
+### google-generativeai(不推荐)
+- **为什么不推荐**:
+  - SDK 对 JSON Schema 格式要求严格,与 OpenAI 工具格式不完全兼容
+  - 难以调试和自定义
+  - HTTP API 提供更好的控制和灵活性
+- **何时使用**:仅用于学习或简单场景
+
+## 🔧 开发依赖
+
+如果你要开发或测试框架,还需要:
+
+```bash
+# 测试框架
+pip install pytest pytest-asyncio
+
+# 代码质量
+pip install black flake8 mypy
+
+# 文档生成
+pip install mkdocs mkdocs-material
+```
+
+## 🌐 Python 版本要求
+
+- **最低版本**:Python 3.9
+- **推荐版本**:Python 3.11+
+- **兼容性**:
+  - ✅ Python 3.9+
+  - ✅ Python 3.10
+  - ✅ Python 3.11
+  - ✅ Python 3.12
+
+## 📋 依赖检查
+
+运行以下命令检查依赖是否正确安装:
+
+```bash
+python3 -c "
+import httpx
+import dotenv
+print('✅ 核心依赖安装成功')
+
+try:
+    import docstring_parser
+    print('✅ docstring-parser 已安装')
+except ImportError:
+    print('⚠️  docstring-parser 未安装(可选)')
+"
+```
+
+## 🐛 常见问题
+
+### Q1: 安装 httpx 时报错 "No module named 'socksio'"
+
+**解决方案**:
+```bash
+pip install 'httpx[socks]'
+```
+
+### Q2: ImportError: cannot import name 'AsyncClient' from 'httpx'
+
+**解决方案**:升级 httpx 到最新版本
+```bash
+pip install --upgrade httpx
+```
+
+### Q3: 代理设置
+
+如果你使用代理,在 `.env` 中配置:
+```bash
+HTTP_PROXY=socks5://127.0.0.1:7890
+HTTPS_PROXY=socks5://127.0.0.1:7890
+```
+
+或者在代码中设置:
+```python
+import httpx
+
+client = httpx.AsyncClient(
+    proxies={
+        "http://": "socks5://127.0.0.1:7890",
+        "https://": "socks5://127.0.0.1:7890"
+    }
+)
+```
+
+## 📦 生成 requirements.txt
+
+如果你修改了依赖,可以重新生成:
+
+```bash
+# 导出当前环境所有包(不推荐)
+pip freeze > requirements-full.txt
+
+# 手动维护 requirements.txt(推荐)
+# 只包含项目直接依赖,不包括子依赖
+```
+
+## 🔄 更新依赖
+
+```bash
+# 更新所有依赖到最新版本
+pip install --upgrade -r requirements.txt
+
+# 更新特定包
+pip install --upgrade httpx
+```
+
+## 🎯 最小化依赖原则
+
+本框架遵循最小化依赖原则:
+- ✅ 只依赖必需的包
+- ✅ 避免大型框架(如 Django, FastAPI)
+- ✅ 使用标准库优先(asyncio, json, logging)
+- ✅ 可选依赖明确标注
+
+## 📊 依赖关系图
+
+```
+agent/
+├── httpx (必需)
+│   ├── httpcore
+│   ├── certifi
+│   ├── idna
+│   └── socksio (SOCKS 代理支持)
+│
+├── python-dotenv (必需)
+│
+└── docstring-parser (可选)
+```
+
+## 🌟 生产环境建议
+
+生产环境推荐固定版本:
+
+```txt
+# requirements-prod.txt
+httpx[socks]==0.28.1
+python-dotenv==1.2.1
+docstring-parser==0.16
+```
+
+使用:
+```bash
+pip install -r requirements-prod.txt
+```
+
+## 📚 相关文档
+
+- [httpx 文档](https://www.python-httpx.org/)
+- [python-dotenv 文档](https://github.com/theskumar/python-dotenv)
+- [docstring-parser 文档](https://github.com/rr-/docstring_parser)
+- [Gemini API 文档](https://ai.google.dev/api/rest)

+ 165 - 0
examples/README_gemini_agent.md

@@ -0,0 +1,165 @@
+# Gemini Agent 测试成功 ✅
+
+## 问题诊断与修复
+
+### 🔍 发现的问题
+
+1. **消息历史丢失**
+   - **问题**:tool 消息缺少 `name` 字段
+   - **位置**:`agent/runner.py:417`
+   - **修复**:添加 `name: tool_name` 字段到 tool 消息
+
+2. **架构设计不当**
+   - **问题**:模型封装放在 example 中,应该在基础模块
+   - **修复**:创建 `agent/llm/providers/gemini.py` 模块
+
+3. **SDK 兼容性问题**
+   - **问题**:`google-generativeai` SDK 对参数格式要求严格,与 OpenAI JSON Schema 不兼容
+   - **修复**:使用 HTTP REST API(`httpx`)代替 SDK
+
+### ✅ 最终方案
+
+#### 1. HTTP API Provider(推荐)
+
+**文件**:`agent/llm/providers/gemini.py`
+
+**特性**:
+- 使用 `httpx` 直接调用 Gemini REST API
+- 完全控制消息格式转换
+- 正确处理:
+  - `systemInstruction`
+  - `functionCall` / `functionResponse`
+  - 连续 user 消息合并(Gemini 要求严格的 user/model 交替)
+
+**关键实现**:
+
+```python
+def _convert_messages_to_gemini(messages):
+    """OpenAI -> Gemini 格式转换"""
+    # 1. system -> systemInstruction
+    # 2. assistant + tool_calls -> model + functionCall
+    # 3. tool -> user + functionResponse
+    # 4. 合并连续的 user 消息
+    ...
+
+def _convert_tools_to_gemini(tools):
+    """工具定义转换,清理不支持的字段(default)"""
+    ...
+```
+
+#### 2. 使用方式
+
+```python
+from agent.llm.providers.gemini import create_gemini_llm_call
+from agent.runner import AgentRunner
+
+# 创建 LLM 调用函数
+gemini_llm_call = create_gemini_llm_call()
+
+# 创建 Agent
+runner = AgentRunner(
+    tool_registry=registry,
+    llm_call=gemini_llm_call
+)
+
+# 运行
+async for event in runner.run(
+    task="你的任务",
+    model="gemini-2.5-pro",
+    tools=["tool1", "tool2"],
+    system_prompt="系统提示"
+):
+    # 处理事件
+    ...
+```
+
+## 测试结果
+
+**任务**:"北京今天的天气怎么样?顺便帮我计算一下 15 * 8 等于多少。"
+
+**执行流程**:
+```
+1. [Gemini HTTP] Converted 1 messages: ['user']
+   → 调用工具: get_current_weather, calculate
+
+2. [Gemini HTTP] Converted 3 messages: ['user', 'model', 'user']
+   → 生成答案: "北京今天天气晴朗,15摄氏度。15 * 8 的计算结果是 120。"
+
+✓ Trace 完成
+  总 Tokens: 643
+```
+
+**结果**:✅ 完美运行,没有重复调用,正确结束
+
+## 架构对比
+
+### ❌ 之前(错误)
+
+```
+examples/
+└── gemini_basic_agent.py  # 包含所有逻辑
+    ├── 工具定义
+    ├── Gemini SDK 调用
+    └── Agent 运行
+```
+
+**问题**:
+- 模型封装混在示例中
+- 使用 SDK 导致兼容性问题
+- 消息历史处理有bug
+
+### ✅ 现在(正确)
+
+```
+agent/
+├── llm/
+│   └── providers/
+│       └── gemini.py          # HTTP API 封装
+├── tools/
+│   └── registry.py            # 工具注册
+└── runner.py                  # Agent 运行器
+
+examples/
+└── gemini_basic_agent.py      # 简洁的示例代码
+    ├── 工具定义
+    └── 使用 agent.llm.providers
+```
+
+**优势**:
+- 清晰的模块分层
+- 易于扩展(添加其他 provider)
+- HTTP API 完全控制格式
+
+## 依赖
+
+```bash
+pip install httpx python-dotenv
+
+# 如果有代理
+pip install 'httpx[socks]'
+```
+
+## 环境变量
+
+```bash
+# .env
+GEMINI_API_KEY=your_api_key_here
+```
+
+## 参考
+
+- Resonote 实现:`/Users/sunlit/Code/Resonote/llm/providers/gemini.py`
+- Gemini REST API 文档:https://ai.google.dev/api/rest
+
+## 下一步
+
+可以扩展支持更多 LLM provider:
+- OpenAI(已有 SDK)
+- Claude(Anthropic API)
+- DeepSeek
+- 本地模型(Ollama)
+
+每个 provider 只需实现相同的接口:
+```python
+async def llm_call(messages, model, tools, **kwargs) -> Dict
+```

+ 224 - 0
examples/gemini_basic_agent.py

@@ -0,0 +1,224 @@
+"""
+Gemini Agent 基础示例
+
+使用 Gemini 2.5 Pro 模型,演示带工具调用的 Agent
+
+依赖:
+    pip install httpx python-dotenv
+
+使用方法:
+    python examples/gemini_basic_agent.py
+"""
+
+import os
+import sys
+import json
+import asyncio
+from typing import Dict, Any, List, Optional
+from dotenv import load_dotenv
+
+# 添加项目根目录到 Python 路径
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+# 加载环境变量
+load_dotenv()
+
+# 导入框架
+from agent.tools import tool, ToolResult, get_tool_registry
+from agent.runner import AgentRunner
+from agent.llm.providers.gemini import create_gemini_llm_call
+
+
+# ============================================================
+# 定义工具
+# ============================================================
+
+@tool()
+async def get_current_weather(location: str, unit: str = "celsius", uid: str = "") -> Dict[str, Any]:
+    """
+    获取指定地点的当前天气
+
+    Args:
+        location: 城市名称,如 "北京"、"San Francisco"
+        unit: 温度单位,"celsius" 或 "fahrenheit"
+
+    Returns:
+        天气信息字典
+    """
+    # 模拟天气数据
+    weather_data = {
+        "北京": {"temp": 15, "condition": "晴朗", "humidity": 45},
+        "上海": {"temp": 20, "condition": "多云", "humidity": 60},
+        "San Francisco": {"temp": 18, "condition": "Foggy", "humidity": 70},
+        "New York": {"temp": 10, "condition": "Rainy", "humidity": 80}
+    }
+
+    data = weather_data.get(location, {"temp": 22, "condition": "Unknown", "humidity": 50})
+
+    if unit == "fahrenheit":
+        data["temp"] = data["temp"] * 9/5 + 32
+
+    return {
+        "location": location,
+        "temperature": data["temp"],
+        "unit": unit,
+        "condition": data["condition"],
+        "humidity": data["humidity"]
+    }
+
+
+@tool()
+async def calculate(expression: str, uid: str = "") -> ToolResult:
+    """
+    执行数学计算
+
+    Args:
+        expression: 数学表达式,如 "2 + 2"、"10 * 5"
+
+    Returns:
+        计算结果
+    """
+    try:
+        # 安全地计算简单表达式
+        # 注意:实际生产环境应使用更安全的方法
+        result = eval(expression, {"__builtins__": {}}, {})
+
+        return ToolResult(
+            title="计算结果",
+            output=f"{expression} = {result}",
+            long_term_memory=f"计算了 {expression}"
+        )
+    except Exception as e:
+        return ToolResult(
+            title="计算错误",
+            output=f"无法计算 '{expression}': {str(e)}",
+            long_term_memory=f"计算失败: {expression}"
+        )
+
+
+@tool()
+async def search_knowledge(query: str, max_results: int = 3, uid: str = "") -> ToolResult:
+    """
+    搜索知识库
+
+    Args:
+        query: 搜索关键词
+        max_results: 返回结果数量
+
+    Returns:
+        搜索结果
+    """
+    # 模拟知识库搜索
+    knowledge_base = {
+        "Python": "Python 是一种高级编程语言,以简洁易读的语法著称。",
+        "Agent": "Agent 是能够感知环境并采取行动以实现目标的智能体。",
+        "Gemini": "Gemini 是 Google 开发的多模态大语言模型系列。",
+        "AI": "人工智能(AI)是计算机科学的一个分支,致力于创建智能机器。"
+    }
+
+    results = []
+    for key, value in knowledge_base.items():
+        if query.lower() in key.lower() or query.lower() in value.lower():
+            results.append({"title": key, "content": value})
+            if len(results) >= max_results:
+                break
+
+    if not results:
+        output = f"未找到关于 '{query}' 的信息"
+    else:
+        output = "\n\n".join([f"**{r['title']}**\n{r['content']}" for r in results])
+
+    return ToolResult(
+        title=f"搜索结果: {query}",
+        output=output,
+        long_term_memory=f"搜索了 '{query}',找到 {len(results)} 条结果"
+    )
+
+
+# ============================================================
+# 主函数
+# ============================================================
+
+async def main():
+    print("=" * 60)
+    print("Gemini Agent 基础示例")
+    print("=" * 60)
+    print()
+
+    # 获取工具注册表
+    registry = get_tool_registry()
+
+    # 打印可用工具
+    print("可用工具:")
+    for tool_name in registry.get_tool_names():
+        print(f"  - {tool_name}")
+    print()
+
+    # 创建 Gemini LLM 调用函数
+    gemini_llm_call = create_gemini_llm_call()
+
+    # 创建 Agent Runner
+    runner = AgentRunner(
+        tool_registry=registry,
+        llm_call=gemini_llm_call,
+    )
+
+    # 测试任务
+    task = "北京今天的天气怎么样?顺便帮我计算一下 15 * 8 等于多少。"
+
+    print(f"任务: {task}")
+    print("-" * 60)
+    print()
+
+    # 运行 Agent
+    async for event in runner.run(
+        task=task,
+        model="gemini-2.5-pro",
+        tools=["get_current_weather", "calculate", "search_knowledge"],
+        max_iterations=5,
+        enable_memory=False,  # 暂不启用记忆
+        auto_execute_tools=True,
+        system_prompt="你是一个有用的AI助手,可以使用工具来帮助用户。请简洁明了地回答问题。"
+    ):
+        event_type = event.type
+        data = event.data
+
+        if event_type == "trace_started":
+            print(f"✓ Trace 开始: {data['trace_id']}")
+            print()
+
+        elif event_type == "llm_call_completed":
+            print(f"🤖 LLM 响应:")
+            if data.get("content"):
+                print(f"   {data['content']}")
+            if data.get("tool_calls"):
+                print(f"   工具调用: {len(data['tool_calls'])} 个")
+            print(f"   Tokens: {data.get('tokens', 0)}")
+            print()
+
+        elif event_type == "tool_executing":
+            print(f"🔧 执行工具: {data['tool_name']}")
+            print(f"   参数: {json.dumps(data['arguments'], ensure_ascii=False)}")
+
+        elif event_type == "tool_result":
+            print(f"   结果: {data['result'][:100]}...")
+            print()
+
+        elif event_type == "conclusion":
+            print(f"✅ 最终回答:")
+            print(f"   {data['content']}")
+            print()
+
+        elif event_type == "trace_completed":
+            print(f"✓ Trace 完成")
+            print(f"   总 Tokens: {data.get('total_tokens', 0)}")
+            print(f"   总成本: ${data.get('total_cost', 0):.4f}")
+            print()
+
+        elif event_type == "trace_failed":
+            print(f"❌ Trace 失败: {data.get('error')}")
+            print()
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 1 - 1
examples/tools_examples.py

@@ -27,7 +27,7 @@
 import asyncio
 import asyncio
 import json
 import json
 from typing import List, Dict, Any, Optional
 from typing import List, Dict, Any, Optional
-from reson_agent import tool, ToolResult, ToolContext, get_tool_registry
+from agent import tool, ToolResult, ToolContext, get_tool_registry
 
 
 
 
 # ============================================================
 # ============================================================

+ 3 - 0
requirements.txt

@@ -0,0 +1,3 @@
+# LLM request
+httpx[socks]>=0.28.0
+python-dotenv>=1.0.0

+ 1 - 1
tests/test_runner.py

@@ -3,7 +3,7 @@ AgentRunner 测试
 """
 """
 
 
 import pytest
 import pytest
-from reson_agent import (
+from agent import (
     AgentRunner,
     AgentRunner,
     AgentEvent,
     AgentEvent,
     Trace,
     Trace,

+ 0 - 0
tools/search.py