Talegorithm 1 месяц назад
Родитель
Сommit
a76fce8770

+ 2 - 1
.gitignore

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

+ 6 - 6
agent/__init__.py

@@ -10,12 +10,12 @@ Reson Agent - 可扩展、可学习的 Agent 框架
 - 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"
 

+ 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 包
 """
 
-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"]

+ 7 - 6
agent/runner.py

@@ -13,11 +13,11 @@ from dataclasses import dataclass, field
 from datetime import datetime
 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__)
 
@@ -413,10 +413,11 @@ class AgentRunner:
                             "result": tool_result
                         })
 
-                        # 添加到消息
+                        # 添加到消息(Gemini 需要 name 字段!)
                         messages.append({
                             "role": "tool",
                             "tool_call_id": tc["id"],
+                            "name": tool_name,
                             "content": tool_result
                         })
 

+ 2 - 2
agent/storage/__init__.py

@@ -2,8 +2,8 @@
 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__ = [
     "TraceStore",

+ 2 - 2
agent/storage/memory_impl.py

@@ -7,8 +7,8 @@ Memory Implementation - 内存存储实现
 from typing import Dict, List, Optional, Any
 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:

+ 2 - 2
agent/storage/protocols.py

@@ -6,8 +6,8 @@ Storage Protocols - 存储接口定义
 
 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

+ 3 - 3
agent/tools/__init__.py

@@ -2,9 +2,9 @@
 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__ = [
 	"ToolRegistry",

+ 10 - 4
agent/tools/registry.py

@@ -16,7 +16,7 @@ import logging
 import time
 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__)
 
@@ -84,7 +84,7 @@ class ToolRegistry:
 		# 如果没有提供 Schema,自动生成
 		if schema is None:
 			try:
-				from reson_agent.tools.schema import SchemaGenerator
+				from agent.tools.schema import SchemaGenerator
 				schema = SchemaGenerator.generate(func)
 			except Exception as e:
 				logger.error(f"Failed to generate schema for {func_name}: {e}")
@@ -207,7 +207,7 @@ class ToolRegistry:
 
 			# 处理敏感数据占位符
 			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
 				arguments = replace_sensitive_data(arguments, sensitive_data, current_url)
 
@@ -230,9 +230,15 @@ class ToolRegistry:
 			duration = time.time() - start_time
 			stats.total_duration += duration
 
-			# 返回 JSON 字符串
+			# 返回 JSON 字符串或文本
 			if isinstance(result, str):
 				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)
 
 		except Exception as e:

+ 1 - 1
agent/tools/sensitive.py

@@ -36,7 +36,7 @@ def match_domain(url: str, domain_pattern: str) -> bool:
 	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)
 
 

+ 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 json
 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
-from reson_agent import (
+from agent import (
     AgentRunner,
     AgentEvent,
     Trace,

+ 0 - 0
tools/search.py