├── .python-version ├── day-16 ├── .gitignore ├── client │ ├── __init__.py │ ├── test_client.py │ └── hitl_client.py ├── requirements.txt ├── langgraph_agent │ ├── __init__.py │ ├── checkpointer.py │ ├── agent.py │ └── server.py └── README.md ├── day-01 ├── __init__.py ├── README.md └── main.py ├── day-14 ├── host_agent │ ├── __init__.py │ └── agent.py ├── remote_agent │ ├── __init__.py │ └── agent.py ├── run_demo.py └── README.md ├── day-07 ├── __init__.py └── README.md ├── day-08 └── __init__.py ├── day-03 ├── __init__.py ├── root_agent.yaml └── README.md ├── day-02 ├── __init__.py ├── root_agent.yaml └── README.md ├── day-04 ├── agent │ ├── __init__.py │ └── root_agent.yaml ├── deploy.sh └── README.md ├── .env.example ├── day-18 ├── __init__.py ├── README.md └── api_registry_demo.py ├── shared ├── __init__.py └── config.py ├── day-17 ├── __init__.py ├── README.md └── gemini3_flash_thinking.ipynb ├── .gitignore ├── pyproject.toml ├── day-06 ├── README.md └── day06_ide_context.ipynb ├── day-09 ├── agent.py ├── main.py └── day-09-session-rewind.ipynb ├── assets └── difficulty_curve.svg ├── README.md ├── day-15 ├── README.md └── a2ui.py └── day-05 ├── README.md └── telemetry_demo.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /day-16/.gitignore: -------------------------------------------------------------------------------- 1 | # Runtime data 2 | data/ 3 | -------------------------------------------------------------------------------- /day-01/__init__.py: -------------------------------------------------------------------------------- 1 | """Day 01: Introduction to AI Agents.""" 2 | -------------------------------------------------------------------------------- /day-14/host_agent/__init__.py: -------------------------------------------------------------------------------- 1 | # Host Agent - A2A Client 2 | -------------------------------------------------------------------------------- /day-14/remote_agent/__init__.py: -------------------------------------------------------------------------------- 1 | # Remote Agent - A2A Server 2 | -------------------------------------------------------------------------------- /day-07/__init__.py: -------------------------------------------------------------------------------- 1 | # Day 07: Code Execution - Autonomous Problem Solving 2 | -------------------------------------------------------------------------------- /day-08/__init__.py: -------------------------------------------------------------------------------- 1 | """Day 08: Effective Context Management with ADK Layers.""" 2 | -------------------------------------------------------------------------------- /day-03/__init__.py: -------------------------------------------------------------------------------- 1 | # Day 03: Gemini 3 + ADK 2 | # Build AI agents with Google Search grounding 3 | -------------------------------------------------------------------------------- /day-02/__init__.py: -------------------------------------------------------------------------------- 1 | # Day 02: Hello World with YAML 2 | # This file marks the directory as a valid ADK agent package 3 | -------------------------------------------------------------------------------- /day-04/agent/__init__.py: -------------------------------------------------------------------------------- 1 | # Day 04: Source-Based Deployment 2 | # Agent package for deployment to Vertex AI Agent Engine 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Google AI API Key 2 | # Get your key from: https://aistudio.google.com/apikey 3 | GOOGLE_API_KEY=your_api_key_here 4 | -------------------------------------------------------------------------------- /day-18/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Day 18: Cloud API Registry + ADK 3 | 4 | 核心概念: 5 | - Cloud API Registry = 企业级工具商店 6 | - 管理员统一管理,开发者直接获取已审批的工具 7 | - 简化权限管理和合规性 8 | """ 9 | -------------------------------------------------------------------------------- /shared/__init__.py: -------------------------------------------------------------------------------- 1 | """Shared utilities for the 25-Day Agents Course.""" 2 | 3 | from .config import load_config, get_api_key 4 | 5 | __all__ = ["load_config", "get_api_key"] 6 | -------------------------------------------------------------------------------- /day-16/client/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Day 16: A2A Client for LangGraph Agent 3 | 4 | 测试客户端模块 5 | """ 6 | 7 | from .test_client import test_langgraph_agent 8 | 9 | __all__ = ["test_langgraph_agent"] 10 | -------------------------------------------------------------------------------- /day-02/root_agent.yaml: -------------------------------------------------------------------------------- 1 | name: search_agent 2 | model: gemini-2.0-flash 3 | description: A helpful assistant that can answer user questions. 4 | instruction: | 5 | You are a helpful assistant that can answer user questions. 6 | Always provide clear and concise responses in the user's language. 7 | -------------------------------------------------------------------------------- /day-01/README.md: -------------------------------------------------------------------------------- 1 | # Day 01: Introduction to AI Agents 2 | 3 | ## Learning Goals 4 | 5 | - Set up the development environment 6 | - Understand basic concepts of AI Agents 7 | - Make your first API call to Google Gemini 8 | 9 | ## Running 10 | 11 | ```bash 12 | # From project root 13 | uv run python day-01/main.py 14 | ``` 15 | 16 | ## Notes 17 | 18 | Add your learning notes here... 19 | -------------------------------------------------------------------------------- /day-17/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Day 17: Gemini 3 Flash - 可配置思考级别 3 | 4 | 学习要点: 5 | 1. ThinkingConfig 配置思考级别 6 | 2. BuiltInPlanner 内置规划器 7 | 3. 不同级别的使用场景 8 | """ 9 | 10 | from .thinking_demo import ( 11 | create_agent, 12 | agent_minimal, 13 | agent_low, 14 | agent_high, 15 | ) 16 | 17 | __all__ = [ 18 | "create_agent", 19 | "agent_minimal", 20 | "agent_low", 21 | "agent_high", 22 | ] 23 | -------------------------------------------------------------------------------- /day-16/requirements.txt: -------------------------------------------------------------------------------- 1 | # Day 16: LangGraph + A2A 额外依赖 2 | # 这些依赖是 Day 16 特有的,需要额外安装 3 | 4 | # LangGraph - 图结构 Agent 框架 5 | langgraph>=0.2.0 6 | 7 | # LangChain Google Gemini 集成 8 | langchain-google-genai>=2.0.0 9 | 10 | # LangChain 核心 11 | langchain-core>=0.3.0 12 | 13 | # Human-in-the-Loop 扩展依赖 14 | # FastAPI - REST API 服务 15 | fastapi>=0.104.0 16 | 17 | # Pydantic - 数据验证 18 | pydantic>=2.0.0 19 | 20 | # 可选: SQLite 持久化支持 (生产环境推荐) 21 | # 安装后可使用 CheckpointerType.SQLITE 实现断点续跑 22 | # langgraph-checkpoint-sqlite>=2.0.0 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual environments 24 | .venv/ 25 | venv/ 26 | ENV/ 27 | 28 | # Environment variables 29 | .env 30 | .env.local 31 | 32 | # IDE 33 | .idea/ 34 | .vscode/ 35 | *.swp 36 | *.swo 37 | 38 | # Jupyter 39 | .ipynb_checkpoints/ 40 | 41 | # Testing 42 | .pytest_cache/ 43 | .coverage 44 | htmlcov/ 45 | 46 | # UV 47 | uv.lock 48 | -------------------------------------------------------------------------------- /day-03/root_agent.yaml: -------------------------------------------------------------------------------- 1 | name: search_agent 2 | model: gemini-2.5-flash 3 | description: A helpful assistant that can search the web for current information. 4 | instruction: | 5 | You are a helpful assistant with access to Google Search. 6 | 7 | Use Google Search when users ask about: 8 | - Current events and news 9 | - Weather information 10 | - Stock prices and market data 11 | - Sports scores and results 12 | - Any time-sensitive information 13 | 14 | Always provide clear, accurate responses based on search results. 15 | Cite your sources when appropriate. 16 | tools: 17 | - name: google_search 18 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "agents-course" 3 | version = "0.1.0" 4 | description = "25-Day AI Agents Course by Google - Learning Journey" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "google-genai>=1.0.0", 9 | "google-adk>=1.0.0", 10 | "python-dotenv>=1.0.0", 11 | "google-generativeai>=0.8.5", 12 | "matplotlib>=3.10.8", 13 | ] 14 | 15 | [project.optional-dependencies] 16 | dev = [ 17 | "pytest>=8.0.0", 18 | "ruff>=0.8.0", 19 | ] 20 | 21 | [tool.ruff] 22 | line-length = 100 23 | target-version = "py311" 24 | 25 | [tool.ruff.lint] 26 | select = ["E", "F", "I", "W"] 27 | 28 | [dependency-groups] 29 | dev = [ 30 | "pytest>=8.0.0", 31 | "ruff>=0.8.0", 32 | ] 33 | -------------------------------------------------------------------------------- /day-01/main.py: -------------------------------------------------------------------------------- 1 | """Day 01: Getting Started with Google AI Agents. 2 | 3 | This is the first day of the 25-Day AI Agents Course. 4 | """ 5 | 6 | import sys 7 | from pathlib import Path 8 | 9 | # Add project root to path for imports 10 | sys.path.insert(0, str(Path(__file__).parent.parent)) 11 | 12 | from google import genai 13 | 14 | from shared import get_api_key 15 | 16 | 17 | def main(): 18 | """Main entry point for Day 01.""" 19 | api_key = get_api_key() 20 | client = genai.Client(api_key=api_key) 21 | 22 | # Example: Simple chat with Gemini 23 | response = client.models.generate_content( 24 | model="gemini-2.0-flash", 25 | contents="Hello! I'm starting the 25-Day AI Agents Course. What will I learn?", 26 | ) 27 | print(response.text) 28 | 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /day-06/README.md: -------------------------------------------------------------------------------- 1 | # Day 6: ADK 在各种 IDE 中的集成 2 | 3 | > 构建 Agent 不应该花一小时配置环境。Agent Starter Pack 已经内置了 ADK 的 IDE 上下文。 4 | 5 | ## 今日要点 6 | 7 | Day 6 讲的是如何为 AI IDE 提供项目上下文,让 AI 助手更好地帮你开发 Agent。 8 | 9 | ## 什么是 llms.txt? 10 | 11 | `llms.txt` 是一种为 LLM 提供项目文档的标准格式,类似于给搜索引擎的 `robots.txt`。 12 | 13 | ## IDE 配置文件 14 | 15 | | IDE | 配置文件 | 16 | |-----|----------| 17 | | Cursor | `.cursorrules` | 18 | | Claude Code | `.claude/instructions.md` | 19 | | Windsurf | `.windsurfrules` | 20 | | 通用 | `llms.txt` | 21 | 22 | ## 运行 23 | 24 | ```bash 25 | # 打开 Jupyter notebook 学习 26 | jupyter notebook day-06/day06_ide_context.ipynb 27 | ``` 28 | 29 | ## 资源 30 | 31 | - [llms.txt 规范](https://llmstxt.org/) 32 | - [ADK 文档](https://google.github.io/adk-docs/) 33 | - [Antigravity IDE](https://antigravity.google/) 34 | - [视频](https://www.youtube.com/watch?v=Ep8usBDUTtA) 35 | -------------------------------------------------------------------------------- /day-04/agent/root_agent.yaml: -------------------------------------------------------------------------------- 1 | # Day 04: Agent for Source-Based Deployment 2 | # This agent is configured for deployment to Vertex AI Agent Engine 3 | 4 | name: deployed_assistant 5 | model: gemini-2.5-flash 6 | description: | 7 | A production-ready assistant deployed to Agent Engine. 8 | Demonstrates source-based deployment with Google Search capability. 9 | 10 | instruction: | 11 | You are a helpful production assistant running on Vertex AI Agent Engine. 12 | 13 | Your capabilities: 14 | - Answer questions about current events using Google Search 15 | - Provide factual information with citations 16 | - Help users with general queries 17 | 18 | Guidelines: 19 | - Always cite your sources when using search results 20 | - Be concise and accurate 21 | - If you don't know something, say so clearly 22 | - Present information in a clear, organized format 23 | 24 | tools: 25 | - name: google_search 26 | -------------------------------------------------------------------------------- /shared/config.py: -------------------------------------------------------------------------------- 1 | """Configuration utilities for the course.""" 2 | 3 | import os 4 | from pathlib import Path 5 | 6 | from dotenv import load_dotenv 7 | 8 | 9 | def load_config() -> None: 10 | """Load environment variables from .env file.""" 11 | env_path = Path(__file__).parent.parent / ".env" 12 | load_dotenv(env_path) 13 | 14 | 15 | def get_api_key(key_name: str = "GOOGLE_API_KEY") -> str: 16 | """Get API key from environment variables. 17 | 18 | Args: 19 | key_name: Name of the environment variable containing the API key. 20 | 21 | Returns: 22 | The API key string. 23 | 24 | Raises: 25 | ValueError: If the API key is not found. 26 | """ 27 | load_config() 28 | api_key = os.getenv(key_name) 29 | if not api_key: 30 | raise ValueError( 31 | f"{key_name} not found. Please set it in .env file or environment variables." 32 | ) 33 | return api_key 34 | -------------------------------------------------------------------------------- /day-16/langgraph_agent/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Day 16: LangGraph Agent with A2A support 3 | 4 | 这个模块提供了: 5 | 1. 基于 LangGraph 构建的 ReAct Agent (agent.py) 6 | 2. 生产级 Human-in-the-Loop 实现 (hitl_agent.py) 7 | 3. REST API 服务 (server.py) 8 | 4. 状态持久化支持 (checkpointer.py) 9 | """ 10 | 11 | from .agent import create_langgraph_agent, run_langgraph, langgraph_adk_agent, app 12 | from .hitl_agent import ( 13 | create_hitl_graph, 14 | run_hitl_agent, 15 | resume_after_approval, 16 | RiskLevel, 17 | HITLState, 18 | ) 19 | from .checkpointer import ( 20 | get_checkpointer, 21 | get_task_store, 22 | TaskStore, 23 | TaskMetadata, 24 | CheckpointerType, 25 | ) 26 | 27 | __all__ = [ 28 | # 原有的 ReAct Agent 29 | "create_langgraph_agent", 30 | "run_langgraph", 31 | "langgraph_adk_agent", 32 | "app", 33 | # HITL Agent 34 | "create_hitl_graph", 35 | "run_hitl_agent", 36 | "resume_after_approval", 37 | "RiskLevel", 38 | "HITLState", 39 | # Checkpointer 40 | "get_checkpointer", 41 | "get_task_store", 42 | "TaskStore", 43 | "TaskMetadata", 44 | "CheckpointerType", 45 | ] 46 | -------------------------------------------------------------------------------- /day-09/agent.py: -------------------------------------------------------------------------------- 1 | # Day 9: Undo Buttons for your Agents - Session Rewind 2 | # 3 | # ADK 内置了"时间旅行"功能,让你可以实现: 4 | # - 编辑消息(Edit Message) 5 | # - 重新生成(Regenerate) 6 | # - 撤销操作(Undo) 7 | 8 | from google.adk import Agent 9 | from google.adk.tools.tool_context import ToolContext 10 | from google.genai import types 11 | 12 | 13 | async def update_state(tool_context: ToolContext, key: str, value: str) -> dict: 14 | """更新状态值""" 15 | tool_context.state[key] = value 16 | return {"status": f"已更新 '{key}' 为 '{value}'"} 17 | 18 | 19 | async def load_state(tool_context: ToolContext, key: str) -> dict: 20 | """读取状态值""" 21 | return {key: tool_context.state.get(key, "未找到")} 22 | 23 | 24 | async def save_artifact( 25 | tool_context: ToolContext, filename: str, content: str 26 | ) -> dict: 27 | """保存文件内容""" 28 | artifact_bytes = content.encode("utf-8") 29 | artifact_part = types.Part( 30 | inline_data=types.Blob(mime_type="text/plain", data=artifact_bytes) 31 | ) 32 | version = await tool_context.save_artifact(filename, artifact_part) 33 | return {"status": "成功", "filename": filename, "version": version} 34 | 35 | 36 | async def load_artifact(tool_context: ToolContext, filename: str) -> dict: 37 | """读取文件内容""" 38 | artifact = await tool_context.load_artifact(filename) 39 | if not artifact: 40 | return {"error": f"文件 '{filename}' 未找到"} 41 | content = artifact.inline_data.data.decode("utf-8") 42 | return {"filename": filename, "content": content} 43 | 44 | 45 | # 创建 Agent 46 | root_agent = Agent( 47 | name="state_agent", 48 | model="gemini-2.0-flash", 49 | instruction="""你是一个状态和文件管理 Agent。 50 | 51 | 你可以: 52 | - 更新状态值 (update_state) 53 | - 读取状态值 (load_state) 54 | - 保存文件 (save_artifact) 55 | - 读取文件 (load_artifact) 56 | 57 | 根据用户的请求使用相应的工具。""", 58 | tools=[ 59 | update_state, 60 | load_state, 61 | save_artifact, 62 | load_artifact, 63 | ], 64 | ) 65 | -------------------------------------------------------------------------------- /day-17/README.md: -------------------------------------------------------------------------------- 1 | # Day 17: Gemini 3 Flash - 可配置思考级别 2 | 3 | ## 核心概念 4 | 5 | Gemini 3 Flash 是 Google 最新的快速模型,最大亮点是**可配置的思考级别(Thinking Level)**。 6 | 7 | ### 什么是 Thinking Level? 8 | 9 | 简单说:控制模型"想多久"再回答。 10 | 11 | | 级别 | 特点 | 适用场景 | 12 | |------|------|----------| 13 | | `MINIMAL` | 最快响应,几乎不思考 | 简单问答、聊天 | 14 | | `LOW` | 快速响应,少量推理 | 日常任务、一般查询 | 15 | | `MEDIUM` | 平衡模式 | 中等复杂度任务 | 16 | | `HIGH` | 深度推理,更长思考 | 复杂问题、数学、编程 | 17 | 18 | ### 为什么重要? 19 | 20 | 1. **省钱** - 低思考级别消耗更少 token 21 | 2. **更快** - 简单任务不需要深度思考 22 | 3. **更准** - 复杂任务可以开启深度推理 23 | 24 | ## 在 ADK 中使用 25 | 26 | ```python 27 | from google.adk.agents import Agent 28 | from google.adk.planners import BuiltInPlanner 29 | from google.genai import types 30 | 31 | agent = Agent( 32 | model='gemini-3-flash-preview', 33 | name='my_agent', 34 | instruction="你是一个有用的助手", 35 | planner=BuiltInPlanner( 36 | thinking_config=types.ThinkingConfig( 37 | thinking_level="LOW" # 可选: MINIMAL, LOW, MEDIUM, HIGH 38 | ) 39 | ), 40 | ) 41 | ``` 42 | 43 | ## 关键配置 44 | 45 | ### ThinkingConfig 参数 46 | 47 | ```python 48 | types.ThinkingConfig( 49 | thinking_level="HIGH" # 思考级别 50 | ) 51 | ``` 52 | 53 | ### BuiltInPlanner 54 | 55 | - ADK 的内置规划器 56 | - 用于配置模型的思考行为 57 | - 通过 `thinking_config` 传入配置 58 | 59 | ## 与 Gemini 2.5 的区别 60 | 61 | | 特性 | Gemini 2.5 | Gemini 3 | 62 | |------|------------|----------| 63 | | 配置参数 | `thinking_budget` | `thinking_level` | 64 | | 控制方式 | token 预算 | 级别选择 | 65 | | 效率 | 基准 | 平均减少 30% token | 66 | 67 | **注意**: 参数不通用!Gemini 3 用 `thinking_level`,Gemini 2.5 用 `thinking_budget`。 68 | 69 | ## 定价 70 | 71 | - 输入: $0.50 / 百万 token 72 | - 输出: $3.00 / 百万 token 73 | - 比 Gemini 3 Pro 便宜 4 倍 74 | 75 | ## 文件说明 76 | 77 | | 文件 | 说明 | 78 | |------|------| 79 | | `gemini3_flash_thinking.ipynb` | 基础教程 - 思考级别入门 | 80 | | `adaptive_thinking_demo.ipynb` | 扩展案例 - 智能客服自适应思考 | 81 | | `thinking_demo.py` | Python 脚本版本 | 82 | 83 | ## 运行示例 84 | 85 | ```bash 86 | # 安装依赖 87 | pip install google-adk google-genai python-dotenv 88 | 89 | # 设置 API Key 90 | export GOOGLE_API_KEY=your_key_here 91 | 92 | # 运行 Jupyter 93 | jupyter notebook 94 | ``` 95 | 96 | ## 参考资料 97 | 98 | - [Gemini 3 Flash 官方公告](https://blog.google/products/gemini/gemini-3-flash/) 99 | - [ADK 模型配置文档](https://google.github.io/adk-docs/agents/models/) 100 | - [多智能体系统指南](https://developers.googleblog.com/developers-guide-to-multi-agent-patterns-in-adk/) 101 | - [TechCrunch 报道](https://techcrunch.com/2025/12/17/google-launches-gemini-3-flash-makes-it-the-default-model-in-the-gemini-app/) 102 | -------------------------------------------------------------------------------- /day-14/remote_agent/agent.py: -------------------------------------------------------------------------------- 1 | """ 2 | Day 14: Remote Agent (A2A Server) 3 | 4 | 这是一个专业的翻译 Agent,通过 A2A 协议对外提供服务。 5 | 其他 Agent 可以通过 A2A 协议调用它来完成翻译任务。 6 | 7 | 运行方式: 8 | python -m remote_agent.agent 9 | 或 10 | uvicorn remote_agent.agent:app --host localhost --port 8001 11 | """ 12 | 13 | import os 14 | import sys 15 | 16 | # 添加项目根目录到路径 17 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 18 | 19 | # 加载 .env 文件中的环境变量 20 | from dotenv import load_dotenv 21 | # 从项目根目录加载 .env 22 | project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 23 | load_dotenv(os.path.join(project_root, ".env")) 24 | 25 | from google.adk.agents import LlmAgent 26 | from google.adk.a2a.utils.agent_to_a2a import to_a2a 27 | 28 | # ============================================================ 29 | # 1. 定义远程 Agent - 专业翻译助手 30 | # ============================================================ 31 | 32 | translator_agent = LlmAgent( 33 | name="translator", 34 | description="专业多语言翻译 Agent - 精通中、英、日、韩、法、德等多种语言的翻译服务", 35 | model="gemini-2.0-flash", 36 | instruction="""你是一位专业的多语言翻译专家。 37 | 38 | 你的能力: 39 | - 精通中文、英文、日文、韩文、法文、德文等主流语言 40 | - 能够准确理解原文的语境和文化背景 41 | - 翻译时保持原文的风格和语气 42 | - 可以处理技术文档、商务文件、文学作品等不同类型的内容 43 | 44 | 翻译原则: 45 | 1. 信:准确传达原文的意思 46 | 2. 达:翻译通顺流畅 47 | 3. 雅:保持文字的优美和得体 48 | 49 | 当用户提供文本时: 50 | - 首先识别源语言 51 | - 询问目标语言(如果用户没有指定) 52 | - 提供高质量的翻译结果 53 | - 必要时提供翻译说明或文化背景解释 54 | 55 | 示例: 56 | 用户:请把 "Hello, how are you?" 翻译成中文 57 | 回复:你好,你怎么样?/ 你好,最近好吗?(更口语化的表达) 58 | """, 59 | ) 60 | 61 | # ============================================================ 62 | # 2. 使用 to_a2a() 将 Agent 转换为 A2A 服务 63 | # ============================================================ 64 | 65 | # 关键函数:to_a2a() 66 | # 这是 ADK 提供的便捷方法,只需一行代码即可将 Agent 转换为 A2A 兼容的服务 67 | # 它会自动: 68 | # - 创建 Agent Card(描述 Agent 能力的元数据) 69 | # - 设置 HTTP 端点 70 | # - 处理 A2A 协议的请求/响应转换 71 | 72 | app = to_a2a( 73 | agent=translator_agent, 74 | host="localhost", 75 | port=8001, 76 | protocol="http" 77 | ) 78 | 79 | # ============================================================ 80 | # 3. 启动服务 81 | # ============================================================ 82 | 83 | if __name__ == "__main__": 84 | import uvicorn 85 | 86 | print("=" * 60) 87 | print("Day 14: A2A Remote Agent - 翻译服务") 88 | print("=" * 60) 89 | print() 90 | print("Agent 信息:") 91 | print(f" 名称: {translator_agent.name}") 92 | print(f" 描述: {translator_agent.description}") 93 | print() 94 | print("A2A 服务端点:") 95 | print(" URL: http://localhost:8001") 96 | print(" Agent Card: http://localhost:8001/.well-known/agent.json") 97 | print() 98 | print("提示: 在另一个终端运行 host_agent 来测试 A2A 通信") 99 | print("=" * 60) 100 | print() 101 | 102 | # 启动 uvicorn 服务器 103 | uvicorn.run(app, host="localhost", port=8001) 104 | -------------------------------------------------------------------------------- /day-07/README.md: -------------------------------------------------------------------------------- 1 | # Day 7: LLM 可以执行代码 - 自主问题解决 2 | 3 | > 探索 LLM 如何不仅能编写代码,还能自主执行、调试和优化代码,将其转变为强大的问题解决者。 4 | 5 | ## 今日要点 6 | 7 | Day 7 讲的是 **Code Execution(代码执行)** 功能 - 让 AI Agent 能够在安全的沙箱环境中运行代码,从而实现自主问题解决。 8 | 9 | ## 什么是 Code Execution? 10 | 11 | 传统 LLM 只能**生成**代码,但不能**运行**代码。Code Execution 改变了这一点: 12 | 13 | ``` 14 | ┌─────────────────────────────────────────────────────────────────────────┐ 15 | │ 没有 Code Execution │ 16 | ├─────────────────────────────────────────────────────────────────────────┤ 17 | │ │ 18 | │ 用户: "计算 2024 年 1 月 1 日到今天有多少天?" │ 19 | │ │ 20 | │ AI: "让我算一下... 大约是 350 天左右" ← 可能算错! │ 21 | │ │ 22 | └─────────────────────────────────────────────────────────────────────────┘ 23 | 24 | ┌─────────────────────────────────────────────────────────────────────────┐ 25 | │ 有 Code Execution │ 26 | ├─────────────────────────────────────────────────────────────────────────┤ 27 | │ │ 28 | │ 用户: "计算 2024 年 1 月 1 日到今天有多少天?" │ 29 | │ │ 30 | │ AI: [自动生成并执行 Python 代码] │ 31 | │ ```python │ 32 | │ from datetime import date │ 33 | │ days = (date.today() - date(2024, 1, 1)).days │ 34 | │ print(f"已经过了 {days} 天") │ 35 | │ ``` │ 36 | │ 输出: "已经过了 351 天" ← 精确计算! │ 37 | │ │ 38 | └─────────────────────────────────────────────────────────────────────────┘ 39 | ``` 40 | 41 | ## 核心优势 42 | 43 | | 优势 | 说明 | 44 | |------|------| 45 | | **精确计算** | 数学运算由代码执行,避免 LLM 的计算错误 | 46 | | **数据处理** | 可以处理 CSV、JSON 等数据文件 | 47 | | **动态验证** | 生成代码后立即验证是否正确 | 48 | | **迭代调试** | 如果出错,可以自动修复并重试 | 49 | 50 | ## 运行 51 | 52 | ```bash 53 | # 打开 Jupyter notebook 学习 54 | jupyter notebook day-07/code_execution.ipynb 55 | ``` 56 | 57 | ## 资源 58 | 59 | - [Code Execution on Vertex AI Agent Engine](https://cloud.google.com/vertex-ai/docs/agent-engine/code-execution) 60 | - [Tutorial: Get Started with Code Execution](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/agents/agent_engine/tutorial_get_started_with_code_execution.ipynb) 61 | - [Retail AI Location Strategy (Case Study)](https://github.com/google/adk-samples/tree/main/python/agents/retail-ai-location-strategy) 62 | - [视频](https://www.youtube.com/watch?v=QH9jK_RkbHc) 63 | -------------------------------------------------------------------------------- /assets/difficulty_curve.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 25天课程难度曲线 9 | 10 | 难度 11 | 12 | 天数 13 | 14 | Day 1-7 15 | 基础 + 部署 16 | 17 | Day 8-13 18 | 高级特性 19 | 20 | Day 14-16 21 | 多Agent + A2A 22 | 23 | Day 17-18 24 | Gemini 3 + Registry 25 | 26 | Day 19-25 27 | 企业级应用 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /day-14/run_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Day 14: A2A 协议演示 - 一键运行脚本 4 | 5 | 这个脚本会: 6 | 1. 启动远程 Agent (A2A Server) 在后台 7 | 2. 等待服务就绪 8 | 3. 运行 Host Agent (A2A Client) 进行测试 9 | 4. 清理并退出 10 | 11 | 运行方式: 12 | python run_demo.py 13 | """ 14 | 15 | import asyncio 16 | import subprocess 17 | import sys 18 | import time 19 | import os 20 | import signal 21 | 22 | # 确保使用正确的 Python 环境 23 | PYTHON = sys.executable 24 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 25 | 26 | 27 | async def check_server_ready(url: str, max_attempts: int = 30) -> bool: 28 | """检查服务器是否就绪""" 29 | import httpx 30 | 31 | for i in range(max_attempts): 32 | try: 33 | async with httpx.AsyncClient() as client: 34 | response = await client.get(f"{url}/.well-known/agent.json") 35 | if response.status_code == 200: 36 | print(f" ✅ 服务器就绪!") 37 | return True 38 | except Exception: 39 | pass 40 | print(f" 等待服务器启动... ({i + 1}/{max_attempts})") 41 | await asyncio.sleep(1) 42 | return False 43 | 44 | 45 | async def run_demo(): 46 | """运行完整的 A2A 演示""" 47 | print("=" * 60) 48 | print("Day 14: A2A 协议演示") 49 | print("=" * 60) 50 | print() 51 | 52 | server_process = None 53 | 54 | try: 55 | # 步骤 1: 启动远程 Agent 56 | print("📡 步骤 1: 启动远程 Agent (A2A Server)...") 57 | print() 58 | 59 | server_process = subprocess.Popen( 60 | [PYTHON, "-m", "remote_agent.agent"], 61 | cwd=BASE_DIR, 62 | stdout=subprocess.PIPE, 63 | stderr=subprocess.STDOUT, 64 | text=True, 65 | ) 66 | 67 | # 等待服务器就绪 68 | print(" 等待服务器启动...") 69 | if not await check_server_ready("http://localhost:8001"): 70 | print(" ❌ 服务器启动失败!") 71 | return 72 | 73 | print() 74 | 75 | # 步骤 2: 运行客户端测试 76 | print("🖥️ 步骤 2: 运行 Host Agent (A2A Client)...") 77 | print() 78 | 79 | # 导入并运行 host agent 80 | sys.path.insert(0, BASE_DIR) 81 | from host_agent.agent import main as host_main 82 | await host_main() 83 | 84 | except KeyboardInterrupt: 85 | print("\n\n⚠️ 用户中断...") 86 | 87 | except Exception as e: 88 | print(f"\n❌ 发生错误: {e}") 89 | import traceback 90 | traceback.print_exc() 91 | 92 | finally: 93 | # 清理 94 | print("\n🧹 清理资源...") 95 | if server_process: 96 | server_process.terminate() 97 | try: 98 | server_process.wait(timeout=5) 99 | except subprocess.TimeoutExpired: 100 | server_process.kill() 101 | print(" 远程 Agent 已停止") 102 | 103 | print() 104 | print("=" * 60) 105 | print("演示结束!") 106 | print("=" * 60) 107 | 108 | 109 | if __name__ == "__main__": 110 | # 检查依赖 111 | try: 112 | import httpx 113 | except ImportError: 114 | print("正在安装 httpx...") 115 | subprocess.run([PYTHON, "-m", "pip", "install", "httpx"], check=True) 116 | 117 | asyncio.run(run_demo()) 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 25-Day AI Agents Course by Google 2 | 3 | A hands-on learning journey through Google's AI Agents capabilities. 4 | 5 | > **Official Course**: [Advent of Agents 2025](https://adventofagents.com/) - 25 days of Zero to Production-Ready AI Agents on Google Cloud 6 | 7 | ## About This Course 8 | 9 | This is Google Cloud's **Advent of Agents 2025** program - a 25-day journey to master AI Agents using: 10 | - **Gemini 3** - Google's latest AI models 11 | - **Agent Development Kit (ADK)** - Comprehensive agent development platform 12 | - **Agent Engine** - Production deployment infrastructure 13 | 14 | ### Course Highlights 15 | - 🎯 One feature per day, each taking less than 5 minutes to try 16 | - 📋 Copy-paste commands that work out of the box 17 | - 📚 Links to official documentation 18 | - 🆓 100% free 19 | 20 | > 📖 **Prerequisite**: [5-Day AI Agents Intensive Course](https://github.com/anxiong2025/5-Day-AI-Agents-Intensive-Course-with-Google) - Google's foundational course on AI Agents 21 | 22 | ### Difficulty Curve 23 | 24 |

25 | Course Difficulty Curve 26 |

27 | 28 | ## Setup 29 | 30 | ### Prerequisites 31 | 32 | - Python 3.11+ 33 | - [uv](https://github.com/astral-sh/uv) package manager 34 | 35 | ### Installation 36 | 37 | ```bash 38 | # Install dependencies 39 | uv sync 40 | 41 | # Create .env file and add your API key 42 | cp .env.example .env 43 | # Edit .env and add your GOOGLE_API_KEY 44 | ``` 45 | 46 | ## Project Structure 47 | 48 | ``` 49 | . 50 | ├── day-01/ # Day 1: Introduction 51 | ├── day-02/ # Day 2: ... 52 | ├── ... 53 | ├── shared/ # Shared utilities 54 | │ ├── __init__.py 55 | │ └── config.py # Configuration helpers 56 | ├── pyproject.toml # Project dependencies 57 | └── README.md 58 | ``` 59 | 60 | ## Daily Progress 61 | 62 | | Day | Topic | Status | 63 | |-----|-------|--------| 64 | | 01 | Introduction to AI Agents | ✅ Done | 65 | | 02 | YAML Agent Configuration | ✅ Done | 66 | | 03 | Gemini Search Agent | ✅ Done | 67 | | 04 | Agent Engine Deployment | ✅ Done | 68 | | 05 | Telemetry & Tracing | ✅ Done | 69 | | 06 | ADK IDE Integration | ✅ Done | 70 | | 07 | Code Execution | ✅ Done | 71 | | 08 | Context Management | ✅ Done | 72 | | 09 | Session Rewind | ✅ Done | 73 | | 10 | Context Caching & Compaction | ✅ Done | 74 | | 11 | Google Managed MCP | ✅ Done | 75 | | 12 | Multimodal Streaming Agents | ✅ Done | 76 | | 13 | Interactions API | ✅ Done | 77 | | 14 | A2A Remote Agents | ✅ Done | 78 | | 15 | Agent-to-UI | ✅ Done | 79 | | 16 | LangGraph + A2A | ✅ Done | 80 | | 17 | Gemini 3 Flash Thinking Levels | ✅ Done | 81 | | 18 | Cloud API Registry + ADK | ✅ Done | 82 | | 19-25 | Enterprise Topics | ⏳ Pending | 83 | 84 | ## Running Daily Exercises 85 | 86 | ```bash 87 | # Run day 1 exercises 88 | uv run python day-01/main.py 89 | 90 | # Run with dev dependencies (for testing) 91 | uv sync --dev 92 | uv run pytest 93 | ``` 94 | 95 | ## Resources 96 | 97 | - [Advent of Agents 2025](https://adventofagents.com/) - Official course website 98 | - [Google AI Studio](https://aistudio.google.com/) 99 | - [Gemini API Documentation](https://ai.google.dev/docs) 100 | - [Agent Development Kit (ADK)](https://google.github.io/adk-docs/) 101 | -------------------------------------------------------------------------------- /day-18/README.md: -------------------------------------------------------------------------------- 1 | # Day 18: Cloud API Registry + ADK 2 | 3 | ## 一句话理解 4 | 5 | **Cloud API Registry = 企业级工具商店** 6 | 7 | 以前:每个开发者自己找 API、配置权限、管理工具 8 | 现在:公司统一管理,开发者直接"领取"已审批的工具 9 | 10 | ## 核心概念 11 | 12 | ### 问题:企业开发 Agent 最大的痛点是什么? 13 | 14 | 不是模型,是**工具管理**: 15 | - 哪些 API 可以用? 16 | - 谁有权限用? 17 | - 怎么保证安全合规? 18 | 19 | ### 解决方案:Cloud API Registry 20 | 21 | ``` 22 | ┌─────────────────────────────────────────┐ 23 | │ Cloud API Registry │ 24 | │ (企业工具仓库 - 管理员统一管理) │ 25 | ├─────────────────────────────────────────┤ 26 | │ BigQuery Tool ✅ 已审批 │ 27 | │ Cloud Storage ✅ 已审批 │ 28 | │ Gmail API ❌ 未授权 │ 29 | │ 自定义内部API ✅ 已审批 │ 30 | └─────────────────────────────────────────┘ 31 | │ 32 | ▼ 33 | ┌─────────────────────────────────────────┐ 34 | │ 开发者的 Agent │ 35 | │ registry.get_tool("bigquery") │ 36 | │ → 直接获取已配置好的工具 │ 37 | └─────────────────────────────────────────┘ 38 | ``` 39 | 40 | ## 两个角色 41 | 42 | | 角色 | 职责 | 43 | |------|------| 44 | | **管理员** | 在 Cloud Console 审批/管理可用工具 | 45 | | **开发者** | 用 `ApiRegistry` 获取已审批的工具 | 46 | 47 | ## 使用步骤 48 | 49 | ### 步骤 1:管理员启用 MCP 服务 50 | 51 | ```bash 52 | # 启用 BigQuery 的 MCP 服务 53 | gcloud beta services mcp enable bigquery.googleapis.com \ 54 | --project=YOUR_PROJECT_ID 55 | ``` 56 | 57 | ### 步骤 2:开发者使用工具 58 | 59 | ```python 60 | from google.adk import Agent 61 | from google.adk.tools.google_cloud import ApiRegistry 62 | 63 | # 1. 连接到企业的工具仓库 64 | registry = ApiRegistry(project_id="your-project-id") 65 | 66 | # 2. 获取已审批的工具 67 | bq_tool = registry.get_tool("google-bigquery") 68 | 69 | # 3. 给 Agent 装上这个工具 70 | agent = Agent( 71 | model="gemini-3-pro", 72 | tools=[bq_tool] 73 | ) 74 | ``` 75 | 76 | 就这么简单!不用自己配置 API 密钥、权限等。 77 | 78 | ## 支持的 Google Cloud 工具 79 | 80 | | 工具 | 用途 | 81 | |------|------| 82 | | `google-bigquery` | 数据仓库查询 | 83 | | `google-cloud-storage` | 文件存储 | 84 | | `google-spanner` | 分布式数据库 | 85 | | `google-youtube` | YouTube 数据 | 86 | | 更多... | 持续增加中 | 87 | 88 | ## 核心优势 89 | 90 | ### 1. 统一发现 91 | 所有可用工具在一个地方,不用到处找文档 92 | 93 | ### 2. 集中治理 94 | 管理员控制谁能用什么工具,保证合规 95 | 96 | ### 3. 简化集成 97 | 开发者不用关心底层配置,拿来就用 98 | 99 | ## 与普通方式对比 100 | 101 | ### 以前(手动配置) 102 | ```python 103 | # 需要自己处理认证、配置 104 | from google.cloud import bigquery 105 | client = bigquery.Client(project="xxx", credentials=xxx) 106 | # 还要自己封装成 Agent 工具... 107 | ``` 108 | 109 | ### 现在(API Registry) 110 | ```python 111 | # 一行搞定 112 | bq_tool = registry.get_tool("google-bigquery") 113 | ``` 114 | 115 | ## 适用场景 116 | 117 | - 企业级 Agent 开发 118 | - 需要统一管理 API 权限 119 | - 多团队协作开发 120 | - 合规性要求高的项目 121 | 122 | ## 前置要求 123 | 124 | 1. Google Cloud 项目 125 | 2. 启用 Cloud API Registry 126 | 3. 配置好 `gcloud` CLI 认证 127 | 128 | ## 参考资料 129 | 130 | - [Cloud API Registry 文档](https://docs.cloud.google.com/api-registry/docs/overview) 131 | - [ADK Cloud API Registry 指南](https://google.github.io/adk-docs/tools/google-cloud/api-registry/) 132 | - [Tool Governance 博客](https://cloud.google.com/blog/products/ai-machine-learning/new-enhanced-tool-governance-in-vertex-ai-agent-builder) 133 | - [官方教程 Notebook](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/agents/agent_engine/tutorial_get_started_with_cloud_api_registry.ipynb) 134 | -------------------------------------------------------------------------------- /day-02/README.md: -------------------------------------------------------------------------------- 1 | # Day 02: Hello World with YAML 2 | 3 | Build your first AI agent with Gemini 3 in under 5 minutes without writing a single line of code. 4 | 5 | ## Learning Goals 6 | 7 | - Understand ADK Agent Config (YAML-based agents) 8 | - Create a simple agent using YAML configuration 9 | - Use built-in tools like Google Search 10 | - Run and test your agent with `adk web` 11 | 12 | ## Prerequisites 13 | 14 | ```bash 15 | pip install google-adk 16 | ``` 17 | 18 | ## Quick Start 19 | 20 | ### 1. Create a YAML-based Agent 21 | 22 | Use the ADK CLI to create a config-based agent: 23 | 24 | ```bash 25 | adk create my_agent --type=config 26 | ``` 27 | 28 | This generates: 29 | - `my_agent/root_agent.yaml` - Agent configuration 30 | - `my_agent/.env` - Environment variables 31 | 32 | ### 2. Basic YAML Structure 33 | 34 | ```yaml 35 | name: assistant_agent 36 | model: gemini-2.5-flash 37 | description: A helper agent that can answer users' questions. 38 | instruction: You are an agent to help answer users' various questions. 39 | ``` 40 | 41 | ### 3. Run Your Agent 42 | 43 | ```bash 44 | cd day-02 45 | adk web 46 | ``` 47 | 48 | Open the browser and select your agent from the dropdown. 49 | 50 | ## YAML Configuration Options 51 | 52 | | Field | Description | 53 | |-------|-------------| 54 | | `name` | Agent identifier | 55 | | `model` | Gemini model to use (e.g., `gemini-2.5-flash`) | 56 | | `description` | Brief description of the agent | 57 | | `instruction` | System prompt / behavior instructions | 58 | | `tools` | List of tools the agent can use | 59 | 60 | ## Built-in Tools 61 | 62 | ADK supports these built-in tools: 63 | 64 | - `google_search` - Search the web for information 65 | - `code_execution` - Execute Python code 66 | - `vertex_ai_search` - Search using Vertex AI 67 | 68 | ## Example: Agent with Google Search 69 | 70 | See [root_agent.yaml](root_agent.yaml) for a working example. 71 | 72 | ```yaml 73 | name: search_agent 74 | model: gemini-2.5-flash 75 | description: A helpful assistant that can search the web. 76 | instruction: | 77 | You are a helpful assistant. 78 | Use Google Search for current events and factual information. 79 | tools: 80 | - google_search 81 | ``` 82 | 83 | ## Advanced: Mixing Python with YAML 84 | 85 | You can add custom Python tools to your YAML agent: 86 | 87 | 1. Create `tools.py` in your agent folder: 88 | 89 | ```python 90 | def get_weather(city: str) -> str: 91 | """Get weather for a city.""" 92 | return f"The weather in {city} is sunny." 93 | ``` 94 | 95 | 2. Reference it in `root_agent.yaml`: 96 | 97 | ```yaml 98 | tools: 99 | - google_search 100 | - tools.get_weather 101 | ``` 102 | 103 | ## Advanced: MCP Server Integration 104 | 105 | Connect to an MCP server for additional tools: 106 | 107 | ```yaml 108 | tools: 109 | - type: MCPToolset 110 | stdio_server_params: 111 | command: uvx 112 | args: 113 | - mcp-server-time 114 | ``` 115 | 116 | ## Limitations 117 | 118 | - Currently only supports Gemini models 119 | - Some advanced features require Python code 120 | - Experimental feature - API may change 121 | 122 | ## Resources 123 | 124 | - [ADK Agent Config Documentation](https://google.github.io/adk-docs/agents/config/) 125 | - [2-Minute ADK: YAML Tutorial](https://medium.com/google-cloud/2-minute-adk-build-agents-the-easy-way-yaml-a55678d64a75) 126 | - [Third Party MCP Tools in ADK](https://google.github.io/adk-docs/tools/third-party/) 127 | - [ADK Samples Repository](https://github.com/google/adk-samples) 128 | 129 | ## Notes 130 | 131 | Day 2 introduces the no-code approach to building agents using YAML configuration. This is the fastest way to prototype AI agents with Google ADK. 132 | -------------------------------------------------------------------------------- /day-04/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Day 04: Source-Based Deployment Script 3 | # Deploy ADK agent to Vertex AI Agent Engine 4 | 5 | set -e 6 | 7 | # Configuration - Update these values 8 | export GOOGLE_CLOUD_PROJECT="${GOOGLE_CLOUD_PROJECT:-your-project-id}" 9 | export GOOGLE_CLOUD_LOCATION="${GOOGLE_CLOUD_LOCATION:-us-central1}" 10 | export STAGING_BUCKET="${STAGING_BUCKET:-gs://your-staging-bucket}" 11 | export AGENT_NAME="deployed_assistant" 12 | 13 | echo "==========================================" 14 | echo "Day 04: Source-Based Deployment" 15 | echo "==========================================" 16 | echo "Project: $GOOGLE_CLOUD_PROJECT" 17 | echo "Region: $GOOGLE_CLOUD_LOCATION" 18 | echo "Bucket: $STAGING_BUCKET" 19 | echo "Agent: $AGENT_NAME" 20 | echo "==========================================" 21 | 22 | # Check prerequisites 23 | check_prerequisites() { 24 | echo "Checking prerequisites..." 25 | 26 | # Check gcloud 27 | if ! command -v gcloud &> /dev/null; then 28 | echo "Error: gcloud CLI not found. Install from https://cloud.google.com/sdk/docs/install" 29 | exit 1 30 | fi 31 | 32 | # Check uv 33 | if ! command -v uv &> /dev/null; then 34 | echo "Error: uv not found. Install with: curl -LsSf https://astral.sh/uv/install.sh | sh" 35 | exit 1 36 | fi 37 | 38 | # Check adk 39 | if ! command -v adk &> /dev/null; then 40 | echo "Installing google-adk..." 41 | pip install google-adk 42 | fi 43 | 44 | echo "All prerequisites met!" 45 | } 46 | 47 | # Authenticate with Google Cloud 48 | authenticate() { 49 | echo "Authenticating with Google Cloud..." 50 | gcloud auth login 51 | gcloud config set project $GOOGLE_CLOUD_PROJECT 52 | gcloud auth application-default login 53 | } 54 | 55 | # Create staging bucket if needed 56 | create_bucket() { 57 | BUCKET_NAME=$(echo $STAGING_BUCKET | sed 's|gs://||') 58 | if ! gsutil ls $STAGING_BUCKET &> /dev/null; then 59 | echo "Creating staging bucket: $STAGING_BUCKET" 60 | gsutil mb -l $GOOGLE_CLOUD_LOCATION $STAGING_BUCKET 61 | else 62 | echo "Staging bucket exists: $STAGING_BUCKET" 63 | fi 64 | } 65 | 66 | # Option 1: Deploy with ADK CLI 67 | deploy_with_adk() { 68 | echo "Deploying with ADK CLI..." 69 | adk deploy agent_engine \ 70 | --project $GOOGLE_CLOUD_PROJECT \ 71 | --region $GOOGLE_CLOUD_LOCATION \ 72 | --staging_bucket $STAGING_BUCKET \ 73 | --trace_to_cloud \ 74 | --display_name "$AGENT_NAME" \ 75 | --description "Day 04 deployed assistant" \ 76 | agent 77 | } 78 | 79 | # Option 2: Enhance existing project with Agent Starter Pack 80 | enhance_project() { 81 | echo "Enhancing project with Agent Starter Pack..." 82 | cd .. 83 | uvx agent-starter-pack enhance --adk -d agent_engine 84 | cd day-04 85 | } 86 | 87 | # Option 3: Create new project with Agent Starter Pack 88 | create_new_project() { 89 | echo "Creating new project with Agent Starter Pack..." 90 | uvx agent-starter-pack create $AGENT_NAME -a adk_base -d agent_engine 91 | } 92 | 93 | # Test locally before deployment 94 | test_locally() { 95 | echo "Testing agent locally..." 96 | cd agent 97 | adk web 98 | } 99 | 100 | # Main menu 101 | main() { 102 | echo "" 103 | echo "Choose an action:" 104 | echo "1) Check prerequisites" 105 | echo "2) Authenticate with Google Cloud" 106 | echo "3) Test agent locally" 107 | echo "4) Deploy with ADK CLI" 108 | echo "5) Enhance project with Agent Starter Pack" 109 | echo "6) Create new project with Agent Starter Pack" 110 | echo "q) Quit" 111 | echo "" 112 | read -p "Enter choice: " choice 113 | 114 | case $choice in 115 | 1) check_prerequisites ;; 116 | 2) authenticate ;; 117 | 3) test_locally ;; 118 | 4) 119 | check_prerequisites 120 | create_bucket 121 | deploy_with_adk 122 | ;; 123 | 5) enhance_project ;; 124 | 6) create_new_project ;; 125 | q) exit 0 ;; 126 | *) echo "Invalid choice" ;; 127 | esac 128 | } 129 | 130 | # Run main menu 131 | main 132 | -------------------------------------------------------------------------------- /day-09/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Day 9: Undo Buttons for your Agents - Session Rewind 演示 4 | 5 | 这个示例展示了 ADK 的 Session Rewind(会话回溯)功能: 6 | 1. 创建会话并设置初始状态 7 | 2. 修改状态 8 | 3. 使用 rewind 回溯到之前的状态 9 | 4. 验证状态已恢复 10 | 11 | 这个功能可以用来实现: 12 | - "编辑消息" 功能 13 | - "重新生成" 功能 14 | - 任何需要撤销的场景 15 | """ 16 | 17 | import asyncio 18 | 19 | import agent 20 | from google.adk.agents.run_config import RunConfig 21 | from google.adk.events.event import Event 22 | from google.adk.runners import InMemoryRunner 23 | from google.genai import types 24 | 25 | APP_NAME = "rewind_demo" 26 | USER_ID = "demo_user" 27 | 28 | 29 | async def call_agent( 30 | runner: InMemoryRunner, user_id: str, session_id: str, prompt: str 31 | ) -> list[Event]: 32 | """调用 Agent 并返回事件列表""" 33 | print(f"\n👤 用户: {prompt}") 34 | content = types.Content( 35 | role="user", parts=[types.Part.from_text(text=prompt)] 36 | ) 37 | events = [] 38 | async for event in runner.run_async( 39 | user_id=user_id, 40 | session_id=session_id, 41 | new_message=content, 42 | run_config=RunConfig(), 43 | ): 44 | events.append(event) 45 | if event.content and event.author and event.author != "user": 46 | for part in event.content.parts: 47 | if part.text: 48 | print(f" 🤖 Agent: {part.text}") 49 | elif part.function_call: 50 | print(f" 🛠️ 工具调用: {part.function_call.name}") 51 | elif part.function_response: 52 | print(f" 📦 工具响应: {part.function_response.response}") 53 | return events 54 | 55 | 56 | async def main(): 57 | """演示 Session Rewind 功能""" 58 | print("=" * 60) 59 | print("🎯 Day 9: Session Rewind (会话回溯) 功能演示") 60 | print("=" * 60) 61 | 62 | # 创建 Runner 63 | runner = InMemoryRunner( 64 | agent=agent.root_agent, 65 | app_name=APP_NAME, 66 | ) 67 | 68 | # 创建会话 69 | session = await runner.session_service.create_session( 70 | app_name=APP_NAME, user_id=USER_ID 71 | ) 72 | print(f"\n📝 创建会话: {session.id}") 73 | 74 | # ===== 步骤 1: 初始化状态 ===== 75 | print("\n" + "=" * 60) 76 | print("步骤 1: 初始化状态") 77 | print("=" * 60) 78 | await call_agent(runner, USER_ID, session.id, "设置状态 color 为 red") 79 | await call_agent(runner, USER_ID, session.id, "保存文件 note.txt 内容为 version1") 80 | 81 | # ===== 步骤 2: 检查当前状态 ===== 82 | print("\n" + "=" * 60) 83 | print("步骤 2: 检查当前状态") 84 | print("=" * 60) 85 | await call_agent(runner, USER_ID, session.id, "查询状态 color 的值") 86 | await call_agent(runner, USER_ID, session.id, "读取文件 note.txt") 87 | 88 | # ===== 步骤 3: 修改状态 (这是我们要回溯的点) ===== 89 | print("\n" + "=" * 60) 90 | print("步骤 3: 修改状态 (⚠️ 这是回溯点)") 91 | print("=" * 60) 92 | events_update = await call_agent( 93 | runner, USER_ID, session.id, "更新状态 color 为 blue" 94 | ) 95 | rewind_invocation_id = events_update[0].invocation_id 96 | print(f"\n📌 记录回溯点 invocation_id: {rewind_invocation_id}") 97 | 98 | await call_agent(runner, USER_ID, session.id, "保存文件 note.txt 内容为 version2") 99 | 100 | # ===== 步骤 4: 检查修改后的状态 ===== 101 | print("\n" + "=" * 60) 102 | print("步骤 4: 检查修改后的状态") 103 | print("=" * 60) 104 | await call_agent(runner, USER_ID, session.id, "查询状态 color 的值") 105 | await call_agent(runner, USER_ID, session.id, "读取文件 note.txt") 106 | 107 | # ===== 步骤 5: 执行回溯 ===== 108 | print("\n" + "=" * 60) 109 | print("步骤 5: ⏪ 执行 REWIND 回溯") 110 | print("=" * 60) 111 | print(f"回溯到 invocation_id: {rewind_invocation_id} 之前...") 112 | await runner.rewind_async( 113 | user_id=USER_ID, 114 | session_id=session.id, 115 | rewind_before_invocation_id=rewind_invocation_id, 116 | ) 117 | print("✅ 回溯完成!") 118 | 119 | # ===== 步骤 6: 验证回溯后的状态 ===== 120 | print("\n" + "=" * 60) 121 | print("步骤 6: 验证回溯后的状态") 122 | print("=" * 60) 123 | await call_agent(runner, USER_ID, session.id, "查询状态 color 的值") 124 | await call_agent(runner, USER_ID, session.id, "读取文件 note.txt") 125 | 126 | # ===== 总结 ===== 127 | print("\n" + "=" * 60) 128 | print("✨ 演示完成!") 129 | print("=" * 60) 130 | print(""" 131 | 🔑 核心要点: 132 | 1. 使用 runner.rewind_async() 可以回溯会话状态 133 | 2. 需要指定 rewind_before_invocation_id 来确定回溯点 134 | 3. 回溯会恢复状态(state)和文件(artifacts) 135 | 4. 这个功能可以用来实现"撤销"、"编辑消息"、"重新生成"等功能 136 | 137 | 📚 文档链接: 138 | - ADK Session Rewind: https://google.github.io/adk-docs/sessions/rewind/ 139 | - ADK Runtime Resume: https://google.github.io/adk-docs/runtime/resume/ 140 | """) 141 | 142 | 143 | if __name__ == "__main__": 144 | asyncio.run(main()) 145 | -------------------------------------------------------------------------------- /day-15/README.md: -------------------------------------------------------------------------------- 1 | # Day 15: A2UI - Agent 生成动态界面 2 | 3 | ## 从一个糟糕的体验说起 4 | 5 | 你开发了一个餐厅推荐 Agent。用户问:"帮我找一家附近的西餐厅" 6 | 7 | Agent 回复: 8 | 9 | ``` 10 | 我找到了以下餐厅: 11 | 1. 意大利花园 - 人均 ¥180 - 评分 4.8 - 距离 500m 12 | 2. 牛排工坊 - 人均 ¥280 - 评分 4.5 - 距离 800m 13 | ... 14 | 请告诉我您想选择哪家... 15 | ``` 16 | 17 | 用户需要从文字中"脑补"出列表,手动输入选择。 18 | 19 | ## 如果 Agent 能生成 UI 呢? 20 | 21 | ``` 22 | ┌─────────────────────────────────────────────────┐ 23 | │ 🍽️ 附近的西餐厅 │ 24 | ├─────────────────────────────────────────────────┤ 25 | │ 🍕 意大利花园 ¥180 ★4.8 500m [查看] [预订] │ 26 | │ 🥩 牛排工坊 ¥280 ★4.5 800m [查看] [预订] │ 27 | └─────────────────────────────────────────────────┘ 28 | ``` 29 | 30 | 用户直接点击、筛选、预订。这就是 **A2UI** 要解决的问题。 31 | 32 | ## A2UI 是什么? 33 | 34 | A2UI (Agent-to-User Interface) 是 Google 推出的声明式 UI 协议: 35 | 36 | - **Agent 不生成代码**,只描述"想要什么界面" 37 | - **客户端渲染器** 用预定义的安全组件渲染 38 | - **跨平台**:同一份 JSON 可渲染到 Web、iOS、Android 39 | 40 | ## 快速开始 41 | 42 | ### 1. 进入目录 43 | 44 | ```bash 45 | cd day-15 46 | ``` 47 | 48 | ### 2. 启动服务器 49 | 50 | ```bash 51 | uv run python server.py 52 | ``` 53 | 54 | 看到以下输出表示启动成功: 55 | 56 | ``` 57 | ============================================================ 58 | Day 15: A2UI Demo Server 59 | ============================================================ 60 | 61 | 🌐 访问: http://localhost:8002 62 | 63 | 功能: 64 | - 输入自然语言描述,生成动态 UI 65 | - 实时渲染 A2UI 组件 66 | - 查看生成的 JSON 结构 67 | 68 | ============================================================ 69 | INFO: Uvicorn running on http://localhost:8002 70 | ``` 71 | 72 | ### 3. 打开浏览器 73 | 74 | 访问 http://localhost:8002 75 | 76 | ### 4. 试试这些功能 77 | 78 | - 点击快捷按钮:**登录表单**、**餐厅推荐**、**天气卡片** 79 | - 或输入自定义描述:"创建一个用户注册表单" 80 | - 查看右侧生成的 A2UI JSON 结构 81 | - 点击按钮,观察右下角事件日志 82 | 83 | ## 项目结构 84 | 85 | ``` 86 | day-15/ 87 | ├── README.md # 本文件 88 | ├── a2ui.py # A2UI Python SDK 89 | ├── server.py # Web 服务器 + JS 渲染器 90 | └── day-15-a2ui.ipynb # Jupyter 教程(可选) 91 | ``` 92 | 93 | ## 代码示例 94 | 95 | ### 使用 Python SDK 96 | 97 | ```python 98 | from a2ui import A2UI 99 | 100 | # 链式 API 构建界面 101 | ui = (A2UI("login") 102 | .text("title", "用户登录", "h1") 103 | .text_field("username", "/user/name", "用户名") 104 | .text_field("password", "/user/password", "密码") 105 | .button("submit", "登录", "submit_login") 106 | .column("form", ["title", "username", "password", "submit"]) 107 | .card("card", "form")) 108 | 109 | # 生成 A2UI JSONL 110 | print(ui.build("card")) 111 | ``` 112 | 113 | 输出: 114 | 115 | ```jsonl 116 | {"surfaceUpdate": {"surfaceId": "login", "components": [...]}} 117 | {"beginRendering": {"surfaceId": "login", "root": "card"}} 118 | ``` 119 | 120 | ### 使用工厂函数 121 | 122 | ```python 123 | from a2ui import create_restaurant_list, create_weather_card 124 | 125 | # 餐厅列表 126 | restaurants = [ 127 | {"name": "意大利花园", "price": 180, "rating": 4.8, "distance": "500m"}, 128 | {"name": "牛排工坊", "price": 280, "rating": 4.5, "distance": "800m"}, 129 | ] 130 | ui = create_restaurant_list(restaurants) 131 | print(ui.build("main")) 132 | 133 | # 天气卡片 134 | ui = create_weather_card("北京", 25, "晴天", 45) 135 | print(ui.build("card")) 136 | ``` 137 | 138 | ### 直接运行测试 139 | 140 | ```bash 141 | uv run python a2ui.py 142 | ``` 143 | 144 | ## A2UI 核心概念 145 | 146 | ### 三种消息类型 147 | 148 | | 消息 | 作用 | 149 | |------|------| 150 | | `surfaceUpdate` | 定义 UI 组件及属性 | 151 | | `dataModelUpdate` | 设置数据模型(可选) | 152 | | `beginRendering` | 开始渲染,指定根组件 | 153 | 154 | ### 支持的组件 155 | 156 | | 组件 | 用途 | 示例 | 157 | |------|------|------| 158 | | Text | 文本显示 | 标题、段落 | 159 | | Button | 按钮 | 提交、取消 | 160 | | TextField | 输入框 | 用户名、密码 | 161 | | Column | 垂直布局 | 表单排列 | 162 | | Row | 水平布局 | 按钮组 | 163 | | Card | 卡片容器 | 内容分组 | 164 | | Image | 图片 | 头像、封面 | 165 | | Divider | 分隔线 | 内容分隔 | 166 | | Checkbox | 复选框 | 勾选选项 | 167 | 168 | ### 架构图 169 | 170 | ``` 171 | 用户输入 "登录表单" 172 | │ 173 | ▼ 174 | ┌──────────────┐ 175 | │ Gemini │ 理解意图,生成 A2UI JSON 176 | └──────┬───────┘ 177 | │ 178 | ▼ 179 | ┌──────────────┐ 180 | │ Python SDK │ a2ui.py 构建组件 181 | └──────┬───────┘ 182 | │ 183 | ▼ 184 | ┌──────────────┐ 185 | │ FastAPI │ server.py 提供 API 186 | └──────┬───────┘ 187 | │ HTTP 188 | ▼ 189 | ┌──────────────┐ 190 | │ JS Renderer │ 浏览器渲染真实 UI 191 | └──────────────┘ 192 | ``` 193 | 194 | ## 与 Gemini 集成 195 | 196 | 服务器会自动检测 `GOOGLE_API_KEY` 环境变量: 197 | 198 | - **有 API Key**:使用 Gemini 动态生成 UI 199 | - **无 API Key**:使用预定义模板(仍可正常演示) 200 | 201 | 设置 API Key: 202 | 203 | ```bash 204 | # 在项目根目录 .env 文件中 205 | GOOGLE_API_KEY=your_api_key_here 206 | ``` 207 | 208 | ## 与 Day 14 A2A 的关系 209 | 210 | | Day 14 A2A | Day 15 A2UI | 211 | |------------|-------------| 212 | | Agent 之间如何通信 | Agent 如何向用户展示界面 | 213 | | 协议层 | 表现层 | 214 | 215 | A2UI 消息可以通过 A2A 协议传输,实现多 Agent 协作生成 UI。 216 | 217 | ## 常见问题 218 | 219 | ### Q: 端口 8002 被占用? 220 | 221 | 修改 `server.py` 最后一行的端口号: 222 | 223 | ```python 224 | uvicorn.run(app, host="localhost", port=8003) # 改成其他端口 225 | ``` 226 | 227 | ### Q: 提示 ModuleNotFoundError? 228 | 229 | 确保在项目根目录安装了依赖: 230 | 231 | ```bash 232 | cd .. # 回到项目根目录 233 | uv sync 234 | ``` 235 | 236 | ### Q: Gemini 生成的 UI 不符合预期? 237 | 238 | 这是正常的,LLM 生成有随机性。可以: 239 | 1. 重新生成 240 | 2. 调整 prompt 描述更具体 241 | 3. 直接使用快捷按钮(使用预定义模板) 242 | 243 | ## 扩展阅读 244 | 245 | - [A2UI 官网](https://a2ui.org/) 246 | - [A2UI GitHub](https://github.com/google/A2UI) 247 | - [A2UI Composer](https://a2ui-editor.ag-ui.com/) - 在线可视化编辑器 248 | - [CopilotKit A2UI 集成](https://www.copilotkit.ai/blog/how-to-build-agent-to-user-interface-a2ui-agents-using-a2a-ag-ui) 249 | -------------------------------------------------------------------------------- /day-03/README.md: -------------------------------------------------------------------------------- 1 | # Day 03: Gemini 3 + ADK 2 | 3 | Build a powerful AI Agent using Gemini 3 and ADK with native support for Google Search grounding, computer use, and real-time streaming. 4 | 5 | ## Learning Goals 6 | 7 | - Use Google Search tool for real-time information grounding 8 | - Understand Gemini 3's native tool capabilities 9 | - Build agents that can access current information 10 | - Learn about search result display requirements 11 | 12 | ## Prerequisites 13 | 14 | ```bash 15 | pip install google-adk 16 | ``` 17 | 18 | ## Quick Start 19 | 20 | ### 1. Create Agent with Google Search 21 | 22 | ```yaml 23 | # root_agent.yaml 24 | name: search_agent 25 | model: gemini-2.5-flash 26 | description: An assistant with Google Search capability. 27 | instruction: | 28 | You are a helpful assistant that can search the web. 29 | Use Google Search for current events and factual information. 30 | tools: 31 | - name: google_search 32 | ``` 33 | 34 | ### 2. Run Your Agent 35 | 36 | ```bash 37 | cd day-03 38 | adk web 39 | ``` 40 | 41 | ## Google Search Tool 42 | 43 | ### Overview 44 | 45 | The `google_search` tool enables agents to perform web searches using Google Search. This provides **grounding** - the ability to access real-time information beyond the model's training data. 46 | 47 | ### Key Features 48 | 49 | | Feature | Description | 50 | |---------|-------------| 51 | | **Real-time data** | Access current news, prices, events | 52 | | **Grounding** | Reduce hallucinations with factual sources | 53 | | **Citations** | Responses include source URLs | 54 | | **Gemini 2+ only** | Requires Gemini 2 or later models | 55 | 56 | ### Python Configuration 57 | 58 | ```python 59 | from google.adk.agents import Agent 60 | from google.adk.tools import google_search 61 | 62 | agent = Agent( 63 | name="search_assistant", 64 | model="gemini-2.5-flash", 65 | instruction="You are a helpful assistant. Use Google Search when needed.", 66 | tools=[google_search] 67 | ) 68 | ``` 69 | 70 | ### YAML Configuration 71 | 72 | ```yaml 73 | name: search_assistant 74 | model: gemini-2.5-flash 75 | description: An assistant that can search the web. 76 | instruction: | 77 | You are a helpful assistant. 78 | Use Google Search for current events and factual information. 79 | tools: 80 | - name: google_search 81 | ``` 82 | 83 | ## Important Limitations 84 | 85 | ### Tool Mixing Rules 86 | 87 | 1. **Built-in tools** (like `google_search`) only work with Gemini models 88 | 2. **Cannot mix** built-in tools with custom Python tools in the same agent 89 | 3. **One built-in tool** per agent (use sub-agents for multiple) 90 | 91 | ### Display Requirements 92 | 93 | When using Google Search grounding in production: 94 | - You must display search suggestions returned in the response 95 | - The UI code (HTML) is in `renderedContent` field 96 | - Follow Google's display policies 97 | 98 | ## Multi-Agent Pattern 99 | 100 | For agents that need both Google Search and custom tools: 101 | 102 | ```yaml 103 | # root_agent.yaml - Coordinator 104 | name: coordinator 105 | model: gemini-2.5-flash 106 | description: Coordinates between search and tools. 107 | instruction: | 108 | Use the search_agent for current information. 109 | Use the calculator_agent for calculations. 110 | 111 | sub_agents: 112 | - name: search_agent 113 | model: gemini-2.5-flash 114 | description: Searches the web. 115 | instruction: Use Google Search to find information. 116 | tools: 117 | - name: google_search 118 | 119 | - name: calculator_agent 120 | model: gemini-2.5-flash 121 | description: Performs calculations. 122 | instruction: Help with math calculations. 123 | tools: 124 | - tools.calculate 125 | ``` 126 | 127 | ## Example: News Agent 128 | 129 | ```yaml 130 | name: news_agent 131 | model: gemini-2.5-flash 132 | description: A news assistant that provides current information. 133 | instruction: | 134 | You are a news assistant. 135 | 136 | When users ask about: 137 | - Current events: Use Google Search 138 | - Weather: Use Google Search 139 | - Stock prices: Use Google Search 140 | - Sports scores: Use Google Search 141 | 142 | Always cite your sources and provide links when available. 143 | Present information in a clear, organized format. 144 | tools: 145 | - name: google_search 146 | ``` 147 | 148 | ## Resources 149 | 150 | ### Official Documentation 151 | - [ADK Built-in Tools](https://google.github.io/adk-docs/tools/built-in-tools/) 152 | - [Grounding with Google Search](https://ai.google.dev/gemini-api/docs/grounding) 153 | - [ADK Python Repository](https://github.com/google/adk-python) 154 | 155 | ### Tutorials 156 | - [Build an AI Agent with Gemini 3 (Video)](https://www.youtube.com/watch?v=9EGtawwvINs&list=PLOU2XLYxmsIJCVXV1bLV7qnT5hilN3YJ7&index=4&t=1s) 157 | - [Gemini 3 Agent Demo (GitHub)](https://github.com/GoogleCloudPlatform/devrel-demos/tree/main/ai-ml/agent-labs/gemini-3-pro-agent-demo) 158 | - [Google Codelabs: Empowering with Tools](https://codelabs.developers.google.com/devsite/codelabs/build-agents-with-adk-empowering-with-tools) 159 | 160 | ### Announcements 161 | - [Gemini 3 Announcement](https://blog.google/products/gemini-3/#gemini-3) 162 | - [Agent Development Kit Blog](https://developers.googleblog.com/en/agent-development-kit-easy-to-build-multi-agent-applications/) 163 | 164 | ## Notes 165 | 166 | Day 3 introduces the Google Search tool, enabling your agents to access real-time information. This is essential for building agents that can answer questions about current events, prices, weather, and other time-sensitive data. 167 | -------------------------------------------------------------------------------- /day-18/api_registry_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Day 18: Cloud API Registry + ADK 演示 3 | 4 | 前置要求: 5 | 1. 配置 Google Cloud 项目 6 | 2. 启用 API Registry 和相关服务 7 | 3. 运行: gcloud auth application-default login 8 | 9 | 运行: 10 | python api_registry_demo.py 11 | """ 12 | 13 | import os 14 | 15 | # ============================================================================= 16 | # 配置 17 | # ============================================================================= 18 | 19 | # 从环境变量获取项目 ID,或手动设置 20 | PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "your-project-id") 21 | 22 | 23 | # ============================================================================= 24 | # 示例 1: 基础用法 - 获取单个工具 25 | # ============================================================================= 26 | 27 | def demo_basic_usage(): 28 | """ 29 | 最简单的用法:从 Registry 获取一个工具 30 | """ 31 | from google.adk import Agent 32 | from google.adk.tools.google_cloud import ApiRegistry 33 | 34 | # 1. 连接到 Cloud API Registry 35 | registry = ApiRegistry(project_id=PROJECT_ID) 36 | 37 | # 2. 获取 BigQuery 工具(管理员需要先启用) 38 | bq_tool = registry.get_tool("google-bigquery") 39 | 40 | # 3. 创建带有该工具的 Agent 41 | agent = Agent( 42 | model="gemini-3-flash-preview", 43 | name="data_analyst", 44 | instruction="你是一个数据分析师,可以查询 BigQuery 数据库。", 45 | tools=[bq_tool] 46 | ) 47 | 48 | print("✅ Agent 已配置 BigQuery 工具") 49 | return agent 50 | 51 | 52 | # ============================================================================= 53 | # 示例 2: 获取多个工具 54 | # ============================================================================= 55 | 56 | def demo_multiple_tools(): 57 | """ 58 | 获取多个已审批的工具 59 | """ 60 | from google.adk import Agent 61 | from google.adk.tools.google_cloud import ApiRegistry 62 | 63 | registry = ApiRegistry(project_id=PROJECT_ID) 64 | 65 | # 获取多个工具 66 | tools = [ 67 | registry.get_tool("google-bigquery"), # 数据查询 68 | registry.get_tool("google-cloud-storage"), # 文件存储 69 | ] 70 | 71 | agent = Agent( 72 | model="gemini-3-flash-preview", 73 | name="cloud_assistant", 74 | instruction="你是云服务助手,可以查询数据和管理文件。", 75 | tools=tools 76 | ) 77 | 78 | print("✅ Agent 已配置多个工具") 79 | return agent 80 | 81 | 82 | # ============================================================================= 83 | # 示例 3: 列出所有可用工具 84 | # ============================================================================= 85 | 86 | def demo_list_tools(): 87 | """ 88 | 查看 Registry 中有哪些可用工具 89 | """ 90 | from google.adk.tools.google_cloud import ApiRegistry 91 | 92 | registry = ApiRegistry(project_id=PROJECT_ID) 93 | 94 | # 列出所有可用工具 95 | available_tools = registry.list_tools() 96 | 97 | print("📦 可用工具列表:") 98 | for tool in available_tools: 99 | print(f" - {tool.name}: {tool.description}") 100 | 101 | return available_tools 102 | 103 | 104 | # ============================================================================= 105 | # 示例 4: 完整工作流程 106 | # ============================================================================= 107 | 108 | async def demo_full_workflow(): 109 | """ 110 | 完整演示:创建 Agent 并执行查询 111 | """ 112 | from google.adk import Agent 113 | from google.adk.runners import Runner 114 | from google.adk.sessions import InMemorySessionService 115 | from google.adk.tools.google_cloud import ApiRegistry 116 | from google.genai.types import Content, Part 117 | 118 | # 1. 设置 Registry 和工具 119 | registry = ApiRegistry(project_id=PROJECT_ID) 120 | bq_tool = registry.get_tool("google-bigquery") 121 | 122 | # 2. 创建 Agent 123 | agent = Agent( 124 | model="gemini-3-flash-preview", 125 | name="data_analyst", 126 | instruction="""你是数据分析师。 127 | 用户询问数据问题时,使用 BigQuery 工具查询。 128 | 用中文简洁回答。""", 129 | tools=[bq_tool] 130 | ) 131 | 132 | # 3. 运行对话 133 | session_service = InMemorySessionService() 134 | session = await session_service.create_session( 135 | app_name="api_registry_demo", 136 | user_id="demo_user" 137 | ) 138 | 139 | runner = Runner( 140 | agent=agent, 141 | app_name="api_registry_demo", 142 | session_service=session_service 143 | ) 144 | 145 | # 4. 发送查询 146 | question = "查询公开数据集中 2023 年的数据统计" 147 | user_content = Content(role="user", parts=[Part(text=question)]) 148 | 149 | print(f"📝 问题: {question}") 150 | print("-" * 40) 151 | 152 | async for event in runner.run_async( 153 | user_id="demo_user", 154 | session_id=session.id, 155 | new_message=user_content 156 | ): 157 | if hasattr(event, 'content') and event.content: 158 | if hasattr(event.content, 'parts'): 159 | for part in event.content.parts: 160 | if hasattr(part, 'text') and part.text: 161 | print(f"🤖 回答: {part.text}") 162 | 163 | 164 | # ============================================================================= 165 | # 主程序 166 | # ============================================================================= 167 | 168 | if __name__ == "__main__": 169 | print("=" * 50) 170 | print("Day 18: Cloud API Registry + ADK 演示") 171 | print("=" * 50) 172 | print(f"项目 ID: {PROJECT_ID}") 173 | print() 174 | 175 | # 注意:以下代码需要正确配置 Google Cloud 才能运行 176 | # 这里只展示代码结构,实际运行需要: 177 | # 1. 有效的 Google Cloud 项目 178 | # 2. 启用相关 API 179 | # 3. 配置好认证 180 | 181 | print("📌 示例代码已准备好") 182 | print("📌 请确保配置好 Google Cloud 项目后运行") 183 | print() 184 | print("启用 BigQuery MCP 服务:") 185 | print(f" gcloud beta services mcp enable bigquery.googleapis.com --project={PROJECT_ID}") 186 | print() 187 | print("运行完整演示:") 188 | print(" 取消下面的注释并运行") 189 | 190 | # 取消注释运行演示: 191 | # demo_basic_usage() 192 | # demo_multiple_tools() 193 | # demo_list_tools() 194 | # import asyncio 195 | # asyncio.run(demo_full_workflow()) 196 | -------------------------------------------------------------------------------- /day-06/day06_ide_context.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Day 6: ADK IDE Integration\n", 8 | "\n", 9 | "> Building agents shouldn't require an hour of environment configuration.\n", 10 | "\n", 11 | "今天学习如何为 AI IDE 提供项目上下文。" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "## 1. What is llms.txt?\n", 19 | "\n", 20 | "`llms.txt` 是一种为 LLM 提供项目文档的标准格式,类似于 `robots.txt`。\n", 21 | "\n", 22 | "**支持的 IDE:**\n", 23 | "- Antigravity (Google)\n", 24 | "- Gemini CLI\n", 25 | "- Cursor\n", 26 | "- Firebase Studio\n", 27 | "- Claude Code\n", 28 | "- Windsurf" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "## 2. IDE 配置文件对照表\n", 36 | "\n", 37 | "| IDE | 配置文件 |\n", 38 | "|-----|----------|\n", 39 | "| Cursor | `.cursorrules` |\n", 40 | "| Claude Code | `.claude/instructions.md` |\n", 41 | "| Windsurf | `.windsurfrules` |\n", 42 | "| Generic | `llms.txt` |" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "from pathlib import Path\n", 52 | "from datetime import datetime" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "## 3. 生成 llms.txt" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "LLMS_TXT = \"\"\"\\\n", 69 | "# ADK Agent Project\n", 70 | "\n", 71 | "> AI Agent built with Google ADK\n", 72 | "\n", 73 | "## Tech Stack\n", 74 | "- Framework: Google ADK\n", 75 | "- Model: Gemini 2.0 Flash\n", 76 | "- Language: Python 3.11+\n", 77 | "\n", 78 | "## Key Patterns\n", 79 | "\n", 80 | "```python\n", 81 | "from google.adk import Agent, tool\n", 82 | "\n", 83 | "@tool\n", 84 | "def search(query: str) -> str:\n", 85 | " \\\"\\\"\\\"Search for information.\\\"\\\"\\\" \n", 86 | " return results\n", 87 | "\n", 88 | "agent = Agent(\n", 89 | " name=\"my-agent\",\n", 90 | " model=\"gemini-2.0-flash\",\n", 91 | " tools=[search]\n", 92 | ")\n", 93 | "```\n", 94 | "\n", 95 | "## Docs\n", 96 | "- https://google.github.io/adk-docs/\n", 97 | "\"\"\"\n", 98 | "\n", 99 | "print(LLMS_TXT)" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "metadata": {}, 105 | "source": [ 106 | "## 4. 生成 .cursorrules" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "CURSOR_RULES = \"\"\"\\\n", 116 | "# ADK Development Rules\n", 117 | "\n", 118 | "You are an expert in Google ADK.\n", 119 | "\n", 120 | "## Tech Stack\n", 121 | "- Google ADK + Gemini 2.0 Flash\n", 122 | "- Python 3.11+ with uv\n", 123 | "\n", 124 | "## Code Style\n", 125 | "- Use type hints\n", 126 | "- Write clear tool docstrings (LLM uses them)\n", 127 | "- Handle errors gracefully\n", 128 | "\n", 129 | "## Tool Pattern\n", 130 | "```python\n", 131 | "@tool\n", 132 | "def func(param: str) -> str:\n", 133 | " \\\"\\\"\\\"Clear description for the LLM.\\\"\\\"\\\"\n", 134 | " return result\n", 135 | "```\n", 136 | "\"\"\"\n", 137 | "\n", 138 | "print(CURSOR_RULES)" 139 | ] 140 | }, 141 | { 142 | "cell_type": "markdown", 143 | "metadata": {}, 144 | "source": [ 145 | "## 5. 一键生成所有配置" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": null, 151 | "metadata": {}, 152 | "outputs": [], 153 | "source": [ 154 | "def generate_ide_configs(project_root: Path):\n", 155 | " \"\"\"Generate IDE config files for ADK project.\"\"\"\n", 156 | " \n", 157 | " configs = {\n", 158 | " \"llms.txt\": LLMS_TXT,\n", 159 | " \".cursorrules\": CURSOR_RULES,\n", 160 | " \".windsurfrules\": CURSOR_RULES, # Similar format\n", 161 | " }\n", 162 | " \n", 163 | " for filename, content in configs.items():\n", 164 | " path = project_root / filename\n", 165 | " path.write_text(content)\n", 166 | " print(f\"[OK] {filename}\")\n", 167 | " \n", 168 | " # Claude Code\n", 169 | " claude_dir = project_root / \".claude\"\n", 170 | " claude_dir.mkdir(exist_ok=True)\n", 171 | " (claude_dir / \"instructions.md\").write_text(CURSOR_RULES)\n", 172 | " print(f\"[OK] .claude/instructions.md\")" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": null, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "# 生成配置文件到项目根目录\n", 182 | "project_root = Path.cwd().parent # 从 day-06 回到项目根目录\n", 183 | "print(f\"Project: {project_root}\\n\")\n", 184 | "\n", 185 | "generate_ide_configs(project_root)" 186 | ] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "metadata": {}, 191 | "source": [ 192 | "## 6. Key Takeaways\n", 193 | "\n", 194 | "1. **llms.txt** - 为 AI 提供项目上下文的标准格式\n", 195 | "2. **不同 IDE 不同配置文件**,但目标相同\n", 196 | "3. **好的上下文 = 更好的 AI 辅助**\n", 197 | "4. **Agent Starter Pack** 已包含预配置\n", 198 | "\n", 199 | "## Resources\n", 200 | "\n", 201 | "- [llms.txt 规范](https://llmstxt.org/)\n", 202 | "- [ADK 文档](https://google.github.io/adk-docs/)\n", 203 | "- [Antigravity IDE](https://antigravity.google/)\n", 204 | "- [视频教程](https://www.youtube.com/watch?v=Ep8usBDUTtA)" 205 | ] 206 | } 207 | ], 208 | "metadata": { 209 | "kernelspec": { 210 | "display_name": "Python 3", 211 | "language": "python", 212 | "name": "python3" 213 | }, 214 | "language_info": { 215 | "name": "python", 216 | "version": "3.11.0" 217 | } 218 | }, 219 | "nbformat": 4, 220 | "nbformat_minor": 4 221 | } 222 | -------------------------------------------------------------------------------- /day-04/README.md: -------------------------------------------------------------------------------- 1 | # Day 04: 基于源代码的部署 2 | 3 | 直接从源代码部署你的 Agent 到 Agent Engine —— 告别序列化的烦恼。 4 | 5 | ## 学习目标 6 | 7 | - 将 ADK Agent 部署到 Vertex AI Agent Engine 8 | - 使用 Agent Starter Pack 进行生产级部署 9 | - 理解 source-based 与 cloudpickle 部署的区别 10 | - 设置 CI/CD 流水线 11 | 12 | ## 前置条件 13 | 14 | ```bash 15 | # 安装 UV 工具(如果还没装的话) 16 | curl -LsSf https://astral.sh/uv/install.sh | sh 17 | 18 | # 安装 Google Cloud CLI 19 | # 参考: https://cloud.google.com/sdk/docs/install 20 | ``` 21 | 22 | ## 序列化问题 23 | 24 | 传统的 Agent 部署使用 **cloudpickle** 序列化 Python 对象,经常会遇到问题: 25 | 26 | | 问题 | 描述 | 27 | |------|------| 28 | | **版本不一致** | 开发和生产环境的 cloudpickle 版本不同 | 29 | | **属性缺失** | `AttributeError: Can't get attribute '_class_setstate'` | 30 | | **依赖冲突** | 复杂的依赖关系导致序列化失败 | 31 | | **调试困难** | 序列化错误很难追踪定位 | 32 | 33 | **基于源代码的部署** 直接部署你的源代码,而不是序列化后的对象,从根本上解决这些问题。 34 | 35 | ## 快速开始 36 | 37 | ### 方式一:新建项目 38 | 39 | 创建一个带 Agent Engine 部署配置的新项目: 40 | 41 | ```bash 42 | uvx agent-starter-pack create my-agent -a adk_base -d agent_engine 43 | cd my-agent 44 | ``` 45 | 46 | ### 方式二:现有项目 47 | 48 | 为现有的 ADK Agent 添加部署能力: 49 | 50 | ```bash 51 | # 进入包含你 agent 的父目录 52 | cd /path/to/parent 53 | 54 | # 添加部署配置 55 | uvx agent-starter-pack enhance --adk -d agent_engine 56 | ``` 57 | 58 | ## 项目结构 59 | 60 | 增强后的项目结构: 61 | 62 | ``` 63 | my-agent/ 64 | ├── agent/ 65 | │ ├── __init__.py 66 | │ ├── agent.py # Agent 代码 67 | │ └── root_agent.yaml # Agent 配置 68 | ├── deployment/ 69 | │ ├── terraform/ # 基础设施即代码 70 | │ └── cloudbuild.yaml # CI/CD 流水线 71 | ├── Makefile # 部署命令 72 | ├── pyproject.toml # 依赖配置 73 | └── README.md 74 | ``` 75 | 76 | ## 部署到 Agent Engine 77 | 78 | ### 第一步:配置 Google Cloud 79 | 80 | ```bash 81 | # 设置项目 82 | export GOOGLE_CLOUD_PROJECT="your-project-id" 83 | export GOOGLE_CLOUD_LOCATION="us-central1" 84 | export STAGING_BUCKET="gs://your-staging-bucket" 85 | 86 | # 认证 87 | gcloud auth login 88 | gcloud config set project $GOOGLE_CLOUD_PROJECT 89 | ``` 90 | 91 | ### 第二步:使用 Make 部署 92 | 93 | ```bash 94 | # 部署到 Agent Engine 95 | make deploy 96 | ``` 97 | 98 | ### 第三步:使用 ADK CLI 部署 99 | 100 | 也可以直接用 ADK 命令: 101 | 102 | ```bash 103 | adk deploy agent_engine \ 104 | --project $GOOGLE_CLOUD_PROJECT \ 105 | --region $GOOGLE_CLOUD_LOCATION \ 106 | --staging_bucket $STAGING_BUCKET \ 107 | --trace_to_cloud \ 108 | --display_name "my-agent" \ 109 | --description "我的部署 Agent" \ 110 | agent 111 | ``` 112 | 113 | ## Agent Starter Pack 功能 114 | 115 | Agent Starter Pack 提供: 116 | 117 | | 功能 | 描述 | 118 | |------|------| 119 | | **CI/CD 流水线** | Cloud Build 或 GitHub Actions | 120 | | **Terraform** | 基础设施即代码 | 121 | | **OpenTelemetry** | 内置追踪和监控 | 122 | | **会话管理** | 有状态对话 | 123 | | **评估框架** | Agent 测试工具 | 124 | 125 | ## 支持的区域 126 | 127 | Agent Engine 在以下区域可用: 128 | 129 | - `us-central1` (爱荷华) 130 | - `us-east1` (南卡罗来纳) 131 | - `europe-west1` (比利时) 132 | - `asia-northeast1` (东京) 133 | 134 | ## 示例:部署搜索 Agent 135 | 136 | ### 1. 创建 Agent 137 | 138 | ```yaml 139 | # agent/root_agent.yaml 140 | name: search_assistant 141 | model: gemini-2.5-flash 142 | description: 部署到 Agent Engine 的搜索助手 143 | instruction: | 144 | 你是一个有帮助的助手。 145 | 使用 Google Search 获取时事和事实信息。 146 | 总是引用你的信息来源。 147 | tools: 148 | - name: google_search 149 | ``` 150 | 151 | ### 2. 部署 152 | 153 | ```bash 154 | adk deploy agent_engine \ 155 | --project $GOOGLE_CLOUD_PROJECT \ 156 | --region us-central1 \ 157 | --staging_bucket $STAGING_BUCKET \ 158 | --trace_to_cloud \ 159 | agent 160 | ``` 161 | 162 | ### 3. 测试 163 | 164 | 部署后,你可以通过以下方式与 Agent 交互: 165 | - Google Cloud Console Agent Engine 界面 166 | - REST API 167 | - Python SDK 168 | 169 | ## Python SDK 使用 170 | 171 | 部署后,可以用代码调用你的 Agent: 172 | 173 | ```python 174 | from google.cloud import aiplatform 175 | 176 | # 初始化 177 | aiplatform.init( 178 | project="your-project-id", 179 | location="us-central1" 180 | ) 181 | 182 | # 获取已部署的 Agent 183 | agent = aiplatform.Agent("projects/your-project/locations/us-central1/agents/your-agent-id") 184 | 185 | # 创建会话 186 | session = agent.create_session() 187 | 188 | # 与 Agent 对话 189 | response = session.chat("最近有什么 AI 新闻?") 190 | print(response.text) 191 | ``` 192 | 193 | ## 监控你的 Agent 194 | 195 | ### Cloud Console 196 | 197 | 在 [Agent Engine Console](https://console.cloud.google.com/vertex-ai/agent-engine) 查看: 198 | - 监控对话 199 | - 查看会话历史 200 | - 追踪性能指标 201 | 202 | ### Cloud Trace 203 | 204 | 使用 `--trace_to_cloud` 参数可获得自动追踪: 205 | - 请求延迟 206 | - 工具执行时间 207 | - 错误追踪 208 | 209 | ## 常见问题排查 210 | 211 | ### 常见错误 212 | 213 | **1. Cloudpickle 版本不匹配** 214 | ``` 215 | AttributeError: Can't get attribute '_class_setstate' 216 | ``` 217 | 解决方案:确保 `pyproject.toml` 中的 cloudpickle 版本一致 218 | 219 | **2. 区域不支持** 220 | ``` 221 | InvalidArgument: Region not supported for Agent Engine 222 | ``` 223 | 解决方案:使用支持的区域(us-central1, us-east1 等) 224 | 225 | **3. 依赖缺失** 226 | ``` 227 | ModuleNotFoundError: No module named 'xxx' 228 | ``` 229 | 解决方案:把所有依赖加到 `pyproject.toml` 230 | 231 | ### 先本地调试 232 | 233 | 部署前一定要先本地测试: 234 | 235 | ```bash 236 | # 本地运行 237 | adk web 238 | 239 | # 测试你的 Agent 240 | # 没问题了再部署 241 | ``` 242 | 243 | ## Agent Engine vs Cloud Run 244 | 245 | | 特性 | Agent Engine | Cloud Run | 246 | |------|-------------|-----------| 247 | | **会话管理** | 内置 | 需要自己实现 | 248 | | **自动扩缩** | 自动 | 自动 | 249 | | **自定义 UI** | 仅 REST API | 完全灵活 | 250 | | **计费方式** | 按会话 | 按请求 | 251 | | **A2A 协议** | REST API | 直接支持 | 252 | 253 | 选择 **Agent Engine** 如果: 254 | - 想要快速部署 255 | - 需要内置会话管理 256 | - 需要生产监控 257 | 258 | 选择 **Cloud Run** 如果: 259 | - 需要自定义 Web UI 260 | - 需要 A2A 协议支持 261 | - 需要更多基础设施控制 262 | 263 | ## 资源链接 264 | 265 | ### 官方文档 266 | - [ADK 部署指南](https://google.github.io/adk-docs/deploy/) 267 | - [Agent Engine 文档](https://google.github.io/adk-docs/deploy/agent-engine/) 268 | - [Agent Starter Pack](https://github.com/GoogleCloudPlatform/agent-starter-pack) 269 | 270 | ### 教程 271 | - [Source-Based Deployment 视频教程](https://youtu.be/8RjzMG3BKA0) 272 | - [ADK + Agent Engine 多 Agent 应用 (Codelab)](https://codelabs.developers.google.com/multi-agent-app-with-adk) 273 | - [部署多 Agent 系统 (Google Skills)](https://www.skills.google/course_templates/1275) 274 | 275 | ### 快速入门 276 | - [快速入门:ADK + Agent Engine](https://cloud.google.com/agent-builder/agent-engine/quickstart-adk) 277 | - [部署 Agent](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/deploy) 278 | 279 | ## 小结 280 | 281 | Day 4 的核心是 **基于源代码的部署** —— 一种更干净的 Agent 部署方式,避免序列化带来的各种问题。Agent Starter Pack 让这个过程变得简单,提供了生产级的模板和 CI/CD 流水线。 282 | 283 | 关键命令:`uvx agent-starter-pack enhance --adk -d agent_engine` 可以为任何现有的 ADK 项目添加部署能力。 284 | -------------------------------------------------------------------------------- /day-16/client/test_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Day 16: A2A Client - 测试 LangGraph Agent 3 | 4 | 这是 A2A 客户端,用于测试 LangGraph Agent 服务。 5 | 6 | 运行前确保: 7 | 1. LangGraph Agent 已在 http://localhost:8016 运行 8 | 2. 运行: python -m client.test_client 9 | """ 10 | 11 | import asyncio 12 | import os 13 | import sys 14 | 15 | # 添加项目根目录到路径 16 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 17 | 18 | import httpx 19 | from a2a.client import ClientFactory, ClientConfig 20 | from a2a.types import Role 21 | from a2a.client.helpers import create_text_message_object 22 | 23 | # LangGraph Agent 的 URL 24 | LANGGRAPH_AGENT_URL = "http://localhost:8016" 25 | 26 | 27 | def get_client_config() -> ClientConfig: 28 | """创建不使用代理的客户端配置""" 29 | httpx_client = httpx.AsyncClient( 30 | trust_env=False, 31 | timeout=120.0, 32 | ) 33 | return ClientConfig(httpx_client=httpx_client) 34 | 35 | 36 | # ============================================================ 37 | # 测试函数 38 | # ============================================================ 39 | 40 | async def test_langgraph_agent(): 41 | """测试 LangGraph Agent 的 A2A 服务""" 42 | print("\n" + "=" * 60) 43 | print("测试 LangGraph + A2A Agent") 44 | print("=" * 60) 45 | 46 | try: 47 | # 1. 连接到 LangGraph Agent 48 | print("\n1️⃣ 连接 LangGraph Agent...") 49 | client = await ClientFactory.connect( 50 | LANGGRAPH_AGENT_URL, 51 | client_config=get_client_config() 52 | ) 53 | print(f" ✅ 连接成功!") 54 | print(f" Agent 名称: {client._card.name}") 55 | print(f" Agent 描述: {client._card.description[:60]}...") 56 | 57 | # 显示技能 58 | if client._card.skills: 59 | print(" 技能列表:") 60 | for skill in client._card.skills: 61 | print(f" - {skill.name}: {skill.description}") 62 | 63 | # 2. 测试问答功能 64 | test_questions = [ 65 | "什么是 LangGraph?它和 LangChain 有什么关系?", 66 | "请帮我分析:为什么 AI Agent 需要使用图结构来组织工作流?", 67 | "A2A 协议的主要优势是什么?", 68 | ] 69 | 70 | for i, question in enumerate(test_questions, 1): 71 | print(f"\n{'=' * 60}") 72 | print(f"问题 {i}: {question}") 73 | print("=" * 60) 74 | 75 | # 创建消息 76 | message = create_text_message_object( 77 | role=Role.user, 78 | content=question, 79 | ) 80 | 81 | # 发送请求并等待响应 82 | print("\n⏳ 等待 Agent 响应...") 83 | result_text = None 84 | 85 | async for event in client.send_message(request=message): 86 | if isinstance(event, tuple) and len(event) >= 1: 87 | task = event[0] 88 | 89 | # 从 Task 的 artifacts 中提取响应 90 | # artifacts[].parts[].root.text 是响应的位置 91 | if task and hasattr(task, 'artifacts') and task.artifacts: 92 | for artifact in task.artifacts: 93 | if hasattr(artifact, 'parts'): 94 | for part in artifact.parts: 95 | # Part 对象有 root 属性,包含 TextPart 96 | if hasattr(part, 'root') and part.root: 97 | if hasattr(part.root, 'text') and part.root.text: 98 | result_text = part.root.text 99 | # 兼容直接访问 text 的情况 100 | elif hasattr(part, 'text') and part.text: 101 | result_text = part.text 102 | 103 | if result_text: 104 | print(f"\n🤖 Agent 回答:") 105 | print("-" * 40) 106 | # 格式化输出,每行最多 60 个字符 107 | lines = result_text.split('\n') 108 | for line in lines: 109 | if len(line) > 80: 110 | # 长行分割 111 | words = line.split() 112 | current_line = "" 113 | for word in words: 114 | if len(current_line) + len(word) + 1 <= 80: 115 | current_line += (" " if current_line else "") + word 116 | else: 117 | if current_line: 118 | print(f" {current_line}") 119 | current_line = word 120 | if current_line: 121 | print(f" {current_line}") 122 | else: 123 | print(f" {line}") 124 | print("-" * 40) 125 | else: 126 | print(" ⚠️ 未收到有效响应") 127 | 128 | # 短暂等待,避免请求过快 129 | await asyncio.sleep(1) 130 | 131 | except Exception as e: 132 | print(f"\n❌ 错误: {e}") 133 | import traceback 134 | traceback.print_exc() 135 | print("\n请确保:") 136 | print("1. LangGraph Agent 正在运行 (python -m langgraph_agent.agent)") 137 | print("2. 端口 8016 没有被占用") 138 | print("3. GOOGLE_API_KEY 环境变量已设置") 139 | 140 | 141 | async def check_agent_health(): 142 | """检查 Agent 健康状态""" 143 | print("\n检查 Agent 健康状态...") 144 | try: 145 | async with httpx.AsyncClient(trust_env=False) as client: 146 | # 检查 Agent Card 147 | resp = await client.get(f"{LANGGRAPH_AGENT_URL}/.well-known/agent.json") 148 | if resp.status_code == 200: 149 | print("✅ Agent 服务正常运行") 150 | agent_card = resp.json() 151 | print(f" 名称: {agent_card.get('name', 'Unknown')}") 152 | print(f" 版本: {agent_card.get('version', 'Unknown')}") 153 | return True 154 | else: 155 | print(f"⚠️ Agent 响应异常: {resp.status_code}") 156 | return False 157 | except httpx.ConnectError: 158 | print("❌ 无法连接到 Agent 服务") 159 | print(f" 请确保服务正在 {LANGGRAPH_AGENT_URL} 运行") 160 | return False 161 | except Exception as e: 162 | print(f"❌ 检查失败: {e}") 163 | return False 164 | 165 | 166 | # ============================================================ 167 | # 主程序 168 | # ============================================================ 169 | 170 | async def main(): 171 | print("=" * 60) 172 | print("Day 16: LangGraph + A2A 客户端测试") 173 | print("=" * 60) 174 | print() 175 | print(f"目标 Agent: {LANGGRAPH_AGENT_URL}") 176 | print() 177 | 178 | # 先检查健康状态 179 | is_healthy = await check_agent_health() 180 | 181 | if is_healthy: 182 | # 运行测试 183 | await test_langgraph_agent() 184 | else: 185 | print("\n无法进行测试,请先启动 Agent 服务:") 186 | print(" cd day-16") 187 | print(" uv run python -m langgraph_agent.agent") 188 | 189 | print("\n" + "=" * 60) 190 | print("测试结束") 191 | print("=" * 60) 192 | 193 | 194 | if __name__ == "__main__": 195 | asyncio.run(main()) 196 | -------------------------------------------------------------------------------- /day-16/langgraph_agent/checkpointer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checkpointer 模块 - 状态持久化 3 | 4 | 生产级别的状态持久化实现,支持: 5 | - 内存存储(默认,快速测试) 6 | - SQLite 本地持久化(需要安装 langgraph-checkpoint-sqlite) 7 | - 可扩展到 PostgreSQL/Redis(生产环境) 8 | 9 | 核心功能: 10 | - 保存和恢复 Agent 执行状态 11 | - 支持断点续跑 12 | - 支持 time-travel debugging 13 | 14 | 安装 SQLite 支持: 15 | uv pip install langgraph-checkpoint-sqlite 16 | """ 17 | 18 | import os 19 | import json 20 | import sqlite3 21 | import logging 22 | from typing import Optional, Any, Dict 23 | from datetime import datetime 24 | from contextlib import contextmanager 25 | from dataclasses import dataclass, asdict 26 | from enum import Enum 27 | 28 | # LangGraph 的 checkpointer 29 | from langgraph.checkpoint.memory import MemorySaver 30 | 31 | # SQLite Saver 是可选的,需要单独安装 32 | try: 33 | from langgraph.checkpoint.sqlite import SqliteSaver 34 | SQLITE_AVAILABLE = True 35 | except ImportError: 36 | SQLITE_AVAILABLE = False 37 | SqliteSaver = None 38 | 39 | logger = logging.getLogger(__name__) 40 | 41 | 42 | class CheckpointerType(Enum): 43 | """Checkpointer 类型""" 44 | MEMORY = "memory" 45 | SQLITE = "sqlite" 46 | 47 | 48 | @dataclass 49 | class TaskMetadata: 50 | """任务元数据 - 用于追踪任务状态""" 51 | task_id: str 52 | thread_id: str 53 | created_at: str 54 | updated_at: str 55 | status: str # pending, waiting_approval, approved, rejected, completed, failed 56 | current_node: Optional[str] = None 57 | pending_action: Optional[str] = None 58 | user_input: Optional[str] = None 59 | error_message: Optional[str] = None 60 | 61 | 62 | class TaskStore: 63 | """ 64 | 任务存储 - 管理 Human-in-the-Loop 任务状态 65 | 66 | 与 LangGraph Checkpointer 配合使用: 67 | - Checkpointer: 保存图执行状态 68 | - TaskStore: 保存业务元数据(审批状态、用户输入等) 69 | """ 70 | 71 | def __init__(self, db_path: str = "data/tasks.db"): 72 | self.db_path = db_path 73 | self._ensure_db_dir() 74 | self._init_db() 75 | 76 | def _ensure_db_dir(self): 77 | """确保数据库目录存在""" 78 | db_dir = os.path.dirname(self.db_path) 79 | if db_dir and not os.path.exists(db_dir): 80 | os.makedirs(db_dir) 81 | logger.info(f"Created database directory: {db_dir}") 82 | 83 | def _init_db(self): 84 | """初始化数据库表""" 85 | with self._get_connection() as conn: 86 | conn.execute(""" 87 | CREATE TABLE IF NOT EXISTS tasks ( 88 | task_id TEXT PRIMARY KEY, 89 | thread_id TEXT NOT NULL, 90 | created_at TEXT NOT NULL, 91 | updated_at TEXT NOT NULL, 92 | status TEXT NOT NULL, 93 | current_node TEXT, 94 | pending_action TEXT, 95 | user_input TEXT, 96 | error_message TEXT 97 | ) 98 | """) 99 | conn.execute(""" 100 | CREATE INDEX IF NOT EXISTS idx_tasks_status 101 | ON tasks(status) 102 | """) 103 | conn.execute(""" 104 | CREATE INDEX IF NOT EXISTS idx_tasks_thread 105 | ON tasks(thread_id) 106 | """) 107 | conn.commit() 108 | logger.info("Task store initialized") 109 | 110 | @contextmanager 111 | def _get_connection(self): 112 | """获取数据库连接(上下文管理器)""" 113 | conn = sqlite3.connect(self.db_path) 114 | conn.row_factory = sqlite3.Row 115 | try: 116 | yield conn 117 | finally: 118 | conn.close() 119 | 120 | def create_task(self, task_id: str, thread_id: str, user_input: str) -> TaskMetadata: 121 | """创建新任务""" 122 | now = datetime.utcnow().isoformat() 123 | task = TaskMetadata( 124 | task_id=task_id, 125 | thread_id=thread_id, 126 | created_at=now, 127 | updated_at=now, 128 | status="pending", 129 | user_input=user_input, 130 | ) 131 | 132 | with self._get_connection() as conn: 133 | conn.execute(""" 134 | INSERT INTO tasks 135 | (task_id, thread_id, created_at, updated_at, status, 136 | current_node, pending_action, user_input, error_message) 137 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 138 | """, ( 139 | task.task_id, task.thread_id, task.created_at, task.updated_at, 140 | task.status, task.current_node, task.pending_action, 141 | task.user_input, task.error_message 142 | )) 143 | conn.commit() 144 | 145 | logger.info(f"Created task: {task_id}") 146 | return task 147 | 148 | def get_task(self, task_id: str) -> Optional[TaskMetadata]: 149 | """获取任务""" 150 | with self._get_connection() as conn: 151 | row = conn.execute( 152 | "SELECT * FROM tasks WHERE task_id = ?", (task_id,) 153 | ).fetchone() 154 | 155 | if row: 156 | return TaskMetadata(**dict(row)) 157 | return None 158 | 159 | def update_task(self, task_id: str, **updates) -> Optional[TaskMetadata]: 160 | """更新任务""" 161 | updates["updated_at"] = datetime.utcnow().isoformat() 162 | 163 | set_clause = ", ".join([f"{k} = ?" for k in updates.keys()]) 164 | values = list(updates.values()) + [task_id] 165 | 166 | with self._get_connection() as conn: 167 | conn.execute( 168 | f"UPDATE tasks SET {set_clause} WHERE task_id = ?", 169 | values 170 | ) 171 | conn.commit() 172 | 173 | logger.info(f"Updated task {task_id}: {updates}") 174 | return self.get_task(task_id) 175 | 176 | def get_pending_approvals(self) -> list[TaskMetadata]: 177 | """获取所有等待审批的任务""" 178 | with self._get_connection() as conn: 179 | rows = conn.execute( 180 | "SELECT * FROM tasks WHERE status = 'waiting_approval' ORDER BY created_at" 181 | ).fetchall() 182 | return [TaskMetadata(**dict(row)) for row in rows] 183 | 184 | def get_tasks_by_status(self, status: str) -> list[TaskMetadata]: 185 | """按状态获取任务列表""" 186 | with self._get_connection() as conn: 187 | rows = conn.execute( 188 | "SELECT * FROM tasks WHERE status = ? ORDER BY created_at DESC", 189 | (status,) 190 | ).fetchall() 191 | return [TaskMetadata(**dict(row)) for row in rows] 192 | 193 | 194 | def create_checkpointer( 195 | checkpointer_type: CheckpointerType = CheckpointerType.MEMORY, 196 | db_path: str = "data/checkpoints.db" 197 | ) -> Any: 198 | """ 199 | 创建 Checkpointer 实例 200 | 201 | Args: 202 | checkpointer_type: 存储类型(默认 MEMORY) 203 | db_path: SQLite 数据库路径(仅 SQLITE 类型需要) 204 | 205 | Returns: 206 | LangGraph Checkpointer 实例 207 | 208 | 注意: 209 | SQLite 需要安装: uv pip install langgraph-checkpoint-sqlite 210 | """ 211 | if checkpointer_type == CheckpointerType.MEMORY: 212 | logger.info("Using MemorySaver (data will be lost on restart)") 213 | return MemorySaver() 214 | 215 | elif checkpointer_type == CheckpointerType.SQLITE: 216 | if not SQLITE_AVAILABLE: 217 | logger.warning( 218 | "SQLite checkpointer not available. " 219 | "Install with: uv pip install langgraph-checkpoint-sqlite. " 220 | "Falling back to MemorySaver." 221 | ) 222 | return MemorySaver() 223 | 224 | # 确保目录存在 225 | db_dir = os.path.dirname(db_path) 226 | if db_dir and not os.path.exists(db_dir): 227 | os.makedirs(db_dir) 228 | 229 | logger.info(f"Using SqliteSaver with database: {db_path}") 230 | # 创建连接 231 | conn = sqlite3.connect(db_path, check_same_thread=False) 232 | return SqliteSaver(conn) 233 | 234 | else: 235 | raise ValueError(f"Unknown checkpointer type: {checkpointer_type}") 236 | 237 | 238 | # 全局单例 239 | _task_store: Optional[TaskStore] = None 240 | _checkpointer: Optional[Any] = None 241 | 242 | 243 | def get_task_store(db_path: str = "data/tasks.db") -> TaskStore: 244 | """获取 TaskStore 单例""" 245 | global _task_store 246 | if _task_store is None: 247 | _task_store = TaskStore(db_path) 248 | return _task_store 249 | 250 | 251 | def get_checkpointer( 252 | checkpointer_type: CheckpointerType = CheckpointerType.SQLITE, 253 | db_path: str = "data/checkpoints.db" 254 | ) -> Any: 255 | """获取 Checkpointer 单例""" 256 | global _checkpointer 257 | if _checkpointer is None: 258 | _checkpointer = create_checkpointer(checkpointer_type, db_path) 259 | return _checkpointer 260 | -------------------------------------------------------------------------------- /day-05/README.md: -------------------------------------------------------------------------------- 1 | # Day 5: Production Observability - 生产级可观测性 2 | 3 | ## 凌晨 3 点的噩梦 4 | 5 | 你的 Agent 上线了,用户反馈"有时候回答很慢"、"偶尔答非所问"。 6 | 7 | 你打开日志,看到的是: 8 | 9 | ``` 10 | INFO: Request received 11 | INFO: Response sent 12 | INFO: Request received 13 | INFO: Response sent 14 | ``` 15 | 16 | 这能告诉你什么?什么都没有。 17 | 18 | - 哪个请求慢?慢在哪里? 19 | - LLM 调用了几次?每次花了多久? 20 | - Token 消耗多少?成本怎么样? 21 | - 用户问了什么?Agent 答了什么? 22 | 23 | ## 生产环境需要的是可观测性 24 | 25 | Agent Starter Pack 提供了两层可观测性: 26 | 27 | | 层级 | 内容 | 导出目标 | 默认状态 | 28 | |------|------|----------|----------| 29 | | **Agent Telemetry** | 执行追踪、延迟、系统指标 | Cloud Trace | 始终开启 | 30 | | **Prompt-Response Logging** | LLM 交互、Token 使用 | GCS + BigQuery + Cloud Logging | 部署环境开启 | 31 | 32 | ## 快速开始 33 | 34 | ### 1. 创建 Agent 项目 35 | 36 | ```bash 37 | # 使用 agent-starter-pack 创建项目 38 | uvx agent-starter-pack create 39 | 40 | # 选择 adk_base 模板 41 | ``` 42 | 43 | ### 2. 本地运行(Telemetry 自动配置) 44 | 45 | ```bash 46 | cd your-agent-project 47 | make playground 48 | ``` 49 | 50 | ### 3. 部署到 Cloud Run(完整可观测性) 51 | 52 | ```bash 53 | gcloud config set project YOUR_PROJECT_ID 54 | make deploy 55 | ``` 56 | 57 | 部署后,Terraform 会自动配置: 58 | - Cloud Trace 分布式追踪 59 | - GCS 存储 JSONL 日志 60 | - BigQuery 外部表(SQL 查询日志) 61 | - Cloud Logging 专用 bucket 62 | 63 | ## 核心概念 64 | 65 | ### OpenTelemetry 追踪 66 | 67 | ``` 68 | 用户请求 69 | │ 70 | ▼ 71 | ┌─────────────────────────────────────────────┐ 72 | │ Span: handle_request (总耗时 2.5s) │ 73 | │ ├── Span: parse_input (50ms) │ 74 | │ ├── Span: llm_call_1 (800ms) │ 75 | │ │ └── model: gemini-2.0-flash │ 76 | │ │ └── tokens: 1500 │ 77 | │ ├── Span: tool_execution (600ms) │ 78 | │ │ └── tool: search_database │ 79 | │ ├── Span: llm_call_2 (900ms) │ 80 | │ │ └── model: gemini-2.0-flash │ 81 | │ │ └── tokens: 2000 │ 82 | │ └── Span: format_response (150ms) │ 83 | └─────────────────────────────────────────────┘ 84 | ``` 85 | 86 | 每个 Span 记录: 87 | - 开始/结束时间 88 | - 父子关系 89 | - 自定义属性(model、tokens 等) 90 | - 错误信息 91 | 92 | ### 隐私保护模式 93 | 94 | 默认使用 `NO_CONTENT` 模式,只记录元数据: 95 | 96 | ```python 97 | # 记录的内容 98 | { 99 | "model": "gemini-2.0-flash", 100 | "input_tokens": 1500, 101 | "output_tokens": 500, 102 | "latency_ms": 800, 103 | "timestamp": "2025-01-15T10:30:00Z" 104 | } 105 | 106 | # 不记录 107 | # - 用户的 prompt 108 | # - LLM 的 response 109 | ``` 110 | 111 | 这样既能监控性能,又保护用户隐私。 112 | 113 | ## 代码示例 114 | 115 | ### 基础 Telemetry 配置 116 | 117 | ```python 118 | # app_utils/telemetry.py 119 | import os 120 | import logging 121 | 122 | def setup_telemetry() -> str | None: 123 | """配置 OpenTelemetry 和 GenAI 遥测""" 124 | 125 | bucket = os.environ.get("LOGS_BUCKET_NAME") 126 | capture_content = os.environ.get( 127 | "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "false" 128 | ) 129 | 130 | if bucket and capture_content != "false": 131 | logging.info("Prompt-response logging enabled - mode: NO_CONTENT") 132 | 133 | # 设置为 NO_CONTENT 模式(只记录元数据) 134 | os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = "NO_CONTENT" 135 | os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT", "jsonl") 136 | os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK", "upload") 137 | 138 | # 配置上传路径 139 | path = os.environ.get("GENAI_TELEMETRY_PATH", "completions") 140 | os.environ.setdefault( 141 | "OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH", 142 | f"gs://{bucket}/{path}", 143 | ) 144 | else: 145 | logging.info("Prompt-response logging disabled") 146 | 147 | return bucket 148 | ``` 149 | 150 | ### 自定义 Span 添加 151 | 152 | ```python 153 | from opentelemetry import trace 154 | 155 | tracer = trace.get_tracer(__name__) 156 | 157 | async def process_request(request): 158 | with tracer.start_as_current_span("process_request") as span: 159 | # 添加自定义属性 160 | span.set_attribute("user_id", request.user_id) 161 | span.set_attribute("request_type", request.type) 162 | 163 | # 调用 LLM 164 | with tracer.start_as_current_span("llm_call") as llm_span: 165 | response = await model.generate(request.prompt) 166 | llm_span.set_attribute("model", "gemini-2.0-flash") 167 | llm_span.set_attribute("tokens", response.usage.total_tokens) 168 | 169 | return response 170 | ``` 171 | 172 | ### 查询 BigQuery 日志 173 | 174 | ```sql 175 | -- 查询过去 24 小时的 LLM 调用统计 176 | SELECT 177 | DATE(timestamp) as date, 178 | model, 179 | COUNT(*) as calls, 180 | AVG(latency_ms) as avg_latency, 181 | SUM(input_tokens + output_tokens) as total_tokens 182 | FROM `your-project.telemetry.genai_logs` 183 | WHERE timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 24 HOUR) 184 | GROUP BY date, model 185 | ORDER BY date DESC, calls DESC 186 | ``` 187 | 188 | ## 环境配置 189 | 190 | ### 本地开发 191 | 192 | ```bash 193 | # .env 文件 194 | GOOGLE_CLOUD_PROJECT=your-project-id 195 | 196 | # 可选:启用本地 prompt-response 日志 197 | LOGS_BUCKET_NAME=gs://your-bucket 198 | OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=NO_CONTENT 199 | ``` 200 | 201 | ### 生产环境(Terraform 自动配置) 202 | 203 | ```hcl 204 | # 环境变量由 Terraform 自动设置 205 | resource "google_cloud_run_service" "agent" { 206 | template { 207 | spec { 208 | containers { 209 | env { 210 | name = "LOGS_BUCKET_NAME" 211 | value = google_storage_bucket.logs.name 212 | } 213 | env { 214 | name = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" 215 | value = "NO_CONTENT" 216 | } 217 | } 218 | } 219 | } 220 | } 221 | ``` 222 | 223 | ## 架构图 224 | 225 | ``` 226 | ┌─────────────────────────────────────────────────────────────┐ 227 | │ Agent 应用 │ 228 | │ ┌──────────────────────────────────────────────────────┐ │ 229 | │ │ OpenTelemetry SDK │ │ 230 | │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ 231 | │ │ │ Traces │ │ Metrics │ │ Logs │ │ │ 232 | │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ 233 | │ └─────────┼────────────────┼────────────────┼─────────┘ │ 234 | └────────────┼────────────────┼────────────────┼────────────┘ 235 | │ │ │ 236 | ▼ ▼ ▼ 237 | ┌───────────┐ ┌───────────┐ ┌───────────────┐ 238 | │Cloud Trace│ │Cloud │ │Cloud Logging │ 239 | │ │ │Monitoring │ │(10年保留) │ 240 | └───────────┘ └───────────┘ └───────┬───────┘ 241 | │ 242 | ▼ 243 | ┌───────────────┐ 244 | │ GCS │ 245 | │ (JSONL) │ 246 | └───────┬───────┘ 247 | │ 248 | ▼ 249 | ┌───────────────┐ 250 | │ BigQuery │ 251 | │ (外部表/SQL) │ 252 | └───────────────┘ 253 | ``` 254 | 255 | ## 验证部署 256 | 257 | ### 1. 检查 Cloud Trace 258 | 259 | ```bash 260 | # 打开 Cloud Trace 控制台 261 | open "https://console.cloud.google.com/traces/list?project=YOUR_PROJECT_ID" 262 | ``` 263 | 264 | ### 2. 检查 GCS 日志 265 | 266 | ```bash 267 | # 列出日志文件 268 | gsutil ls gs://YOUR_BUCKET/completions/ 269 | 270 | # 查看日志内容 271 | gsutil cat gs://YOUR_BUCKET/completions/2025/01/15/12/completions_*.jsonl | head -5 272 | ``` 273 | 274 | ### 3. BigQuery 查询 275 | 276 | ```bash 277 | # 运行测试查询 278 | bq query --use_legacy_sql=false \ 279 | 'SELECT COUNT(*) as total_calls FROM `your-project.telemetry.genai_logs`' 280 | ``` 281 | 282 | ## 常见问题 283 | 284 | ### Q: 本地开发时看不到日志? 285 | 286 | 本地开发默认禁用 prompt-response 日志,只有 Cloud Trace 开启。如需启用: 287 | 288 | ```bash 289 | export LOGS_BUCKET_NAME=gs://your-bucket 290 | export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=NO_CONTENT 291 | ``` 292 | 293 | ### Q: LangGraph 模板支持吗? 294 | 295 | LangGraph 模板只支持 Agent Telemetry(Cloud Trace),不支持 Prompt-Response Logging,因为 SDK 限制。 296 | 297 | ### Q: 如何记录完整的 prompt/response? 298 | 299 | 将环境变量改为 `ALL`(注意隐私合规): 300 | 301 | ```bash 302 | OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=ALL 303 | ``` 304 | 305 | ## 核心价值 306 | 307 | | 没有可观测性 | 有可观测性 | 308 | |-------------|-----------| 309 | | "用户说慢" → 不知道哪里慢 | 精确定位到某次 LLM 调用耗时 3s | 310 | | "成本超支" → 不知道哪里花的 | Token 使用量按 model/user/time 统计 | 311 | | "有时出错" → 无法复现 | 追踪链完整记录每一步 | 312 | 313 | **生产环境的 Agent,可观测性不是可选项,而是必需品。** 314 | 315 | ## 扩展阅读 316 | 317 | - [Observability Guide](https://googlecloudplatform.github.io/agent-starter-pack/guide/observability.html) 318 | - [Agent Starter Pack GitHub](https://github.com/GoogleCloudPlatform/agent-starter-pack) 319 | - [OpenTelemetry Python](https://opentelemetry.io/docs/languages/python/) 320 | - [Cloud Trace 文档](https://cloud.google.com/trace/docs) 321 | -------------------------------------------------------------------------------- /day-16/README.md: -------------------------------------------------------------------------------- 1 | # Day 16: LangGraph + A2A + Human-in-the-Loop 2 | 3 | ## 概述 4 | 5 | 本项目展示了两个核心能力: 6 | 7 | 1. **LangGraph + A2A** - 使用 LangGraph 构建 Agent,通过 ADK 暴露 A2A 服务 8 | 2. **Human-in-the-Loop (HITL)** - 生产级人机协作实现,支持敏感操作审批 9 | 10 | ## 练习场景:企业 AI 助手审批系统 11 | 12 | ### 场景描述 13 | 14 | 企业级构建 AI 助手,该助手可以帮助员工执行各种操作。但出于安全考虑,某些敏感操作需要管理员审批后才能执行。 15 | 16 | ``` 17 | ┌─────────────────────────────────────────────────────────────────┐ 18 | │ 企业 AI 助手审批系统 │ 19 | ├─────────────────────────────────────────────────────────────────┤ 20 | │ │ 21 | │ 员工请求 AI 分析 管理员审批 执行 │ 22 | │ ───────── ──────── ────────── ──── │ 23 | │ │ 24 | │ "查询销售数据" → 低风险 ────────────────────────→ 直接执行 │ 25 | │ │ 26 | │ "修改客户信息" → 中风险 → [等待审批] → 批准 ────→ 执行修改 │ 27 | │ │ │ 28 | │ "删除用户账号" → 高风险 → [等待审批] → 拒绝 ────→ 操作取消 │ 29 | │ │ │ 30 | │ "批量发送邮件" → 关键级 → [等待审批] → 超时 ────→ 自动拒绝 │ 31 | │ │ 32 | └─────────────────────────────────────────────────────────────────┘ 33 | ``` 34 | 35 | ### 真实场景示例 36 | 37 | | 员工请求 | 风险评估 | 处理方式 | 38 | |----------|----------|----------| 39 | | "帮我查一下上个月的销售报表" | `low` | 直接执行,返回数据 | 40 | | "把客户张三的手机号改成 138xxx" | `medium` | 需要审批,防止误操作 | 41 | | "删除离职员工小王的所有数据" | `high` | 必须审批,不可逆操作 | 42 | | "给所有 VIP 客户发送促销邮件" | `critical` | 必须审批,影响范围大 | 43 | 44 | ### 解决的核心问题 45 | 46 | > **Agent 自主性 vs 安全性的平衡** 47 | 48 | - 太自主 → 可能执行危险操作,造成损失 49 | - 太保守 → 每个操作都要审批,效率低下 50 | - **HITL 方案** → AI 自动评估风险,仅对高风险操作要求审批 51 | 52 | ## 核心知识点 53 | 54 | ### LangGraph 三大核心组件 55 | 56 | ``` 57 | ┌─────────────────────────────────────────────────────────────┐ 58 | │ [START] ──► [Node A] ──► [Node B] ──► [END] │ 59 | │ │ ▲ │ 60 | │ └──► [Node C]┘ (条件边) │ 61 | └─────────────────────────────────────────────────────────────┘ 62 | ``` 63 | 64 | | 组件 | 说明 | 65 | |------|------| 66 | | **State** | 共享内存对象,在所有节点间流转 | 67 | | **Nodes** | 执行逻辑的函数,接收状态返回更新 | 68 | | **Edges** | 节点间转换(静态边/条件边) | 69 | 70 | ### Human-in-the-Loop 架构 71 | 72 | ``` 73 | ┌─────────┐ ┌──────────┐ ┌─────────────┐ ┌─────────┐ 74 | │ Analyze │ ─► │ Classify │ ─► │ Human │ ─► │ Execute │ 75 | │ Request │ │ Risk │ │ Review │ │ Action │ 76 | └─────────┘ └──────────┘ └─────────────┘ └─────────┘ 77 | │ │ 78 | │ Low Risk │ Rejected 79 | └─────────────────┴─────────► [END] 80 | ``` 81 | 82 | **风险等级:** 83 | 84 | | 等级 | 说明 | 是否审批 | 85 | |------|------|----------| 86 | | `low` | 只读查询,无副作用 | 否 | 87 | | `medium` | 数据修改,可撤销 | 可配置 | 88 | | `high` | 数据删除、外部 API 调用 | 是 | 89 | | `critical` | 支付、批量操作、不可逆 | 是 + 二次确认 | 90 | 91 | **核心特性:** 92 | 93 | - **断点续跑** - 服务重启后可继续执行(需安装 SQLite 支持) 94 | - **超时自动拒绝** - 防止任务无限等待(默认 5 分钟) 95 | - **完整审计日志** - 追踪所有操作 96 | - **状态持久化** - 任务元数据保存到 SQLite 97 | 98 | ## 项目结构 99 | 100 | ``` 101 | day-16/ 102 | ├── README.md 103 | ├── requirements.txt 104 | ├── langgraph_agent/ 105 | │ ├── __init__.py 106 | │ ├── agent.py # LangGraph ReAct Agent + A2A 107 | │ ├── hitl_agent.py # Human-in-the-Loop Agent 核心 108 | │ ├── checkpointer.py # 状态持久化 109 | │ └── server.py # REST API 服务 110 | ├── client/ 111 | │ ├── test_client.py # A2A 测试客户端 112 | │ └── hitl_client.py # HITL 交互客户端 113 | └── data/ # 运行时生成 114 | ├── checkpoints.db # LangGraph 状态 115 | └── tasks.db # 任务元数据 116 | ``` 117 | 118 | ## 运行示例 119 | 120 | ### 1. 安装依赖 121 | 122 | ```bash 123 | cd day-16 124 | uv pip install -r requirements.txt 125 | 126 | # 可选:安装 SQLite 持久化支持(生产环境推荐) 127 | uv pip install langgraph-checkpoint-sqlite 128 | ``` 129 | 130 | ### 2. 运行 HITL 服务 131 | 132 | ```bash 133 | uv run python -m langgraph_agent.server 134 | ``` 135 | 136 | 输出: 137 | ``` 138 | ============================================================ 139 | Day 16: Human-in-the-Loop Agent Server 140 | ============================================================ 141 | 142 | 访问地址: 143 | Web UI: http://localhost:8016/ui <- 推荐 144 | API Docs: http://localhost:8016/docs 145 | 146 | 审批超时: 300 秒 147 | ============================================================ 148 | ``` 149 | 150 | ### 3. 打开 Web UI(推荐) 151 | 152 | 浏览器访问: **http://localhost:8016/ui** 153 | 154 | ![Demo](https://fisherai-1312281807.cos.ap-guangzhou.myqcloud.com/CleanShot%202025-12-17%20at%2015.05.12.gif) 155 | 156 | Web UI 功能: 157 | - 📝 提交请求(带快捷示例按钮) 158 | - ⏳ 查看待审批任务 159 | - ✅❌ 一键批准/拒绝 160 | - 📋 任务历史和统计 161 | - 🔍 点击历史记录查看详情 162 | 163 | ### 4. 或使用命令行客户端 164 | 165 | ```bash 166 | uv run python -m client.hitl_client 167 | ``` 168 | 169 | ### 5. 完整演示流程 170 | 171 | ```bash 172 | # 1. 启动服务 173 | uv run python -m langgraph_agent.server 174 | 175 | # 2. 在另一个终端运行客户端,选择 "6. 运行所有演示" 176 | uv run python -m client.hitl_client 177 | 178 | # 演示包括: 179 | # - 低风险任务(查询天气)→ 自动执行 180 | # - 高风险任务(删除数据)→ 等待审批 → 拒绝 181 | # - 中风险任务(发送邮件)→ 等待审批 → 批准 182 | ``` 183 | 184 | ### 6. 使用 curl 测试(可选) 185 | 186 | ```bash 187 | # 场景 1: 低风险查询 - 直接返回结果 188 | curl -X POST http://localhost:8016/tasks \ 189 | -H "Content-Type: application/json" \ 190 | -d '{"message": "查询今天北京的天气"}' 191 | 192 | # 场景 2: 高风险操作 - 需要审批 193 | curl -X POST http://localhost:8016/tasks \ 194 | -H "Content-Type: application/json" \ 195 | -d '{"message": "删除用户 test@example.com 的所有数据"}' 196 | 197 | # 查看待审批任务 198 | curl http://localhost:8016/tasks/pending 199 | 200 | # 批准任务(替换 {task_id}) 201 | curl -X POST http://localhost:8016/tasks/{task_id}/approve \ 202 | -H "Content-Type: application/json" \ 203 | -d '{"approved": true, "comment": "已确认用户已离职", "approver": "admin"}' 204 | 205 | # 或者拒绝 206 | curl -X POST http://localhost:8016/tasks/{task_id}/reject \ 207 | -H "Content-Type: application/json" \ 208 | -d '{"approved": false, "comment": "需要先备份数据", "approver": "admin"}' 209 | ``` 210 | 211 | ### 5. 运行原有的 A2A 服务 212 | 213 | ```bash 214 | # Terminal 1 - 启动 A2A 服务 215 | uv run python -m langgraph_agent.agent 216 | 217 | # Terminal 2 - 运行测试 218 | uv run python -m client.test_client 219 | ``` 220 | 221 | ## 代码核心 222 | 223 | ### 1. HITL Graph 构建 224 | 225 | ```python 226 | from langgraph.graph import StateGraph, END 227 | 228 | class HITLState(TypedDict): 229 | task_id: str 230 | messages: Annotated[list, add] 231 | risk_level: str 232 | requires_approval: bool 233 | approval_status: str # pending, approved, rejected 234 | final_answer: str 235 | 236 | def create_hitl_graph(checkpointer): 237 | graph = StateGraph(HITLState) 238 | 239 | # 添加节点 240 | graph.add_node("analyze", analyze_request_node) 241 | graph.add_node("human_review", human_review_node) 242 | graph.add_node("execute", execute_action_node) 243 | graph.add_node("respond", generate_response_node) 244 | 245 | # 条件路由:根据风险等级决定是否需要审批 246 | graph.add_conditional_edges( 247 | "analyze", 248 | route_after_analysis, 249 | {"human_review": "human_review", "execute": "execute"} 250 | ) 251 | 252 | # 关键:在审批节点前中断,等待外部输入 253 | return graph.compile( 254 | checkpointer=checkpointer, 255 | interrupt_before=["human_review"], 256 | ) 257 | ``` 258 | 259 | ### 2. 状态持久化 260 | 261 | ```python 262 | from langgraph.checkpoint.memory import MemorySaver 263 | # 或使用 SQLite(需要安装 langgraph-checkpoint-sqlite) 264 | # from langgraph.checkpoint.sqlite import SqliteSaver 265 | 266 | checkpointer = MemorySaver() 267 | 268 | # 图执行时使用 thread_id 区分不同会话 269 | config = {"configurable": {"thread_id": "unique-thread-id"}} 270 | result = graph.invoke(initial_state, config) 271 | ``` 272 | 273 | ### 3. 审批后恢复执行 274 | 275 | ```python 276 | def resume_after_approval(task_id, thread_id, approved): 277 | graph = create_hitl_graph(checkpointer) 278 | config = {"configurable": {"thread_id": thread_id}} 279 | 280 | # 注入审批结果到图状态 281 | graph.update_state(config, { 282 | "approval_status": "approved" if approved else "rejected" 283 | }) 284 | 285 | # 从中断点继续执行 286 | result = graph.invoke(None, config) 287 | return result 288 | ``` 289 | 290 | ## 关键学习点 291 | 292 | 1. **interrupt_before** - LangGraph 的中断机制,让图在指定节点前暂停 293 | 2. **Checkpointer** - 状态持久化,支持断点续跑 294 | 3. **update_state** - 外部更新图状态,注入审批结果 295 | 4. **风险评估** - LLM 自动评估操作风险等级 296 | 5. **超时机制** - 后台任务定期检查,超时自动拒绝 297 | 298 | ## API 文档 299 | 300 | 启动服务后访问: http://localhost:8016/docs 301 | 302 | | 端点 | 方法 | 说明 | 303 | |------|------|------| 304 | | `/tasks` | POST | 创建新任务 | 305 | | `/tasks` | GET | 获取任务列表 | 306 | | `/tasks/pending` | GET | 获取待审批任务 | 307 | | `/tasks/{id}` | GET | 获取任务详情 | 308 | | `/tasks/{id}/approve` | POST | 批准任务 | 309 | | `/tasks/{id}/reject` | POST | 拒绝任务 | 310 | | `/health` | GET | 健康检查 | 311 | 312 | ## 配置项 313 | 314 | | 环境变量 | 默认值 | 说明 | 315 | |----------|--------|------| 316 | | `GOOGLE_API_KEY` | - | Google AI API Key | 317 | | `APPROVAL_TIMEOUT_SECONDS` | 300 | 审批超时时间(秒) | 318 | | `HITL_SERVER_URL` | http://localhost:8016 | 服务地址 | 319 | 320 | ## 扩展练习 321 | 322 | 1. **添加更多风险类型** - 扩展 `analyze_request_node` 支持更细粒度的风险判断 323 | 2. **接入真实工具** - 将 `execute_action_node` 中的模拟执行替换为真实 API 调用 324 | 3. **添加审批层级** - 高风险需要一级审批,关键级需要二级审批 325 | 4. **集成通知系统** - 待审批时发送 Slack/邮件通知给管理员 326 | 327 | ## 扩展阅读 328 | 329 | - [LangGraph Human-in-the-Loop](https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/) 330 | - [LangGraph Checkpointing](https://langchain-ai.github.io/langgraph/concepts/persistence/) 331 | - [A2A Protocol](https://a2a-protocol.org/) 332 | -------------------------------------------------------------------------------- /day-16/langgraph_agent/agent.py: -------------------------------------------------------------------------------- 1 | """ 2 | Day 16: LangGraph + A2A Agent 3 | 4 | 这个示例展示了如何: 5 | 1. 使用 LangGraph 构建一个 ReAct 推理图 6 | 2. 将其包装为 ADK Agent 7 | 3. 通过 ADK 的 to_a2a() 对外提供 A2A 服务 8 | 9 | 运行方式: 10 | python -m langgraph_agent.agent 11 | """ 12 | 13 | import os 14 | import sys 15 | from typing import TypedDict, Annotated, Literal 16 | from operator import add 17 | 18 | # 添加项目根目录到路径 19 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 20 | 21 | # 加载 .env 文件中的环境变量 22 | from dotenv import load_dotenv 23 | project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 24 | load_dotenv(os.path.join(project_root, ".env")) 25 | 26 | # LangGraph 相关导入 27 | from langgraph.graph import StateGraph, END 28 | from langchain_google_genai import ChatGoogleGenerativeAI 29 | from langchain_core.messages import HumanMessage, AIMessage, SystemMessage 30 | 31 | # ADK 相关导入 (用于 A2A 服务) 32 | from google.adk.agents import LlmAgent 33 | from google.adk.a2a.utils.agent_to_a2a import to_a2a 34 | 35 | 36 | # ============================================================ 37 | # Part 1: LangGraph ReAct Agent 实现 38 | # ============================================================ 39 | 40 | class AgentState(TypedDict): 41 | """ 42 | LangGraph Agent 的状态定义 43 | 44 | 这是 LangGraph 的核心概念之一:State 45 | 状态在图的所有节点之间共享,每个节点可以读取和更新状态 46 | """ 47 | # 消息历史 - 使用 Annotated 和 add 操作符实现消息追加 48 | messages: Annotated[list, add] 49 | # 思考过程记录 50 | thoughts: list 51 | # 最终答案 52 | final_answer: str 53 | # 迭代次数(防止无限循环) 54 | iteration: int 55 | 56 | 57 | def get_llm(): 58 | """获取 Google Gemini LLM 实例""" 59 | return ChatGoogleGenerativeAI( 60 | model="gemini-2.0-flash", 61 | google_api_key=os.getenv("GOOGLE_API_KEY"), 62 | temperature=0.7, 63 | ) 64 | 65 | 66 | def reasoning_node(state: AgentState) -> dict: 67 | """ 68 | 推理节点 - ReAct 模式中的 "Thought" 步骤 69 | 70 | 这是 LangGraph 的核心概念之一:Node 71 | 节点是执行具体逻辑的函数,接收状态,返回状态更新 72 | """ 73 | llm = get_llm() 74 | 75 | system_prompt = """你是一个善于分析问题的 AI 助手。 76 | 77 | 请按照以下格式思考: 78 | 1. 分析问题的核心需求 79 | 2. 思考需要什么信息来回答 80 | 3. 决定是直接回答还是需要更多分析 81 | 82 | 如果你已经有足够信息回答问题,请在回复开头加上 [FINAL] 83 | 如果需要继续分析,请在回复开头加上 [CONTINUE]""" 84 | 85 | messages = state.get("messages", []) 86 | thoughts = state.get("thoughts", []) 87 | iteration = state.get("iteration", 0) 88 | 89 | # 构建 LLM 消息 90 | llm_messages = [SystemMessage(content=system_prompt)] 91 | 92 | for msg in messages: 93 | if isinstance(msg, dict): 94 | if msg.get("role") == "user": 95 | llm_messages.append(HumanMessage(content=msg.get("content", ""))) 96 | else: 97 | llm_messages.append(AIMessage(content=msg.get("content", ""))) 98 | elif isinstance(msg, (HumanMessage, AIMessage)): 99 | llm_messages.append(msg) 100 | 101 | # 添加之前的思考过程 102 | if thoughts: 103 | thought_summary = "\n".join([f"思考 {i+1}: {t}" for i, t in enumerate(thoughts)]) 104 | llm_messages.append(SystemMessage(content=f"之前的思考:\n{thought_summary}")) 105 | 106 | # 调用 LLM 107 | response = llm.invoke(llm_messages) 108 | thought = response.content 109 | new_thoughts = thoughts + [thought] 110 | 111 | # 判断是否结束 112 | if "[FINAL]" in thought or iteration >= 2: 113 | return { 114 | "thoughts": new_thoughts, 115 | "iteration": iteration + 1, 116 | } 117 | else: 118 | return { 119 | "thoughts": new_thoughts, 120 | "iteration": iteration + 1, 121 | } 122 | 123 | 124 | def answer_node(state: AgentState) -> dict: 125 | """ 126 | 回答节点 - 生成最终答案 127 | """ 128 | llm = get_llm() 129 | 130 | messages = state.get("messages", []) 131 | thoughts = state.get("thoughts", []) 132 | 133 | # 提取原始问题 134 | original_question = "" 135 | for msg in messages: 136 | if isinstance(msg, dict) and msg.get("role") == "user": 137 | original_question = msg.get("content", "") 138 | break 139 | elif isinstance(msg, HumanMessage): 140 | original_question = msg.content 141 | break 142 | 143 | # 构建最终回答提示 144 | thought_summary = "\n".join([f"- {t}" for t in thoughts]) if thoughts else "(直接回答)" 145 | 146 | final_prompt = f"""用户问题:{original_question} 147 | 148 | 分析过程: 149 | {thought_summary} 150 | 151 | 请基于以上分析,给出清晰、准确、有帮助的最终回答:""" 152 | 153 | llm_messages = [ 154 | SystemMessage(content="你是一个专业的 AI 助手,请简洁明了地回答用户问题。"), 155 | HumanMessage(content=final_prompt), 156 | ] 157 | 158 | response = llm.invoke(llm_messages) 159 | 160 | return {"final_answer": response.content} 161 | 162 | 163 | def should_continue(state: AgentState) -> Literal["continue", "answer"]: 164 | """ 165 | 路由函数 - 决定下一步执行哪个节点 166 | 167 | 这是 LangGraph 的核心概念之一:Conditional Edge 168 | 条件边基于当前状态动态决定控制流 169 | """ 170 | thoughts = state.get("thoughts", []) 171 | iteration = state.get("iteration", 0) 172 | 173 | if not thoughts: 174 | return "continue" 175 | 176 | last_thought = thoughts[-1] if thoughts else "" 177 | 178 | # 如果达到最大迭代次数或决定回答 179 | if "[FINAL]" in last_thought or iteration >= 2: 180 | return "answer" 181 | 182 | return "continue" 183 | 184 | 185 | def create_langgraph_agent(): 186 | """ 187 | 创建 LangGraph Agent 188 | 189 | 展示 LangGraph 的核心构建流程: 190 | 1. 创建 StateGraph 191 | 2. 添加节点 (Nodes) 192 | 3. 定义边 (Edges),包括条件边 193 | 4. 设置入口点 194 | 5. 编译图 195 | """ 196 | # 创建状态图 197 | graph = StateGraph(AgentState) 198 | 199 | # 添加节点 200 | graph.add_node("reasoning", reasoning_node) 201 | graph.add_node("answer", answer_node) 202 | 203 | # 设置入口点 204 | graph.set_entry_point("reasoning") 205 | 206 | # 添加条件边 - 这是 LangGraph 实现动态控制流的关键 207 | graph.add_conditional_edges( 208 | "reasoning", 209 | should_continue, 210 | { 211 | "continue": "reasoning", # 继续推理 212 | "answer": "answer", # 生成答案 213 | } 214 | ) 215 | 216 | # 添加结束边 217 | graph.add_edge("answer", END) 218 | 219 | # 编译图 220 | return graph.compile() 221 | 222 | 223 | def run_langgraph(user_input: str) -> str: 224 | """ 225 | 运行 LangGraph Agent 226 | 227 | 这个函数可以被 ADK Agent 的工具调用 228 | """ 229 | agent = create_langgraph_agent() 230 | 231 | # 准备输入状态 232 | input_state = { 233 | "messages": [{"role": "user", "content": user_input}], 234 | "thoughts": [], 235 | "final_answer": "", 236 | "iteration": 0, 237 | } 238 | 239 | # 执行图 240 | result = agent.invoke(input_state) 241 | 242 | return result.get("final_answer", "抱歉,我无法处理这个请求。") 243 | 244 | 245 | # ============================================================ 246 | # Part 2: 使用 ADK 将 LangGraph Agent 包装为 A2A 服务 247 | # ============================================================ 248 | 249 | # 创建 ADK Agent,内部调用 LangGraph 250 | # 这展示了如何将 LangGraph 的复杂推理能力与 ADK 的 A2A 服务能力结合 251 | 252 | langgraph_adk_agent = LlmAgent( 253 | name="langgraph_react_agent", 254 | description="基于 LangGraph 构建的 ReAct 推理 Agent - 能够进行多步骤推理分析,深思熟虑后给出答案", 255 | model="gemini-2.0-flash", 256 | instruction="""你是一个强大的 AI 助手,具有深度推理能力。 257 | 258 | 你的特点: 259 | - 使用 ReAct (Reasoning + Acting) 模式进行思考 260 | - 能够分析复杂问题,进行多步骤推理 261 | - 在回答前会仔细思考,确保答案的准确性和有用性 262 | 263 | 当用户提问时,你会: 264 | 1. 先分析问题的核心需求 265 | 2. 思考需要什么信息或知识来回答 266 | 3. 给出清晰、准确、有帮助的答案 267 | 268 | 你擅长: 269 | - 技术问题解答(编程、架构、AI/ML 等) 270 | - 概念解释和对比分析 271 | - 问题分析和解决方案建议 272 | """, 273 | ) 274 | 275 | # 使用 ADK 的 to_a2a() 将 Agent 转换为 A2A 服务 276 | # 这是最简单可靠的方式,ADK 会自动处理所有 A2A 协议细节 277 | app = to_a2a( 278 | agent=langgraph_adk_agent, 279 | host="localhost", 280 | port=8016, 281 | protocol="http" 282 | ) 283 | 284 | 285 | # ============================================================ 286 | # Part 3: 独立的 LangGraph 演示(不使用 A2A) 287 | # ============================================================ 288 | 289 | def demo_langgraph_only(): 290 | """ 291 | 纯 LangGraph 演示,不涉及 A2A 292 | 用于理解 LangGraph 的工作原理 293 | """ 294 | print("\n" + "=" * 60) 295 | print("LangGraph ReAct Agent 演示") 296 | print("=" * 60) 297 | 298 | test_questions = [ 299 | "什么是 LangGraph?它和 LangChain 有什么关系?", 300 | "解释一下 ReAct 模式是什么?", 301 | ] 302 | 303 | for i, question in enumerate(test_questions, 1): 304 | print(f"\n问题 {i}: {question}") 305 | print("-" * 40) 306 | 307 | answer = run_langgraph(question) 308 | print(f"回答: {answer[:500]}...") 309 | print("-" * 40) 310 | 311 | 312 | # ============================================================ 313 | # 主程序 314 | # ============================================================ 315 | 316 | if __name__ == "__main__": 317 | import uvicorn 318 | import argparse 319 | 320 | parser = argparse.ArgumentParser(description="Day 16: LangGraph + A2A") 321 | parser.add_argument("--demo", action="store_true", help="运行纯 LangGraph 演示(不启动 A2A 服务)") 322 | args = parser.parse_args() 323 | 324 | if args.demo: 325 | # 纯 LangGraph 演示模式 326 | demo_langgraph_only() 327 | else: 328 | # A2A 服务模式 329 | print("=" * 60) 330 | print("Day 16: LangGraph + A2A") 331 | print("=" * 60) 332 | print() 333 | print("Agent 信息:") 334 | print(f" 名称: {langgraph_adk_agent.name}") 335 | print(f" 框架: LangGraph + ADK") 336 | print(f" 模式: ReAct (Reasoning + Acting)") 337 | print() 338 | print("A2A 服务端点:") 339 | print(" URL: http://localhost:8016") 340 | print(" Agent Card: http://localhost:8016/.well-known/agent.json") 341 | print() 342 | print("提示:") 343 | print(" - 运行客户端: python -m client.test_client") 344 | print(" - 纯 LangGraph 演示: python -m langgraph_agent.agent --demo") 345 | print("=" * 60) 346 | print() 347 | 348 | # 启动 A2A 服务 349 | uvicorn.run(app, host="localhost", port=8016) 350 | -------------------------------------------------------------------------------- /day-14/host_agent/agent.py: -------------------------------------------------------------------------------- 1 | """ 2 | Day 14: Host Agent (A2A Client) 3 | 4 | 这是主控 Agent,它通过 A2A 协议调用远程的翻译 Agent。 5 | 演示了如何作为 A2A 客户端与远程 Agent 进行通信。 6 | 7 | 运行前确保: 8 | 1. 设置环境变量: export GOOGLE_API_KEY="your-api-key" 9 | 2. 远程 Agent 已经在 http://localhost:8001 运行 10 | 3. 运行: python -m host_agent.agent 11 | """ 12 | 13 | import asyncio 14 | import os 15 | import sys 16 | import uuid 17 | 18 | # 添加项目根目录到路径 19 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 20 | 21 | import httpx 22 | from a2a.client import ClientFactory, ClientConfig 23 | from a2a.types import Role 24 | from a2a.client.helpers import create_text_message_object 25 | 26 | # 远程 Agent 的 URL 27 | REMOTE_AGENT_URL = "http://localhost:8001" 28 | 29 | 30 | def get_client_config() -> ClientConfig: 31 | """创建不使用代理的客户端配置""" 32 | # 创建 httpx 客户端,完全禁用代理以避免本地连接问题 33 | # trust_env=False 会忽略环境变量中的代理设置 34 | httpx_client = httpx.AsyncClient( 35 | trust_env=False, 36 | timeout=120.0, 37 | ) 38 | return ClientConfig(httpx_client=httpx_client) 39 | 40 | 41 | # ============================================================ 42 | # 1. A2A 客户端 - 直接调用远程 Agent 43 | # ============================================================ 44 | 45 | async def call_remote_agent_directly(text_to_translate: str, target_language: str = "中文"): 46 | """ 47 | 直接使用 A2A 客户端调用远程翻译 Agent 48 | 49 | 这是最基本的 A2A 调用方式,展示了: 50 | - 如何创建 A2A 客户端 51 | - 如何构造请求消息 52 | - 如何处理响应 53 | """ 54 | print("\n" + "=" * 60) 55 | print("方法 1: 直接调用远程 Agent (A2A Client)") 56 | print("=" * 60) 57 | 58 | # 使用 ClientFactory.connect() 连接远程 Agent 59 | # 这会自动获取 Agent Card 并创建合适的客户端 60 | client = await ClientFactory.connect(REMOTE_AGENT_URL, client_config=get_client_config()) 61 | 62 | # 获取 Agent Card 信息 (使用 _card 属性) 63 | print("\n📇 Agent 信息:") 64 | print(f" 名称: {client._card.name}") 65 | print(f" 描述: {client._card.description[:80]}...") 66 | print(f" 技能: {[skill.name for skill in (client._card.skills or [])]}") 67 | 68 | # 构造翻译请求消息 69 | request_text = f"请将以下文本翻译成{target_language}:\n{text_to_translate}" 70 | 71 | print(f"\n📤 发送请求: {request_text[:60]}...") 72 | 73 | # 创建消息对象 (注意: 参数是 content 不是 text) 74 | message = create_text_message_object( 75 | role=Role.user, 76 | content=request_text, 77 | ) 78 | 79 | # 发送消息给远程 Agent (注意: 参数是 request 不是 message) 80 | result = None 81 | async for event in client.send_message(request=message): 82 | # 处理返回的事件 83 | # event 是 (Task, TaskEvent) 或 Message 元组 84 | if isinstance(event, tuple) and len(event) >= 2: 85 | task, task_event = event[0], event[1] 86 | if task_event: 87 | # 处理 TaskStatusUpdateEvent 或 TaskArtifactUpdateEvent 88 | if hasattr(task_event, 'status') and task_event.status: 89 | state = task_event.status.state 90 | print(f" [状态] {state}") 91 | if task_event.status.message: 92 | for part in task_event.status.message.parts: 93 | if hasattr(part, 'text') and part.text: 94 | result = part.text 95 | elif hasattr(task_event, 'artifact') and task_event.artifact: 96 | for part in task_event.artifact.parts: 97 | if hasattr(part, 'text') and part.text: 98 | result = part.text 99 | 100 | if result: 101 | print(f"\n📥 翻译结果:\n {result}") 102 | 103 | return result 104 | 105 | 106 | # ============================================================ 107 | # 2. 流式调用 - 实时获取翻译进度 108 | # ============================================================ 109 | 110 | async def call_remote_agent_streaming(text_to_translate: str, target_language: str = "中文"): 111 | """ 112 | 使用流式模式调用远程 Agent 113 | 114 | 适用于长文本翻译,可以实时看到翻译进度 115 | """ 116 | print("\n" + "=" * 60) 117 | print("方法 2: 流式调用远程 Agent (Streaming)") 118 | print("=" * 60) 119 | 120 | client = await ClientFactory.connect(REMOTE_AGENT_URL, client_config=get_client_config()) 121 | 122 | request_text = f"请将以下文本翻译成{target_language},并提供详细的翻译说明:\n{text_to_translate}" 123 | 124 | print(f"\n📤 发送流式请求...") 125 | print(f" 原文: {text_to_translate[:50]}...") 126 | print("\n📥 实时响应:") 127 | 128 | message = create_text_message_object( 129 | role=Role.user, 130 | content=request_text, 131 | ) 132 | 133 | collected_text = [] 134 | async for event in client.send_message(request=message): 135 | if isinstance(event, tuple) and len(event) >= 2: 136 | task, task_event = event[0], event[1] 137 | if task_event: 138 | if hasattr(task_event, 'status') and task_event.status: 139 | state = task_event.status.state 140 | print(f" [状态更新] {state}") 141 | if task_event.status.message: 142 | for part in task_event.status.message.parts: 143 | if hasattr(part, 'text') and part.text: 144 | collected_text.append(part.text) 145 | print(f" {part.text[:100]}...") 146 | elif hasattr(task_event, 'artifact') and task_event.artifact: 147 | print(" [产出物] ", end="") 148 | for part in task_event.artifact.parts: 149 | if hasattr(part, 'text') and part.text: 150 | collected_text.append(part.text) 151 | print(part.text[:100], end="...") 152 | print() 153 | 154 | if collected_text: 155 | print(f"\n 完整结果: {collected_text[-1][:200]}...") 156 | 157 | 158 | # ============================================================ 159 | # 3. 多轮对话 - 保持上下文 160 | # ============================================================ 161 | 162 | async def multi_turn_conversation(): 163 | """ 164 | 演示与远程 Agent 的多轮对话 165 | 166 | A2A 支持通过 context_id 保持对话上下文 167 | """ 168 | print("\n" + "=" * 60) 169 | print("方法 3: 多轮对话 (Multi-turn Conversation)") 170 | print("=" * 60) 171 | 172 | client = await ClientFactory.connect(REMOTE_AGENT_URL, client_config=get_client_config()) 173 | 174 | context_id = str(uuid.uuid4()) # 创建会话上下文 ID 175 | 176 | conversations = [ 177 | "请翻译:The quick brown fox jumps over the lazy dog.", 178 | "这句话有什么特别之处吗?", 179 | "能给我一个中文版本的类似句子吗?", 180 | ] 181 | 182 | for i, user_input in enumerate(conversations, 1): 183 | print(f"\n🗣️ 轮次 {i}: {user_input}") 184 | 185 | message = create_text_message_object( 186 | role=Role.user, 187 | content=user_input, 188 | ) 189 | 190 | result_text = None 191 | # 注意: 当前 SDK 版本不支持 context_id 参数 192 | # 多轮对话上下文需要通过其他方式管理 193 | async for event in client.send_message(request=message): 194 | if isinstance(event, tuple) and len(event) >= 2: 195 | task, task_event = event[0], event[1] 196 | if task_event: 197 | if hasattr(task_event, 'status') and task_event.status and task_event.status.message: 198 | for part in task_event.status.message.parts: 199 | if hasattr(part, 'text') and part.text: 200 | result_text = part.text 201 | elif hasattr(task_event, 'artifact') and task_event.artifact: 202 | for part in task_event.artifact.parts: 203 | if hasattr(part, 'text') and part.text: 204 | result_text = part.text 205 | 206 | if result_text: 207 | # 截断显示 208 | display_text = result_text[:300] + "..." if len(result_text) > 300 else result_text 209 | print(f"🤖 回复: {display_text}") 210 | 211 | 212 | # ============================================================ 213 | # 简单测试函数 214 | # ============================================================ 215 | 216 | async def simple_test(): 217 | """最简单的 A2A 调用示例""" 218 | print("\n" + "=" * 60) 219 | print("简单测试: A2A 连接和调用") 220 | print("=" * 60) 221 | 222 | # 1. 连接到远程 Agent 223 | print("\n1️⃣ 连接远程 Agent...") 224 | client = await ClientFactory.connect(REMOTE_AGENT_URL, client_config=get_client_config()) 225 | print(f" ✅ 连接成功! Agent: {client._card.name}") 226 | 227 | # 2. 发送简单请求 228 | print("\n2️⃣ 发送翻译请求...") 229 | message = create_text_message_object( 230 | role=Role.user, 231 | content="请翻译:Hello World" 232 | ) 233 | 234 | print(" 等待响应...") 235 | async for event in client.send_message(request=message): 236 | if isinstance(event, tuple) and len(event) >= 2: 237 | task, task_event = event[0], event[1] 238 | if task_event: 239 | if hasattr(task_event, 'artifact') and task_event.artifact: 240 | for part in task_event.artifact.parts: 241 | if hasattr(part, 'text') and part.text: 242 | print(f"\n3️⃣ 收到翻译结果: {part.text}") 243 | 244 | 245 | # ============================================================ 246 | # 主程序 247 | # ============================================================ 248 | 249 | async def main(): 250 | print("=" * 60) 251 | print("Day 14: A2A Host Agent - 调用远程翻译服务") 252 | print("=" * 60) 253 | print() 254 | print(f"远程 Agent URL: {REMOTE_AGENT_URL}") 255 | print("确保远程 Agent 已运行: python -m remote_agent.agent") 256 | print() 257 | 258 | try: 259 | # 首先进行简单测试 260 | await simple_test() 261 | 262 | # 方法 1: 直接调用 263 | await call_remote_agent_directly( 264 | "Hello, World! Welcome to the Agent2Agent protocol.", 265 | "中文" 266 | ) 267 | 268 | # 再测试一个中译英 269 | await call_remote_agent_directly( 270 | "人工智能正在改变我们的生活方式。", 271 | "英文" 272 | ) 273 | 274 | # 方法 2: 流式调用 275 | await call_remote_agent_streaming( 276 | "Artificial Intelligence (AI) is transforming industries worldwide. " 277 | "From healthcare to finance, AI systems are automating tasks.", 278 | "中文" 279 | ) 280 | 281 | # 方法 3: 多轮对话 282 | await multi_turn_conversation() 283 | 284 | except Exception as e: 285 | print(f"\n❌ 错误: {e}") 286 | import traceback 287 | traceback.print_exc() 288 | print("\n请确保:") 289 | print("1. 远程 Agent 正在运行 (python -m remote_agent.agent)") 290 | print("2. 端口 8001 没有被占用") 291 | print("3. GOOGLE_API_KEY 环境变量已设置") 292 | 293 | print("\n" + "=" * 60) 294 | print("演示结束") 295 | print("=" * 60) 296 | 297 | 298 | if __name__ == "__main__": 299 | asyncio.run(main()) 300 | -------------------------------------------------------------------------------- /day-15/a2ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | A2UI Python SDK - Agent-to-User Interface 生成器 3 | 4 | 这个模块提供了生成 A2UI JSONL 的 Python API。 5 | A2UI 是 Google 推出的声明式 UI 协议,让 Agent 能安全地生成动态界面。 6 | """ 7 | 8 | import json 9 | from typing import List, Dict, Any, Optional, Union 10 | from dataclasses import dataclass, field 11 | 12 | 13 | @dataclass 14 | class A2UIComponent: 15 | """A2UI 组件""" 16 | id: str 17 | type: str 18 | props: Dict[str, Any] = field(default_factory=dict) 19 | 20 | def to_dict(self) -> Dict: 21 | return { 22 | "id": self.id, 23 | "component": {self.type: self.props} 24 | } 25 | 26 | 27 | class A2UI: 28 | """ 29 | A2UI 消息构建器 30 | 31 | 使用链式 API 构建 A2UI JSONL 消息。 32 | 33 | 示例: 34 | ui = A2UI("my-surface") 35 | ui.text("title", "Hello World", "h1") 36 | ui.button("btn", "Click Me", "on_click") 37 | ui.column("main", ["title", "btn"]) 38 | print(ui.build("main")) 39 | """ 40 | 41 | def __init__(self, surface_id: str): 42 | self.surface_id = surface_id 43 | self.components: List[A2UIComponent] = [] 44 | self.data: Dict[str, Any] = {} 45 | 46 | # ========== 基础组件 ========== 47 | 48 | def text(self, id: str, content: str, hint: str = "body") -> 'A2UI': 49 | """ 50 | 添加文本组件 51 | 52 | Args: 53 | id: 组件唯一标识 54 | content: 文本内容 55 | hint: 用途提示 (h1, h2, h3, body, caption) 56 | """ 57 | self.components.append(A2UIComponent( 58 | id=id, 59 | type="Text", 60 | props={ 61 | "text": {"literalString": content}, 62 | "usageHint": hint 63 | } 64 | )) 65 | return self 66 | 67 | def text_binding(self, id: str, path: str, hint: str = "body") -> 'A2UI': 68 | """添加数据绑定的文本""" 69 | self.components.append(A2UIComponent( 70 | id=id, 71 | type="Text", 72 | props={ 73 | "text": {"path": path}, 74 | "usageHint": hint 75 | } 76 | )) 77 | return self 78 | 79 | def button(self, id: str, label: str, action: str, style: str = "filled") -> 'A2UI': 80 | """ 81 | 添加按钮组件 82 | 83 | Args: 84 | id: 组件唯一标识 85 | label: 按钮文字 86 | action: 点击时触发的动作名 87 | style: 按钮样式 (filled, outlined, text) 88 | """ 89 | label_id = f"{id}__label" 90 | self.text(label_id, label) 91 | self.components.append(A2UIComponent( 92 | id=id, 93 | type="Button", 94 | props={ 95 | "child": label_id, 96 | "action": {"name": action}, 97 | "style": style 98 | } 99 | )) 100 | return self 101 | 102 | def text_field(self, id: str, path: str, label: str = "", 103 | placeholder: str = "", multiline: bool = False) -> 'A2UI': 104 | """ 105 | 添加文本输入框 106 | 107 | Args: 108 | id: 组件唯一标识 109 | path: 数据绑定路径 (如 /user/name) 110 | label: 标签文字 111 | placeholder: 占位符 112 | multiline: 是否多行 113 | """ 114 | props = {"value": {"path": path}} 115 | if label: 116 | props["labelText"] = {"literalString": label} 117 | if placeholder: 118 | props["hintText"] = {"literalString": placeholder} 119 | if multiline: 120 | props["maxLines"] = 5 121 | 122 | self.components.append(A2UIComponent(id=id, type="TextField", props=props)) 123 | return self 124 | 125 | def checkbox(self, id: str, path: str, label: str) -> 'A2UI': 126 | """添加复选框""" 127 | label_id = f"{id}__label" 128 | self.text(label_id, label) 129 | self.components.append(A2UIComponent( 130 | id=id, 131 | type="Checkbox", 132 | props={ 133 | "value": {"path": path}, 134 | "label": label_id 135 | } 136 | )) 137 | return self 138 | 139 | def image(self, id: str, url: str, alt: str = "") -> 'A2UI': 140 | """添加图片""" 141 | props = {"source": {"literalString": url}} 142 | if alt: 143 | props["altText"] = {"literalString": alt} 144 | self.components.append(A2UIComponent(id=id, type="Image", props=props)) 145 | return self 146 | 147 | def divider(self, id: str) -> 'A2UI': 148 | """添加分隔线""" 149 | self.components.append(A2UIComponent(id=id, type="Divider", props={})) 150 | return self 151 | 152 | def spacer(self, id: str, size: int = 16) -> 'A2UI': 153 | """添加间距""" 154 | self.components.append(A2UIComponent( 155 | id=id, 156 | type="Spacer", 157 | props={"size": size} 158 | )) 159 | return self 160 | 161 | # ========== 布局组件 ========== 162 | 163 | def column(self, id: str, children: List[str], 164 | align: str = "start", gap: int = 8) -> 'A2UI': 165 | """ 166 | 垂直布局容器 167 | 168 | Args: 169 | id: 组件唯一标识 170 | children: 子组件 ID 列表 171 | align: 对齐方式 (start, center, end, stretch) 172 | gap: 子组件间距 173 | """ 174 | self.components.append(A2UIComponent( 175 | id=id, 176 | type="Column", 177 | props={ 178 | "children": children, 179 | "mainAxisAlignment": align, 180 | "spacing": gap 181 | } 182 | )) 183 | return self 184 | 185 | def row(self, id: str, children: List[str], 186 | align: str = "start", gap: int = 8) -> 'A2UI': 187 | """水平布局容器""" 188 | self.components.append(A2UIComponent( 189 | id=id, 190 | type="Row", 191 | props={ 192 | "children": children, 193 | "mainAxisAlignment": align, 194 | "spacing": gap 195 | } 196 | )) 197 | return self 198 | 199 | def card(self, id: str, child: str, elevation: int = 1) -> 'A2UI': 200 | """卡片容器""" 201 | self.components.append(A2UIComponent( 202 | id=id, 203 | type="Card", 204 | props={ 205 | "child": child, 206 | "elevation": elevation 207 | } 208 | )) 209 | return self 210 | 211 | def container(self, id: str, child: str, 212 | padding: int = 16, 213 | background: str = None) -> 'A2UI': 214 | """通用容器""" 215 | props = { 216 | "child": child, 217 | "padding": padding 218 | } 219 | if background: 220 | props["backgroundColor"] = background 221 | self.components.append(A2UIComponent(id=id, type="Container", props=props)) 222 | return self 223 | 224 | # ========== 数据 ========== 225 | 226 | def set_data(self, path: str, value: Any) -> 'A2UI': 227 | """ 228 | 设置数据模型 229 | 230 | Args: 231 | path: 数据路径 (如 "user" 或 "user.name") 232 | value: 数据值 (字符串、数字或字典) 233 | """ 234 | self.data[path] = value 235 | return self 236 | 237 | # ========== 生成 ========== 238 | 239 | def build(self, root_id: str) -> str: 240 | """ 241 | 生成 A2UI JSONL 242 | 243 | Args: 244 | root_id: 根组件 ID 245 | 246 | Returns: 247 | JSONL 格式的字符串,每行一个 JSON 对象 248 | """ 249 | lines = [] 250 | 251 | # 1. surfaceUpdate 252 | lines.append(json.dumps({ 253 | "surfaceUpdate": { 254 | "surfaceId": self.surface_id, 255 | "components": [c.to_dict() for c in self.components] 256 | } 257 | }, ensure_ascii=False)) 258 | 259 | # 2. dataModelUpdate (可选) 260 | if self.data: 261 | contents = [] 262 | for key, value in self.data.items(): 263 | if isinstance(value, dict): 264 | contents.append({ 265 | "key": key, 266 | "valueMap": [ 267 | {"key": k, "valueString": str(v)} 268 | for k, v in value.items() 269 | ] 270 | }) 271 | else: 272 | contents.append({"key": key, "valueString": str(value)}) 273 | 274 | lines.append(json.dumps({ 275 | "dataModelUpdate": { 276 | "surfaceId": self.surface_id, 277 | "contents": contents 278 | } 279 | }, ensure_ascii=False)) 280 | 281 | # 3. beginRendering 282 | lines.append(json.dumps({ 283 | "beginRendering": { 284 | "surfaceId": self.surface_id, 285 | "root": root_id 286 | } 287 | }, ensure_ascii=False)) 288 | 289 | return "\n".join(lines) 290 | 291 | def to_json(self, root_id: str) -> Dict: 292 | """返回结构化的 JSON 对象(用于 API 响应)""" 293 | return { 294 | "surface_id": self.surface_id, 295 | "components": [c.to_dict() for c in self.components], 296 | "data": self.data, 297 | "root": root_id 298 | } 299 | 300 | 301 | # ========== 便捷工厂函数 ========== 302 | 303 | def create_login_form() -> A2UI: 304 | """创建登录表单""" 305 | return (A2UI("login") 306 | .text("title", "用户登录", "h1") 307 | .text_field("username", "/user/username", "用户名", "请输入用户名") 308 | .text_field("password", "/user/password", "密码", "请输入密码") 309 | .button("submit", "登录", "submit_login") 310 | .column("form", ["title", "username", "password", "submit"], gap=16) 311 | .card("card", "form")) 312 | 313 | 314 | def create_restaurant_list(restaurants: List[Dict]) -> A2UI: 315 | """创建餐厅列表""" 316 | ui = A2UI("restaurants") 317 | ui.text("title", "🍱 附近的餐厅", "h1") 318 | 319 | cards = [] 320 | for i, r in enumerate(restaurants): 321 | prefix = f"r{i}" 322 | ui.text(f"{prefix}-name", r.get("name", ""), "h2") 323 | ui.text(f"{prefix}-info", f"¥{r.get('price', 0)} · ★{r.get('rating', 0)} · {r.get('distance', '')}") 324 | ui.button(f"{prefix}-view", "查看", f"view_{i}", "outlined") 325 | ui.button(f"{prefix}-book", "预订", f"book_{i}", "filled") 326 | ui.row(f"{prefix}-actions", [f"{prefix}-view", f"{prefix}-book"], gap=8) 327 | ui.column(f"{prefix}-content", [f"{prefix}-name", f"{prefix}-info", f"{prefix}-actions"], gap=8) 328 | ui.card(f"{prefix}-card", f"{prefix}-content") 329 | cards.append(f"{prefix}-card") 330 | 331 | ui.column("list", cards, gap=16) 332 | ui.column("main", ["title", "list"], gap=24) 333 | return ui 334 | 335 | 336 | def create_weather_card(city: str, temp: int, condition: str, humidity: int) -> A2UI: 337 | """创建天气卡片""" 338 | return (A2UI("weather") 339 | .text("title", f"🌤️ {city}天气", "h1") 340 | .text("temp", f"{temp}°C", "h2") 341 | .text("condition", condition) 342 | .text("humidity", f"湿度 {humidity}%") 343 | .button("refresh", "刷新", "refresh_weather", "outlined") 344 | .column("info", ["title", "temp", "condition", "humidity", "refresh"], gap=12) 345 | .card("card", "info")) 346 | 347 | 348 | # ========== 测试 ========== 349 | 350 | if __name__ == "__main__": 351 | # 测试餐厅列表 352 | restaurants = [ 353 | {"name": "意大利花园", "price": 180, "rating": 4.8, "distance": "500m"}, 354 | {"name": "牛排工坊", "price": 280, "rating": 4.5, "distance": "800m"}, 355 | {"name": "巴黎小馆", "price": 150, "rating": 4.9, "distance": "1.2km"}, 356 | ] 357 | 358 | ui = create_restaurant_list(restaurants) 359 | print("=== A2UI JSONL ===") 360 | print(ui.build("main")) 361 | -------------------------------------------------------------------------------- /day-09/day-09-session-rewind.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Day 9: ⏪ Undo Buttons for your Agents\n", 8 | "\n", 9 | "> Building an \"Edit Message\" or \"Regenerate\" feature shouldn't require complex database migrations. In the ADK, time travel is built-in.\n", 10 | "\n", 11 | "## 今日目标\n", 12 | "\n", 13 | "学习 ADK 的 **Session Rewind** 功能,实现:\n", 14 | "- 编辑消息 (Edit Message)\n", 15 | "- 重新生成 (Regenerate)\n", 16 | "- 撤销操作 (Undo)\n", 17 | "\n", 18 | "## 官方资源\n", 19 | "\n", 20 | "- [ADK Sessions Rewind](https://google.github.io/adk-docs/sessions/rewind/)\n", 21 | "- [ADK Runtime Resume](https://google.github.io/adk-docs/runtime/resume/)\n", 22 | "- [官方示例代码](https://github.com/google/adk-python/blob/main/contributing/samples/rewind_session/main.py)" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "## 1. 环境准备" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "import sys\n", 39 | "sys.path.insert(0, '..')\n", 40 | "\n", 41 | "from shared.config import load_env\n", 42 | "load_env()" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "from google.adk import Agent\n", 52 | "from google.adk.tools.tool_context import ToolContext\n", 53 | "from google.adk.agents.run_config import RunConfig\n", 54 | "from google.adk.runners import InMemoryRunner\n", 55 | "from google.genai import types" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "## 2. 定义工具函数\n", 63 | "\n", 64 | "我们创建一个可以管理状态和文件的 Agent,用来演示回溯功能。" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "async def update_state(tool_context: ToolContext, key: str, value: str) -> dict:\n", 74 | " \"\"\"更新状态值\"\"\"\n", 75 | " tool_context.state[key] = value\n", 76 | " return {\"status\": f\"已更新 '{key}' 为 '{value}'\"}\n", 77 | "\n", 78 | "\n", 79 | "async def load_state(tool_context: ToolContext, key: str) -> dict:\n", 80 | " \"\"\"读取状态值\"\"\"\n", 81 | " return {key: tool_context.state.get(key, \"未找到\")}\n", 82 | "\n", 83 | "\n", 84 | "async def save_artifact(tool_context: ToolContext, filename: str, content: str) -> dict:\n", 85 | " \"\"\"保存文件内容\"\"\"\n", 86 | " artifact_bytes = content.encode(\"utf-8\")\n", 87 | " artifact_part = types.Part(\n", 88 | " inline_data=types.Blob(mime_type=\"text/plain\", data=artifact_bytes)\n", 89 | " )\n", 90 | " version = await tool_context.save_artifact(filename, artifact_part)\n", 91 | " return {\"status\": \"成功\", \"filename\": filename, \"version\": version}\n", 92 | "\n", 93 | "\n", 94 | "async def load_artifact(tool_context: ToolContext, filename: str) -> dict:\n", 95 | " \"\"\"读取文件内容\"\"\"\n", 96 | " artifact = await tool_context.load_artifact(filename)\n", 97 | " if not artifact:\n", 98 | " return {\"error\": f\"文件 '{filename}' 未找到\"}\n", 99 | " content = artifact.inline_data.data.decode(\"utf-8\")\n", 100 | " return {\"filename\": filename, \"content\": content}" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "## 3. 创建 Agent" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": null, 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "state_agent = Agent(\n", 117 | " name=\"state_agent\",\n", 118 | " model=\"gemini-2.0-flash\",\n", 119 | " instruction=\"\"\"你是一个状态和文件管理 Agent。\n", 120 | "\n", 121 | "你可以:\n", 122 | "- 更新状态值 (update_state)\n", 123 | "- 读取状态值 (load_state)\n", 124 | "- 保存文件 (save_artifact)\n", 125 | "- 读取文件 (load_artifact)\n", 126 | "\n", 127 | "根据用户的请求使用相应的工具。用中文回复。\"\"\",\n", 128 | " tools=[\n", 129 | " update_state,\n", 130 | " load_state,\n", 131 | " save_artifact,\n", 132 | " load_artifact,\n", 133 | " ],\n", 134 | ")\n", 135 | "\n", 136 | "print(\"✅ Agent 创建成功\")" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "metadata": {}, 142 | "source": [ 143 | "## 4. 创建辅助函数" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [ 152 | "APP_NAME = \"rewind_demo\"\n", 153 | "USER_ID = \"demo_user\"\n", 154 | "\n", 155 | "\n", 156 | "async def call_agent(runner, session_id: str, prompt: str):\n", 157 | " \"\"\"调用 Agent 并打印结果\"\"\"\n", 158 | " print(f\"\\n👤 用户: {prompt}\")\n", 159 | " content = types.Content(\n", 160 | " role=\"user\", parts=[types.Part.from_text(text=prompt)]\n", 161 | " )\n", 162 | " events = []\n", 163 | " async for event in runner.run_async(\n", 164 | " user_id=USER_ID,\n", 165 | " session_id=session_id,\n", 166 | " new_message=content,\n", 167 | " run_config=RunConfig(),\n", 168 | " ):\n", 169 | " events.append(event)\n", 170 | " if event.content and event.author and event.author != \"user\":\n", 171 | " for part in event.content.parts:\n", 172 | " if part.text:\n", 173 | " print(f\" 🤖 Agent: {part.text}\")\n", 174 | " elif part.function_call:\n", 175 | " print(f\" 🛠️ 工具调用: {part.function_call.name}({part.function_call.args})\")\n", 176 | " elif part.function_response:\n", 177 | " print(f\" 📦 工具响应: {part.function_response.response}\")\n", 178 | " return events" 179 | ] 180 | }, 181 | { 182 | "cell_type": "markdown", 183 | "metadata": {}, 184 | "source": [ 185 | "## 5. Session Rewind 演示\n", 186 | "\n", 187 | "### 5.1 创建 Runner 和 Session" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "metadata": {}, 194 | "outputs": [], 195 | "source": [ 196 | "# 创建 Runner\n", 197 | "runner = InMemoryRunner(\n", 198 | " agent=state_agent,\n", 199 | " app_name=APP_NAME,\n", 200 | ")\n", 201 | "\n", 202 | "# 创建会话\n", 203 | "session = await runner.session_service.create_session(\n", 204 | " app_name=APP_NAME, user_id=USER_ID\n", 205 | ")\n", 206 | "print(f\"📝 创建会话: {session.id}\")" 207 | ] 208 | }, 209 | { 210 | "cell_type": "markdown", 211 | "metadata": {}, 212 | "source": [ 213 | "### 5.2 步骤 1: 初始化状态" 214 | ] 215 | }, 216 | { 217 | "cell_type": "code", 218 | "execution_count": null, 219 | "metadata": {}, 220 | "outputs": [], 221 | "source": [ 222 | "# 设置初始状态\n", 223 | "await call_agent(runner, session.id, \"设置状态 color 为 red\")" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": null, 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "# 保存初始文件\n", 233 | "await call_agent(runner, session.id, \"保存文件 note.txt 内容为 version1\")" 234 | ] 235 | }, 236 | { 237 | "cell_type": "markdown", 238 | "metadata": {}, 239 | "source": [ 240 | "### 5.3 步骤 2: 确认当前状态" 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": null, 246 | "metadata": {}, 247 | "outputs": [], 248 | "source": [ 249 | "await call_agent(runner, session.id, \"查询状态 color 的值\")" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": null, 255 | "metadata": {}, 256 | "outputs": [], 257 | "source": [ 258 | "await call_agent(runner, session.id, \"读取文件 note.txt\")" 259 | ] 260 | }, 261 | { 262 | "cell_type": "markdown", 263 | "metadata": {}, 264 | "source": [ 265 | "### 5.4 步骤 3: 修改状态 ⚠️ 这是回溯点" 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": null, 271 | "metadata": {}, 272 | "outputs": [], 273 | "source": [ 274 | "# 这次修改是我们后面要撤销的\n", 275 | "events_update = await call_agent(runner, session.id, \"更新状态 color 为 blue\")\n", 276 | "\n", 277 | "# 记录这个操作的 invocation_id,用于后续回溯\n", 278 | "rewind_invocation_id = events_update[0].invocation_id\n", 279 | "print(f\"\\n📌 记录回溯点 invocation_id: {rewind_invocation_id}\")" 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": null, 285 | "metadata": {}, 286 | "outputs": [], 287 | "source": [ 288 | "# 再保存新版本的文件\n", 289 | "await call_agent(runner, session.id, \"保存文件 note.txt 内容为 version2\")" 290 | ] 291 | }, 292 | { 293 | "cell_type": "markdown", 294 | "metadata": {}, 295 | "source": [ 296 | "### 5.5 步骤 4: 确认修改后的状态" 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": null, 302 | "metadata": {}, 303 | "outputs": [], 304 | "source": [ 305 | "await call_agent(runner, session.id, \"查询状态 color 的值\")" 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": null, 311 | "metadata": {}, 312 | "outputs": [], 313 | "source": [ 314 | "await call_agent(runner, session.id, \"读取文件 note.txt\")" 315 | ] 316 | }, 317 | { 318 | "cell_type": "markdown", 319 | "metadata": {}, 320 | "source": [ 321 | "### 5.6 步骤 5: ⏪ 执行 REWIND!\n", 322 | "\n", 323 | "现在我们要回溯到 \"color = blue\" 那次修改之前的状态。" 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": null, 329 | "metadata": {}, 330 | "outputs": [], 331 | "source": [ 332 | "print(f\"⏪ 回溯到 invocation_id: {rewind_invocation_id} 之前...\")\n", 333 | "\n", 334 | "await runner.rewind_async(\n", 335 | " user_id=USER_ID,\n", 336 | " session_id=session.id,\n", 337 | " rewind_before_invocation_id=rewind_invocation_id,\n", 338 | ")\n", 339 | "\n", 340 | "print(\"✅ 回溯完成!\")" 341 | ] 342 | }, 343 | { 344 | "cell_type": "markdown", 345 | "metadata": {}, 346 | "source": [ 347 | "### 5.7 步骤 6: 验证回溯后的状态\n", 348 | "\n", 349 | "回溯后,状态应该恢复到:\n", 350 | "- color = **red** (而不是 blue)\n", 351 | "- note.txt = **version1** (而不是 version2)" 352 | ] 353 | }, 354 | { 355 | "cell_type": "code", 356 | "execution_count": null, 357 | "metadata": {}, 358 | "outputs": [], 359 | "source": [ 360 | "await call_agent(runner, session.id, \"查询状态 color 的值\")" 361 | ] 362 | }, 363 | { 364 | "cell_type": "code", 365 | "execution_count": null, 366 | "metadata": {}, 367 | "outputs": [], 368 | "source": [ 369 | "await call_agent(runner, session.id, \"读取文件 note.txt\")" 370 | ] 371 | }, 372 | { 373 | "cell_type": "markdown", 374 | "metadata": {}, 375 | "source": [ 376 | "## 6. 总结\n", 377 | "\n", 378 | "### 🔑 核心要点\n", 379 | "\n", 380 | "1. **`runner.rewind_async()`** - 回溯会话到指定的 invocation 之前\n", 381 | "2. **`rewind_before_invocation_id`** - 指定回溯点的 ID\n", 382 | "3. **完整恢复** - 回溯会同时恢复 state(状态)和 artifacts(文件)\n", 383 | "\n", 384 | "### 💡 应用场景\n", 385 | "\n", 386 | "| 功能 | 实现方式 |\n", 387 | "|------|----------|\n", 388 | "| 编辑消息 | 回溯到该消息之前,然后发送新消息 |\n", 389 | "| 重新生成 | 回溯到上一个用户消息之前,然后重新运行 |\n", 390 | "| 撤销操作 | 回溯到操作之前 |\n", 391 | "| 分支对话 | 回溯到某个点,然后走不同的路径 |\n", 392 | "\n", 393 | "### 📚 参考链接\n", 394 | "\n", 395 | "- [ADK Sessions Rewind 文档](https://google.github.io/adk-docs/sessions/rewind/)\n", 396 | "- [ADK Runtime Resume 文档](https://google.github.io/adk-docs/runtime/resume/)\n", 397 | "- [官方示例代码](https://github.com/google/adk-python/blob/main/contributing/samples/rewind_session/main.py)" 398 | ] 399 | } 400 | ], 401 | "metadata": { 402 | "kernelspec": { 403 | "display_name": "Python 3", 404 | "language": "python", 405 | "name": "python3" 406 | }, 407 | "language_info": { 408 | "name": "python", 409 | "version": "3.11.0" 410 | } 411 | }, 412 | "nbformat": 4, 413 | "nbformat_minor": 4 414 | } 415 | -------------------------------------------------------------------------------- /day-05/telemetry_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Day 5: Production Observability Demo 3 | 4 | 演示如何在 Agent 中集成 OpenTelemetry 进行可观测性监控。 5 | 这是一个简化版本,展示核心概念。 6 | 7 | 实际生产中请使用 Agent Starter Pack 的完整 telemetry 模块。 8 | """ 9 | 10 | import os 11 | import time 12 | import json 13 | import logging 14 | from datetime import datetime 15 | from typing import Optional 16 | from dataclasses import dataclass, field, asdict 17 | 18 | # 配置日志 19 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | # ============================================================ 24 | # Span 数据结构 25 | # ============================================================ 26 | 27 | @dataclass 28 | class Span: 29 | """表示一个追踪 Span""" 30 | name: str 31 | trace_id: str 32 | span_id: str 33 | parent_id: Optional[str] = None 34 | start_time: float = field(default_factory=time.time) 35 | end_time: Optional[float] = None 36 | attributes: dict = field(default_factory=dict) 37 | status: str = "OK" 38 | 39 | def end(self): 40 | self.end_time = time.time() 41 | 42 | @property 43 | def duration_ms(self) -> float: 44 | if self.end_time: 45 | return (self.end_time - self.start_time) * 1000 46 | return 0 47 | 48 | def set_attribute(self, key: str, value): 49 | self.attributes[key] = value 50 | 51 | def to_dict(self) -> dict: 52 | return { 53 | "name": self.name, 54 | "trace_id": self.trace_id, 55 | "span_id": self.span_id, 56 | "parent_id": self.parent_id, 57 | "start_time": datetime.fromtimestamp(self.start_time).isoformat(), 58 | "end_time": datetime.fromtimestamp(self.end_time).isoformat() if self.end_time else None, 59 | "duration_ms": self.duration_ms, 60 | "attributes": self.attributes, 61 | "status": self.status 62 | } 63 | 64 | 65 | # ============================================================ 66 | # 简化版 Tracer 67 | # ============================================================ 68 | 69 | class SimpleTracer: 70 | """简化版追踪器,用于演示概念""" 71 | 72 | def __init__(self, service_name: str): 73 | self.service_name = service_name 74 | self.spans: list[Span] = [] 75 | self._current_span: Optional[Span] = None 76 | self._trace_id = self._generate_id() 77 | 78 | def _generate_id(self) -> str: 79 | import random 80 | return hex(random.getrandbits(64))[2:] 81 | 82 | def start_span(self, name: str) -> Span: 83 | """开始一个新的 Span""" 84 | span = Span( 85 | name=name, 86 | trace_id=self._trace_id, 87 | span_id=self._generate_id(), 88 | parent_id=self._current_span.span_id if self._current_span else None 89 | ) 90 | span.set_attribute("service.name", self.service_name) 91 | self.spans.append(span) 92 | self._current_span = span 93 | return span 94 | 95 | def end_span(self, span: Span): 96 | """结束一个 Span""" 97 | span.end() 98 | # 恢复到父 Span 99 | if span.parent_id: 100 | for s in reversed(self.spans): 101 | if s.span_id == span.parent_id: 102 | self._current_span = s 103 | break 104 | else: 105 | self._current_span = None 106 | 107 | def get_trace_summary(self) -> dict: 108 | """获取追踪摘要""" 109 | return { 110 | "trace_id": self._trace_id, 111 | "service": self.service_name, 112 | "total_spans": len(self.spans), 113 | "spans": [s.to_dict() for s in self.spans] 114 | } 115 | 116 | 117 | # ============================================================ 118 | # GenAI Telemetry 记录器 119 | # ============================================================ 120 | 121 | @dataclass 122 | class GenAITelemetryRecord: 123 | """GenAI 调用遥测记录""" 124 | timestamp: str 125 | model: str 126 | input_tokens: int 127 | output_tokens: int 128 | latency_ms: float 129 | status: str = "success" 130 | # NO_CONTENT 模式下不记录以下字段 131 | # prompt: str = "" 132 | # response: str = "" 133 | 134 | 135 | class GenAITelemetryLogger: 136 | """GenAI 遥测记录器(模拟 GCS 上传)""" 137 | 138 | def __init__(self, bucket_name: Optional[str] = None, capture_content: str = "NO_CONTENT"): 139 | self.bucket_name = bucket_name 140 | self.capture_content = capture_content 141 | self.records: list[GenAITelemetryRecord] = [] 142 | 143 | if bucket_name: 144 | logger.info(f"GenAI Telemetry enabled - bucket: {bucket_name}, mode: {capture_content}") 145 | else: 146 | logger.info("GenAI Telemetry disabled (no bucket configured)") 147 | 148 | def log_completion(self, model: str, input_tokens: int, output_tokens: int, 149 | latency_ms: float, status: str = "success"): 150 | """记录一次 LLM 调用""" 151 | record = GenAITelemetryRecord( 152 | timestamp=datetime.utcnow().isoformat() + "Z", 153 | model=model, 154 | input_tokens=input_tokens, 155 | output_tokens=output_tokens, 156 | latency_ms=latency_ms, 157 | status=status 158 | ) 159 | self.records.append(record) 160 | 161 | # 模拟上传到 GCS(实际中使用 JSONL 格式) 162 | if self.bucket_name: 163 | logger.info(f"[Telemetry] {model}: {input_tokens}+{output_tokens} tokens, {latency_ms:.0f}ms") 164 | 165 | def export_jsonl(self) -> str: 166 | """导出为 JSONL 格式""" 167 | lines = [] 168 | for record in self.records: 169 | lines.append(json.dumps(asdict(record))) 170 | return "\n".join(lines) 171 | 172 | def get_summary(self) -> dict: 173 | """获取统计摘要""" 174 | if not self.records: 175 | return {"total_calls": 0} 176 | 177 | total_input = sum(r.input_tokens for r in self.records) 178 | total_output = sum(r.output_tokens for r in self.records) 179 | avg_latency = sum(r.latency_ms for r in self.records) / len(self.records) 180 | 181 | return { 182 | "total_calls": len(self.records), 183 | "total_input_tokens": total_input, 184 | "total_output_tokens": total_output, 185 | "total_tokens": total_input + total_output, 186 | "avg_latency_ms": round(avg_latency, 2) 187 | } 188 | 189 | 190 | # ============================================================ 191 | # 模拟 Agent 执行 192 | # ============================================================ 193 | 194 | class MockLLM: 195 | """模拟 LLM 调用""" 196 | 197 | def __init__(self, model: str = "gemini-2.0-flash"): 198 | self.model = model 199 | 200 | def generate(self, prompt: str) -> dict: 201 | """模拟生成响应""" 202 | # 模拟延迟 203 | time.sleep(0.1 + len(prompt) * 0.001) 204 | 205 | return { 206 | "text": f"这是对 '{prompt[:20]}...' 的回复", 207 | "usage": { 208 | "input_tokens": len(prompt) * 2, 209 | "output_tokens": 50 + len(prompt) 210 | } 211 | } 212 | 213 | 214 | def simulate_agent_request(tracer: SimpleTracer, telemetry: GenAITelemetryLogger, 215 | llm: MockLLM, user_input: str): 216 | """模拟一次 Agent 请求处理""" 217 | 218 | # 主 Span 219 | main_span = tracer.start_span("agent.handle_request") 220 | main_span.set_attribute("user_input_length", len(user_input)) 221 | 222 | try: 223 | # 解析输入 224 | parse_span = tracer.start_span("agent.parse_input") 225 | time.sleep(0.02) # 模拟解析 226 | tracer.end_span(parse_span) 227 | 228 | # 第一次 LLM 调用(理解意图) 229 | llm_span_1 = tracer.start_span("llm.generate") 230 | llm_span_1.set_attribute("model", llm.model) 231 | llm_span_1.set_attribute("purpose", "intent_understanding") 232 | 233 | start = time.time() 234 | response1 = llm.generate(f"理解用户意图: {user_input}") 235 | latency1 = (time.time() - start) * 1000 236 | 237 | llm_span_1.set_attribute("input_tokens", response1["usage"]["input_tokens"]) 238 | llm_span_1.set_attribute("output_tokens", response1["usage"]["output_tokens"]) 239 | tracer.end_span(llm_span_1) 240 | 241 | telemetry.log_completion( 242 | model=llm.model, 243 | input_tokens=response1["usage"]["input_tokens"], 244 | output_tokens=response1["usage"]["output_tokens"], 245 | latency_ms=latency1 246 | ) 247 | 248 | # 工具执行 249 | tool_span = tracer.start_span("tool.execute") 250 | tool_span.set_attribute("tool_name", "search_database") 251 | time.sleep(0.05) # 模拟工具执行 252 | tracer.end_span(tool_span) 253 | 254 | # 第二次 LLM 调用(生成回复) 255 | llm_span_2 = tracer.start_span("llm.generate") 256 | llm_span_2.set_attribute("model", llm.model) 257 | llm_span_2.set_attribute("purpose", "response_generation") 258 | 259 | start = time.time() 260 | response2 = llm.generate(f"基于搜索结果回复: {user_input}") 261 | latency2 = (time.time() - start) * 1000 262 | 263 | llm_span_2.set_attribute("input_tokens", response2["usage"]["input_tokens"]) 264 | llm_span_2.set_attribute("output_tokens", response2["usage"]["output_tokens"]) 265 | tracer.end_span(llm_span_2) 266 | 267 | telemetry.log_completion( 268 | model=llm.model, 269 | input_tokens=response2["usage"]["input_tokens"], 270 | output_tokens=response2["usage"]["output_tokens"], 271 | latency_ms=latency2 272 | ) 273 | 274 | # 格式化响应 275 | format_span = tracer.start_span("agent.format_response") 276 | time.sleep(0.01) 277 | tracer.end_span(format_span) 278 | 279 | main_span.set_attribute("status", "success") 280 | 281 | except Exception as e: 282 | main_span.set_attribute("status", "error") 283 | main_span.set_attribute("error.message", str(e)) 284 | main_span.status = "ERROR" 285 | 286 | finally: 287 | tracer.end_span(main_span) 288 | 289 | 290 | # ============================================================ 291 | # 演示 292 | # ============================================================ 293 | 294 | def main(): 295 | print("=" * 60) 296 | print("Day 5: Production Observability Demo") 297 | print("=" * 60) 298 | print() 299 | 300 | # 初始化组件 301 | tracer = SimpleTracer(service_name="my-agent") 302 | telemetry = GenAITelemetryLogger( 303 | bucket_name="gs://demo-bucket/logs", # 模拟 bucket 304 | capture_content="NO_CONTENT" 305 | ) 306 | llm = MockLLM(model="gemini-2.0-flash") 307 | 308 | # 模拟多个请求 309 | requests = [ 310 | "北京今天天气怎么样?", 311 | "帮我找一家附近评分高的西餐厅", 312 | "解释一下什么是 OpenTelemetry" 313 | ] 314 | 315 | print("\n📨 处理用户请求...") 316 | print("-" * 60) 317 | 318 | for req in requests: 319 | print(f"\n用户: {req}") 320 | simulate_agent_request(tracer, telemetry, llm, req) 321 | 322 | # 输出追踪结果 323 | print("\n" + "=" * 60) 324 | print("📊 Trace Summary (类似 Cloud Trace)") 325 | print("=" * 60) 326 | 327 | trace_data = tracer.get_trace_summary() 328 | print(f"\nTrace ID: {trace_data['trace_id']}") 329 | print(f"Service: {trace_data['service']}") 330 | print(f"Total Spans: {trace_data['total_spans']}") 331 | 332 | print("\nSpans:") 333 | for span in trace_data['spans']: 334 | indent = " " if span['parent_id'] else "" 335 | print(f"{indent}├── {span['name']}: {span['duration_ms']:.1f}ms") 336 | for key, value in span['attributes'].items(): 337 | if key not in ['service.name']: 338 | print(f"{indent}│ └── {key}: {value}") 339 | 340 | # 输出遥测摘要 341 | print("\n" + "=" * 60) 342 | print("📈 GenAI Telemetry Summary (类似 BigQuery 查询结果)") 343 | print("=" * 60) 344 | 345 | summary = telemetry.get_summary() 346 | print(f""" 347 | Total LLM Calls: {summary['total_calls']} 348 | Total Tokens: {summary['total_tokens']} 349 | - Input: {summary['total_input_tokens']} 350 | - Output: {summary['total_output_tokens']} 351 | Avg Latency: {summary['avg_latency_ms']}ms 352 | """) 353 | 354 | # 输出 JSONL 格式 355 | print("=" * 60) 356 | print("📄 JSONL Export (类似 GCS 日志文件)") 357 | print("=" * 60) 358 | print() 359 | print(telemetry.export_jsonl()) 360 | print() 361 | 362 | print("=" * 60) 363 | print("✅ Demo 完成") 364 | print("=" * 60) 365 | print(""" 366 | 实际生产中: 367 | - Traces 导出到 Cloud Trace,可视化查看调用链 368 | - JSONL 上传到 GCS,通过 BigQuery 外部表查询 369 | - 可以按 model、时间、user 等维度分析 Token 消耗和延迟 370 | """) 371 | 372 | 373 | if __name__ == "__main__": 374 | main() 375 | -------------------------------------------------------------------------------- /day-17/gemini3_flash_thinking.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Day 17: Gemini 3 Flash - 可配置思考级别\n", 8 | "\n", 9 | "Gemini 3 Flash 最大亮点:**Thinking Level(思考级别)**\n", 10 | "\n", 11 | "简单说:控制模型\"想多久\"再回答。" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "## 1. 思考级别一览\n", 19 | "\n", 20 | "| 级别 | 特点 | 适用场景 |\n", 21 | "|------|------|----------|\n", 22 | "| `MINIMAL` | 最快响应 | 简单问答、聊天 |\n", 23 | "| `LOW` | 快速 + 少量推理 | 日常任务 |\n", 24 | "| `MEDIUM` | 平衡模式 | 中等复杂度 |\n", 25 | "| `HIGH` | 深度推理 | 数学、编程、复杂问题 |" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "## 2. 环境准备" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 1, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "import os\n", 42 | "from dotenv import load_dotenv\n", 43 | "\n", 44 | "load_dotenv()\n", 45 | "\n", 46 | "# 确认 API Key 已设置\n", 47 | "assert os.getenv(\"GOOGLE_API_KEY\"), \"请设置 GOOGLE_API_KEY\"" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 2, 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "from google.adk.agents import Agent\n", 57 | "from google.adk.planners import BuiltInPlanner\n", 58 | "from google.adk.runners import Runner\n", 59 | "from google.adk.sessions import InMemorySessionService\n", 60 | "from google.genai import types\n", 61 | "from google.genai.types import Content, Part" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "## 3. 核心配置:ThinkingConfig\n", 69 | "\n", 70 | "关键代码就这几行:" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": 3, 76 | "metadata": {}, 77 | "outputs": [ 78 | { 79 | "name": "stdout", 80 | "output_type": "stream", 81 | "text": [ 82 | "思考级别: ThinkingLevel.LOW\n" 83 | ] 84 | } 85 | ], 86 | "source": [ 87 | "# 配置思考级别\n", 88 | "thinking_config = types.ThinkingConfig(\n", 89 | " thinking_level=\"LOW\" # 可选: MINIMAL, LOW, MEDIUM, HIGH\n", 90 | ")\n", 91 | "\n", 92 | "# 创建规划器\n", 93 | "planner = BuiltInPlanner(thinking_config=thinking_config)\n", 94 | "\n", 95 | "print(f\"思考级别: {thinking_config.thinking_level}\")" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "## 4. 定义工具" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": 4, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "def get_current_time(city: str) -> dict:\n", 112 | " \"\"\"获取指定城市的当前时间\"\"\"\n", 113 | " times = {\n", 114 | " \"北京\": \"22:30\",\n", 115 | " \"东京\": \"23:30\", \n", 116 | " \"纽约\": \"09:30\",\n", 117 | " \"伦敦\": \"14:30\",\n", 118 | " }\n", 119 | " time = times.get(city, \"10:00\")\n", 120 | " return {\"city\": city, \"time\": time}\n", 121 | "\n", 122 | "\n", 123 | "def calculate(expression: str) -> dict:\n", 124 | " \"\"\"计算数学表达式\"\"\"\n", 125 | " try:\n", 126 | " result = eval(expression)\n", 127 | " return {\"expression\": expression, \"result\": result}\n", 128 | " except Exception as e:\n", 129 | " return {\"error\": str(e)}" 130 | ] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "metadata": {}, 135 | "source": [ 136 | "## 5. 创建 Agent 工厂函数" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 5, 142 | "metadata": {}, 143 | "outputs": [ 144 | { 145 | "name": "stdout", 146 | "output_type": "stream", 147 | "text": [ 148 | "✅ 工厂函数已定义\n" 149 | ] 150 | } 151 | ], 152 | "source": [ 153 | "def create_agent(thinking_level: str) -> Agent:\n", 154 | " \"\"\"创建指定思考级别的 Agent\"\"\"\n", 155 | " return Agent(\n", 156 | " model='gemini-3-flash-preview',\n", 157 | " name=f'agent_{thinking_level.lower()}',\n", 158 | " instruction=\"你是一个有用的助手。请用中文简洁回答。\",\n", 159 | " tools=[get_current_time, calculate],\n", 160 | " planner=BuiltInPlanner(\n", 161 | " thinking_config=types.ThinkingConfig(\n", 162 | " thinking_level=thinking_level\n", 163 | " )\n", 164 | " ),\n", 165 | " )\n", 166 | "\n", 167 | "print(\"✅ 工厂函数已定义\")" 168 | ] 169 | }, 170 | { 171 | "cell_type": "markdown", 172 | "metadata": {}, 173 | "source": [ 174 | "## 6. 运行辅助函数(含思考过程显示)\n", 175 | "\n", 176 | "关键:通过 `event.content.parts` 中的 `thought` 属性获取思考内容" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 6, 182 | "metadata": {}, 183 | "outputs": [ 184 | { 185 | "name": "stdout", 186 | "output_type": "stream", 187 | "text": [ 188 | "✅ 辅助函数已定义(支持显示思考过程)\n" 189 | ] 190 | } 191 | ], 192 | "source": [ 193 | "session_service = InMemorySessionService()\n", 194 | "\n", 195 | "async def ask(agent: Agent, question: str, show_thinking: bool = True) -> str:\n", 196 | " \"\"\"\n", 197 | " 向 Agent 提问并返回回答\n", 198 | " \n", 199 | " 参数:\n", 200 | " show_thinking: 是否显示思考过程\n", 201 | " \"\"\"\n", 202 | " session = await session_service.create_session(\n", 203 | " app_name=\"day17_demo\",\n", 204 | " user_id=\"demo_user\"\n", 205 | " )\n", 206 | " \n", 207 | " runner = Runner(\n", 208 | " agent=agent,\n", 209 | " app_name=\"day17_demo\", \n", 210 | " session_service=session_service\n", 211 | " )\n", 212 | " \n", 213 | " user_content = Content(role=\"user\", parts=[Part(text=question)])\n", 214 | " \n", 215 | " response = \"\"\n", 216 | " thinking = \"\"\n", 217 | " \n", 218 | " async for event in runner.run_async(\n", 219 | " user_id=\"demo_user\",\n", 220 | " session_id=session.id,\n", 221 | " new_message=user_content\n", 222 | " ):\n", 223 | " if hasattr(event, 'content') and event.content:\n", 224 | " if hasattr(event.content, 'parts'):\n", 225 | " for part in event.content.parts:\n", 226 | " # 获取思考过程\n", 227 | " if hasattr(part, 'thought') and part.thought:\n", 228 | " thinking += part.text if hasattr(part, 'text') else \"\"\n", 229 | " # 获取最终回答\n", 230 | " elif hasattr(part, 'text') and part.text:\n", 231 | " response += part.text\n", 232 | " \n", 233 | " # 显示思考过程\n", 234 | " if show_thinking and thinking:\n", 235 | " print(f\"💭 思考过程:\\n{thinking}\\n\")\n", 236 | " print(\"-\" * 40)\n", 237 | " \n", 238 | " return response\n", 239 | "\n", 240 | "print(\"✅ 辅助函数已定义(支持显示思考过程)\")" 241 | ] 242 | }, 243 | { 244 | "cell_type": "markdown", 245 | "metadata": {}, 246 | "source": [ 247 | "## 7. 测试:LOW 级别(日常使用推荐)" 248 | ] 249 | }, 250 | { 251 | "cell_type": "code", 252 | "execution_count": 7, 253 | "metadata": {}, 254 | "outputs": [ 255 | { 256 | "name": "stderr", 257 | "output_type": "stream", 258 | "text": [ 259 | "Warning: there are non-text parts in the response: ['function_call'], returning concatenated text result from text parts. Check the full candidates.content.parts accessor to get the full model response.\n" 260 | ] 261 | }, 262 | { 263 | "name": "stdout", 264 | "output_type": "stream", 265 | "text": [ 266 | "🤖 [LOW] 回答: 现在北京时间是 22:30。\n" 267 | ] 268 | } 269 | ], 270 | "source": [ 271 | "agent_low = create_agent(\"LOW\")\n", 272 | "\n", 273 | "response = await ask(agent_low, \"北京现在几点?\")\n", 274 | "print(f\"🤖 [LOW] 回答: {response}\")" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "metadata": {}, 280 | "source": [ 281 | "## 8. 测试:HIGH 级别(复杂推理)" 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": 8, 287 | "metadata": {}, 288 | "outputs": [ 289 | { 290 | "name": "stdout", 291 | "output_type": "stream", 292 | "text": [ 293 | "🤖 [HIGH] 回答: 北京晚上10点(22:00),纽约是当日**上午10点**(目前处于夏令时)。如果是冬令时,则为上午9点。\n", 294 | "\n", 295 | "**时差原因:**\n", 296 | "1. **地球自转**:地球自西向东自转,不同经度的地区看到日出的时间不同。\n", 297 | "2. **时区划分**:为统一时间,全球分为24个时区。北京位于**东八区 (UTC+8)**,纽约位于**西五区 (UTC-5)**。\n", 298 | "3. **夏令时**:纽约在夏季会进入夏令时,将时钟拨快一小时(变为UTC-4),因此两地目前的时差为12小时。\n" 299 | ] 300 | } 301 | ], 302 | "source": [ 303 | "agent_high = create_agent(\"HIGH\")\n", 304 | "\n", 305 | "response = await ask(agent_high, \"如果北京是晚上10点,纽约是什么时候?解释时差原因。\")\n", 306 | "print(f\"🤖 [HIGH] 回答: {response}\")" 307 | ] 308 | }, 309 | { 310 | "cell_type": "markdown", 311 | "metadata": {}, 312 | "source": [ 313 | "## 9. 测试:数学计算" 314 | ] 315 | }, 316 | { 317 | "cell_type": "code", 318 | "execution_count": 9, 319 | "metadata": {}, 320 | "outputs": [ 321 | { 322 | "name": "stdout", 323 | "output_type": "stream", 324 | "text": [ 325 | "🤖 [LOW] 回答: 结果是 126。\n" 326 | ] 327 | } 328 | ], 329 | "source": [ 330 | "response = await ask(agent_low, \"计算 (15 * 8) + (42 / 7)\")\n", 331 | "print(f\"🤖 [LOW] 回答: {response}\")" 332 | ] 333 | }, 334 | { 335 | "cell_type": "markdown", 336 | "metadata": {}, 337 | "source": [ 338 | "## 10. 查看思考过程对比\n", 339 | "\n", 340 | "HIGH 级别会有更详细的思考过程:" 341 | ] 342 | }, 343 | { 344 | "cell_type": "code", 345 | "execution_count": 10, 346 | "metadata": {}, 347 | "outputs": [ 348 | { 349 | "name": "stdout", 350 | "output_type": "stream", 351 | "text": [ 352 | "🔵 LOW 级别思考:\n", 353 | "🤖 回答: 1000 除以 7 约等于 142.86。\n" 354 | ] 355 | } 356 | ], 357 | "source": [ 358 | "print(\"🔵 LOW 级别思考:\")\n", 359 | "agent_low = create_agent(\"LOW\")\n", 360 | "response = await ask(agent_low, \"1000 除以 7 等于多少?保留两位小数。\", show_thinking=True)\n", 361 | "print(f\"🤖 回答: {response}\")" 362 | ] 363 | }, 364 | { 365 | "cell_type": "code", 366 | "execution_count": 11, 367 | "metadata": {}, 368 | "outputs": [ 369 | { 370 | "name": "stdout", 371 | "output_type": "stream", 372 | "text": [ 373 | "🔴 HIGH 级别思考:\n", 374 | "🤖 回答: 1000 除以 7 等于 142.86。\n" 375 | ] 376 | } 377 | ], 378 | "source": [ 379 | "print(\"🔴 HIGH 级别思考:\")\n", 380 | "agent_high = create_agent(\"HIGH\")\n", 381 | "response = await ask(agent_high, \"1000 除以 7 等于多少?保留两位小数。\", show_thinking=True)\n", 382 | "print(f\"🤖 回答: {response}\")" 383 | ] 384 | }, 385 | { 386 | "cell_type": "markdown", 387 | "metadata": {}, 388 | "source": [ 389 | "## 11. 总结\n", 390 | "\n", 391 | "### 选择建议\n", 392 | "\n", 393 | "```\n", 394 | "简单聊天 → MINIMAL(最省钱最快)\n", 395 | "日常任务 → LOW(推荐默认)\n", 396 | "分析任务 → MEDIUM\n", 397 | "复杂推理 → HIGH(最准但最慢)\n", 398 | "```\n", 399 | "\n", 400 | "### 思考过程显示\n", 401 | "\n", 402 | "```python\n", 403 | "# part.thought = True 表示这是思考内容\n", 404 | "for part in event.content.parts:\n", 405 | " if hasattr(part, 'thought') and part.thought:\n", 406 | " print(\"思考:\", part.text)\n", 407 | "```\n", 408 | "\n", 409 | "### 与 Gemini 2.5 的区别\n", 410 | "\n", 411 | "| 版本 | 参数 | 方式 |\n", 412 | "|------|------|------|\n", 413 | "| Gemini 3 | `thinking_level` | 级别选择 |\n", 414 | "| Gemini 2.5 | `thinking_budget` | token 预算 |\n", 415 | "\n", 416 | "**注意:参数不能混用!**" 417 | ] 418 | }, 419 | { 420 | "cell_type": "markdown", 421 | "metadata": {}, 422 | "source": [ 423 | "## 参考资料\n", 424 | "\n", 425 | "- [Gemini 3 Flash 官方公告](https://blog.google/products/gemini/gemini-3-flash/)\n", 426 | "- [ADK 模型配置文档](https://google.github.io/adk-docs/agents/models/)\n", 427 | "- [TechCrunch 报道](https://techcrunch.com/2025/12/17/google-launches-gemini-3-flash-makes-it-the-default-model-in-the-gemini-app/)" 428 | ] 429 | } 430 | ], 431 | "metadata": { 432 | "kernelspec": { 433 | "display_name": ".venv", 434 | "language": "python", 435 | "name": "python3" 436 | }, 437 | "language_info": { 438 | "codemirror_mode": { 439 | "name": "ipython", 440 | "version": 3 441 | }, 442 | "file_extension": ".py", 443 | "mimetype": "text/x-python", 444 | "name": "python", 445 | "nbconvert_exporter": "python", 446 | "pygments_lexer": "ipython3", 447 | "version": "3.12.11" 448 | } 449 | }, 450 | "nbformat": 4, 451 | "nbformat_minor": 4 452 | } 453 | -------------------------------------------------------------------------------- /day-14/README.md: -------------------------------------------------------------------------------- 1 | # Day 14: 使用 A2A 协议连接 Agents 2 | 3 | ## 概述 4 | 5 | 今天我们学习 **Agent2Agent (A2A)** 协议 - 一个开放标准,允许不同的 AI Agents 之间进行通信和协作,无论它们使用什么框架或语言。 6 | 7 | ## A2A 协议核心概念 8 | 9 | ### 什么是 A2A? 10 | 11 | A2A (Agent-to-Agent) 是 Google 推出的开放协议,解决了一个关键问题:**如何让不同团队、不同框架构建的 Agents 互相协作?** 12 | 13 | ``` 14 | ┌─────────────────┐ A2A 协议 ┌─────────────────┐ 15 | │ Host Agent │ ◄──────────────────────► │ Remote Agent │ 16 | │ (客户端) │ JSON-RPC over HTTP │ (服务端) │ 17 | │ ADK/LangChain │ │ ADK/其他框架 │ 18 | └─────────────────┘ └─────────────────┘ 19 | ``` 20 | 21 | ### A2A 的核心组件 22 | 23 | 1. **Agent Card** - Agent 的"名片" 24 | - 描述 Agent 的能力、技能 25 | - 提供 RPC URL 端点 26 | - 类似于 API 文档 27 | - 可通过 `/.well-known/agent-card.json` 访问 28 | 29 | 2. **Task** - 任务单元 30 | - 客户端向服务端发送的工作请求 31 | - 包含 message (消息) 和 context (上下文) 32 | - 有状态流转:submitted → working → completed/failed 33 | 34 | 3. **Message** - 消息 35 | - 包含多个 Parts (文本、文件、数据等) 36 | - 支持角色区分 (user/agent) 37 | 38 | ### A2A vs MCP 39 | 40 | | 特性 | A2A | MCP | 41 | |------|-----|-----| 42 | | 目的 | Agent 之间通信 | Agent 访问外部工具/数据 | 43 | | 通信方向 | Agent ↔ Agent | Agent → 工具/数据源 | 44 | | 复杂度 | 支持复杂任务流 | 简单的请求/响应 | 45 | | 状态管理 | 有状态 (Task 生命周期) | 无状态 | 46 | 47 | ## 项目结构 48 | 49 | ``` 50 | day-14/ 51 | ├── README.md 52 | ├── remote_agent/ # 远程 Agent (A2A 服务端) 53 | │ ├── __init__.py 54 | │ └── agent.py # 提供专业翻译服务的 Agent 55 | ├── host_agent/ # 主机 Agent (A2A 客户端) 56 | │ ├── __init__.py 57 | │ └── agent.py # 调用远程 Agent 的主 Agent 58 | └── run_demo.py # 演示脚本 59 | ``` 60 | 61 | ## 前提条件 62 | 63 | API Key 已在项目根目录 `.env` 文件中配置,代码会自动加载。 64 | 65 | ## 运行示例 (请按顺序操作) 66 | 67 | ### 第一步:启动服务端 (Terminal 1) 68 | 69 | ```bash 70 | cd day-14 71 | uv run python -m remote_agent.agent 72 | ``` 73 | 74 | > 也可以使用 `python3 -m remote_agent.agent` 75 | 76 | 看到以下输出表示服务端启动成功: 77 | ``` 78 | ============================================================ 79 | Day 14: A2A Remote Agent - 翻译服务 80 | ============================================================ 81 | Agent 信息: 82 | 名称: translator 83 | A2A 服务端点: 84 | URL: http://localhost:8001 85 | ============================================================ 86 | INFO: Uvicorn running on http://localhost:8001 87 | ``` 88 | 89 | ### 第二步:运行客户端 (Terminal 2) 90 | 91 | **新开一个终端**,运行: 92 | 93 | ```bash 94 | cd day-14 95 | uv run python -m host_agent.agent 96 | ``` 97 | 98 | > 也可以使用 `python3 -m host_agent.agent` 99 | 100 | 客户端会连接服务端,发送翻译请求,你会看到翻译结果。 101 | 102 | ### 运行效果 103 | 104 | ![A2A 运行效果](https://fisherai-1312281807.cos.ap-guangzhou.myqcloud.com/202512161543243.png) 105 | 106 | 左侧是服务端 (Remote Agent),右侧是客户端 (Host Agent) 的运行输出。 107 | 108 | ### 验证服务 109 | 110 | 在任意终端执行: 111 | ```bash 112 | curl http://localhost:8001/.well-known/agent-card.json 113 | ``` 114 | 115 | ### 关闭服务 116 | 117 | 在服务端终端按 `Ctrl+C` 即可关闭。 118 | 119 | ## 代码详解 120 | 121 | ### Remote Agent (服务端) - 关键代码 122 | 123 | ```python 124 | from google.adk.agents import LlmAgent 125 | from google.adk.a2a.utils.agent_to_a2a import to_a2a 126 | 127 | # 创建一个专业翻译 Agent 128 | translator = LlmAgent( 129 | name="translator", 130 | description="专业多语言翻译 Agent", 131 | model="gemini-2.0-flash", 132 | instruction="你是专业翻译,精通中英日韩等语言..." 133 | ) 134 | 135 | # 一行代码转换为 A2A 服务! 136 | app = to_a2a(translator, host="localhost", port=8001) 137 | 138 | # 启动服务 139 | if __name__ == "__main__": 140 | import uvicorn 141 | uvicorn.run(app, host="localhost", port=8001) 142 | ``` 143 | 144 | ### Host Agent (客户端) - 关键代码 145 | 146 | ```python 147 | import httpx 148 | from a2a.client import ClientFactory, ClientConfig 149 | from a2a.types import Role 150 | from a2a.client.helpers import create_text_message_object 151 | 152 | # 创建无代理的 HTTP 客户端配置 153 | def get_client_config(): 154 | httpx_client = httpx.AsyncClient( 155 | trust_env=False, # 禁用代理 156 | timeout=120.0, 157 | ) 158 | return ClientConfig(httpx_client=httpx_client) 159 | 160 | # 连接远程 Agent 161 | client = await ClientFactory.connect( 162 | "http://localhost:8001", 163 | client_config=get_client_config() 164 | ) 165 | 166 | # 创建消息 167 | message = create_text_message_object( 168 | role=Role.user, 169 | content="请翻译:Hello World" 170 | ) 171 | 172 | # 发送请求并处理响应 173 | async for event in client.send_message(request=message): 174 | if isinstance(event, tuple): 175 | task, task_event = event[0], event[1] 176 | if task_event and hasattr(task_event, 'artifact'): 177 | for part in task_event.artifact.parts: 178 | if hasattr(part, 'text'): 179 | print(f"翻译结果: {part.text}") 180 | ``` 181 | 182 | ## 关键学习点 183 | 184 | 1. **`to_a2a()` 函数** - ADK 提供的便捷方法,将任何 Agent 转换为 A2A 服务 185 | 2. **Agent Card** - 自动从 Agent 元数据生成,描述 Agent 能力 186 | 3. **ClientFactory.connect()** - A2A SDK 提供的连接方法,自动获取 Agent Card 187 | 4. **Task 状态流** - submitted → working → completed/failed 188 | 5. **跨框架互操作** - A2A 是协议标准,不限于 ADK 189 | 190 | ## API 快速参考 191 | 192 | ### 服务端 API 193 | 194 | | 函数 | 描述 | 195 | |------|------| 196 | | `to_a2a(agent, host, port)` | 将 ADK Agent 转换为 A2A 服务 | 197 | 198 | ### 客户端 API 199 | 200 | | 函数/类 | 描述 | 201 | |---------|------| 202 | | `ClientFactory.connect(url)` | 连接远程 A2A Agent | 203 | | `create_text_message_object(role, content)` | 创建文本消息 | 204 | | `client.send_message(request)` | 发送消息并获取响应流 | 205 | 206 | ## 客户端功能演示 207 | 208 | 当前 `host_agent/agent.py` 实现了 3 种 A2A 调用方式: 209 | 210 | ### 方法 1: 直接调用 (`call_remote_agent_directly`) 211 | 212 | 最基本的 A2A 调用模式: 213 | - 连接远程 Agent 214 | - 发送单次请求 215 | - 等待并获取完整响应 216 | 217 | ```python 218 | client = await ClientFactory.connect(url, client_config=config) 219 | message = create_text_message_object(role=Role.user, content="请翻译...") 220 | async for event in client.send_message(request=message): 221 | # 处理响应 222 | ``` 223 | 224 | ### 方法 2: 流式调用 (`call_remote_agent_streaming`) 225 | 226 | 适用于长文本,实时获取响应: 227 | - 边处理边显示结果 228 | - 可以看到任务状态变化 (submitted → working → completed) 229 | 230 | ### 方法 3: 多轮对话 (`multi_turn_conversation`) 231 | 232 | 演示连续对话场景: 233 | - 发送多个相关问题 234 | - 每次都创建新的请求 235 | 236 | > **注意**: 当前 SDK 版本不支持 `context_id` 参数,多轮对话上下文需要应用层自行管理。 237 | 238 | ## 扩展方向 239 | 240 | 基于当前代码,你可以尝试以下扩展: 241 | 242 | ### 1. 添加更多远程 Agent 243 | 244 | ```python 245 | # 创建多个专业 Agent 246 | code_agent = await ClientFactory.connect("http://localhost:8002") # 代码助手 247 | math_agent = await ClientFactory.connect("http://localhost:8003") # 数学助手 248 | 249 | # 根据任务类型选择合适的 Agent 250 | if "翻译" in user_request: 251 | agent = translator_agent 252 | elif "代码" in user_request: 253 | agent = code_agent 254 | ``` 255 | 256 | ### 2. Agent 编排 (Orchestration) 257 | 258 | ```python 259 | async def translate_and_summarize(text: str): 260 | """先翻译再总结""" 261 | # 调用翻译 Agent 262 | translated = await call_agent(translator_url, f"翻译: {text}") 263 | # 调用总结 Agent 264 | summary = await call_agent(summarizer_url, f"总结: {translated}") 265 | return summary 266 | ``` 267 | 268 | ### 3. 并行调用多个 Agent 269 | 270 | ```python 271 | import asyncio 272 | 273 | async def parallel_translate(text: str): 274 | """同时翻译成多种语言""" 275 | tasks = [ 276 | call_remote_agent(text, "中文"), 277 | call_remote_agent(text, "日文"), 278 | call_remote_agent(text, "韩文"), 279 | ] 280 | results = await asyncio.gather(*tasks) 281 | return dict(zip(["中文", "日文", "韩文"], results)) 282 | ``` 283 | 284 | ### 4. 错误重试机制 285 | 286 | ```python 287 | async def call_with_retry(client, message, max_retries=3): 288 | for attempt in range(max_retries): 289 | try: 290 | async for event in client.send_message(request=message): 291 | yield event 292 | return 293 | except Exception as e: 294 | if attempt == max_retries - 1: 295 | raise 296 | await asyncio.sleep(2 ** attempt) # 指数退避 297 | ``` 298 | 299 | ### 5. 健康检查 300 | 301 | ```python 302 | async def health_check(url: str) -> bool: 303 | """检查远程 Agent 是否可用""" 304 | try: 305 | async with httpx.AsyncClient(trust_env=False) as client: 306 | resp = await client.get(f"{url}/.well-known/agent-card.json") 307 | return resp.status_code == 200 308 | except: 309 | return False 310 | ``` 311 | 312 | ### 6. 创建新的远程 Agent (服务端扩展) 313 | 314 | 在 `remote_agent/` 目录下创建新的 Agent 服务: 315 | 316 | ```python 317 | # remote_agent/code_reviewer.py 318 | from google.adk.agents import LlmAgent 319 | from google.adk.a2a.utils.agent_to_a2a import to_a2a 320 | 321 | code_reviewer = LlmAgent( 322 | name="code_reviewer", 323 | description="专业代码审查 Agent - 检查代码质量、安全漏洞和最佳实践", 324 | model="gemini-2.0-flash", 325 | instruction="""你是一位资深的代码审查专家。 326 | 327 | 你的职责: 328 | - 检查代码逻辑错误和潜在 bug 329 | - 识别安全漏洞 (SQL注入、XSS等) 330 | - 评估代码可读性和可维护性 331 | - 建议性能优化方案 332 | - 检查是否符合最佳实践 333 | 334 | 输出格式: 335 | 1. 问题概述 336 | 2. 具体问题列表 (严重程度: 高/中/低) 337 | 3. 改进建议 338 | """, 339 | ) 340 | 341 | app = to_a2a(agent=code_reviewer, host="localhost", port=8002, protocol="http") 342 | 343 | if __name__ == "__main__": 344 | import uvicorn 345 | uvicorn.run(app, host="localhost", port=8002) 346 | ``` 347 | 348 | ### 7. 构建 Agent 注册中心 349 | 350 | ```python 351 | # agent_registry.py 352 | class AgentRegistry: 353 | """简单的 Agent 注册中心""" 354 | 355 | def __init__(self): 356 | self.agents = {} 357 | 358 | async def register(self, name: str, url: str): 359 | """注册一个 Agent""" 360 | if await self.health_check(url): 361 | client = await ClientFactory.connect(url, client_config=get_client_config()) 362 | self.agents[name] = { 363 | "url": url, 364 | "client": client, 365 | "card": client._card, 366 | } 367 | return True 368 | return False 369 | 370 | async def health_check(self, url: str) -> bool: 371 | try: 372 | async with httpx.AsyncClient(trust_env=False) as client: 373 | resp = await client.get(f"{url}/.well-known/agent-card.json") 374 | return resp.status_code == 200 375 | except: 376 | return False 377 | 378 | def get(self, name: str): 379 | """获取已注册的 Agent""" 380 | return self.agents.get(name) 381 | 382 | def list_agents(self): 383 | """列出所有可用的 Agent""" 384 | return { 385 | name: { 386 | "url": info["url"], 387 | "description": info["card"].description, 388 | } 389 | for name, info in self.agents.items() 390 | } 391 | 392 | # 使用示例 393 | registry = AgentRegistry() 394 | await registry.register("translator", "http://localhost:8001") 395 | await registry.register("code_reviewer", "http://localhost:8002") 396 | 397 | # 根据任务智能选择 Agent 398 | translator = registry.get("translator") 399 | ``` 400 | 401 | ### 8. 带工具的远程 Agent 402 | 403 | ```python 404 | # remote_agent/weather_agent.py 405 | from google.adk.agents import LlmAgent 406 | from google.adk.tools import FunctionTool 407 | 408 | def get_weather(city: str) -> str: 409 | """获取城市天气 (模拟)""" 410 | # 实际应用中可以调用真实天气 API 411 | weather_data = { 412 | "北京": "晴天,25°C", 413 | "上海": "多云,28°C", 414 | "东京": "小雨,22°C", 415 | } 416 | return weather_data.get(city, f"{city} 天气数据暂无") 417 | 418 | weather_tool = FunctionTool(func=get_weather) 419 | 420 | weather_agent = LlmAgent( 421 | name="weather_assistant", 422 | description="天气查询 Agent - 提供全球城市天气信息", 423 | model="gemini-2.0-flash", 424 | instruction="你是天气助手,帮助用户查询天气信息。", 425 | tools=[weather_tool], 426 | ) 427 | 428 | app = to_a2a(agent=weather_agent, host="localhost", port=8003, protocol="http") 429 | ``` 430 | 431 | ## 实战项目想法 432 | 433 | ### 项目 1: 多语言文档翻译系统 434 | 435 | ``` 436 | 用户 → Host Agent → 翻译 Agent (中文) 437 | → 翻译 Agent (日文) 438 | → 翻译 Agent (韩文) 439 | → 汇总结果返回用户 440 | ``` 441 | 442 | ### 项目 2: 代码审查流水线 443 | 444 | ``` 445 | 代码提交 → 代码审查 Agent → 安全扫描 Agent → 性能分析 Agent → 生成报告 446 | ``` 447 | 448 | ### 项目 3: 智能客服系统 449 | 450 | ``` 451 | 用户问题 → 路由 Agent (分析问题类型) 452 | ├→ FAQ Agent (常见问题) 453 | ├→ 技术支持 Agent (技术问题) 454 | └→ 订单 Agent (订单相关) 455 | ``` 456 | 457 | ### 项目 4: 研究助手 458 | 459 | ``` 460 | 研究主题 → 搜索 Agent (收集资料) 461 | → 总结 Agent (提取要点) 462 | → 写作 Agent (生成报告) 463 | ``` 464 | 465 | ## 常见问题 466 | 467 | ### Q: 服务启动但客户端连接失败? 468 | A: 检查是否有代理设置。使用 `httpx.AsyncClient(trust_env=False)` 禁用代理。 469 | 470 | ### Q: 看到 "Missing key inputs argument" 错误? 471 | A: 确保设置了 `GOOGLE_API_KEY` 环境变量。 472 | 473 | ### Q: Agent Card 端点是什么? 474 | A: 标准端点是 `/.well-known/agent-card.json`(旧版本是 `/.well-known/agent.json`)。 475 | 476 | ## 扩展阅读 477 | 478 | - [A2A Protocol Spec](https://a2a-protocol.org/) 479 | - [ADK A2A Docs](https://google.github.io/adk-docs/a2a/) 480 | - [Agent Starter Pack](https://github.com/GoogleCloudPlatform/agent-starter-pack) 481 | - [Prototype to Production Whitepaper](https://www.kaggle.com/whitepaper-prototype-to-production) 482 | -------------------------------------------------------------------------------- /day-16/client/hitl_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Human-in-the-Loop 客户端 3 | 4 | 交互式客户端,演示完整的 Human-in-the-Loop 流程: 5 | 1. 提交任务请求 6 | 2. 查看待审批任务 7 | 3. 审批或拒绝任务 8 | 4. 查看执行结果 9 | 10 | 运行方式: 11 | python -m client.hitl_client 12 | """ 13 | 14 | import asyncio 15 | import sys 16 | import os 17 | from typing import Optional 18 | 19 | # 添加项目根目录到路径 20 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 21 | 22 | import httpx 23 | 24 | # HITL Server URL 25 | HITL_SERVER_URL = os.getenv("HITL_SERVER_URL", "http://localhost:8016") 26 | 27 | 28 | class HITLClient: 29 | """Human-in-the-Loop 客户端""" 30 | 31 | def __init__(self, base_url: str = HITL_SERVER_URL): 32 | self.base_url = base_url 33 | self.client = httpx.AsyncClient( 34 | base_url=base_url, 35 | timeout=120.0, 36 | trust_env=False, 37 | ) 38 | 39 | async def close(self): 40 | """关闭客户端""" 41 | await self.client.aclose() 42 | 43 | async def health_check(self) -> bool: 44 | """健康检查""" 45 | try: 46 | resp = await self.client.get("/health") 47 | return resp.status_code == 200 48 | except Exception: 49 | return False 50 | 51 | async def create_task(self, message: str) -> dict: 52 | """创建新任务""" 53 | resp = await self.client.post( 54 | "/tasks", 55 | json={"message": message}, 56 | ) 57 | resp.raise_for_status() 58 | return resp.json() 59 | 60 | async def list_tasks(self, status: Optional[str] = None) -> dict: 61 | """获取任务列表""" 62 | params = {} 63 | if status: 64 | params["status"] = status 65 | resp = await self.client.get("/tasks", params=params) 66 | resp.raise_for_status() 67 | return resp.json() 68 | 69 | async def get_pending_approvals(self) -> dict: 70 | """获取待审批任务""" 71 | resp = await self.client.get("/tasks/pending") 72 | resp.raise_for_status() 73 | return resp.json() 74 | 75 | async def get_task(self, task_id: str) -> dict: 76 | """获取任务详情""" 77 | resp = await self.client.get(f"/tasks/{task_id}") 78 | resp.raise_for_status() 79 | return resp.json() 80 | 81 | async def approve_task( 82 | self, 83 | task_id: str, 84 | comment: str = "", 85 | approver: str = "user", 86 | ) -> dict: 87 | """批准任务""" 88 | resp = await self.client.post( 89 | f"/tasks/{task_id}/approve", 90 | json={ 91 | "approved": True, 92 | "comment": comment, 93 | "approver": approver, 94 | }, 95 | ) 96 | resp.raise_for_status() 97 | return resp.json() 98 | 99 | async def reject_task( 100 | self, 101 | task_id: str, 102 | comment: str = "", 103 | approver: str = "user", 104 | ) -> dict: 105 | """拒绝任务""" 106 | resp = await self.client.post( 107 | f"/tasks/{task_id}/reject", 108 | json={ 109 | "approved": False, 110 | "comment": comment, 111 | "approver": approver, 112 | }, 113 | ) 114 | resp.raise_for_status() 115 | return resp.json() 116 | 117 | 118 | # ============================================================ 119 | # 交互式演示 120 | # ============================================================ 121 | 122 | def print_header(title: str): 123 | """打印标题""" 124 | print() 125 | print("=" * 60) 126 | print(f" {title}") 127 | print("=" * 60) 128 | 129 | 130 | def print_task(task: dict, show_details: bool = False): 131 | """打印任务信息""" 132 | print(f" ID: {task.get('task_id', 'N/A')[:8]}...") 133 | print(f" 状态: {task.get('status', 'N/A')}") 134 | if task.get('pending_action'): 135 | print(f" 待执行: {task.get('pending_action')}") 136 | if task.get('user_input'): 137 | print(f" 用户输入: {task.get('user_input')[:50]}...") 138 | if show_details: 139 | print(f" 创建时间: {task.get('created_at', 'N/A')}") 140 | print(f" 更新时间: {task.get('updated_at', 'N/A')}") 141 | 142 | 143 | async def demo_low_risk(): 144 | """演示低风险任务(自动执行)""" 145 | print_header("场景 1: 低风险任务 - 自动执行") 146 | print(" 请求: 查询今天的天气") 147 | print("-" * 60) 148 | 149 | client = HITLClient() 150 | try: 151 | result = await client.create_task("查询今天北京的天气") 152 | print(f"\n 任务创建成功!") 153 | print(f" Task ID: {result.get('task_id', 'N/A')[:8]}...") 154 | print(f" 状态: {result.get('status')}") 155 | print(f" 需要审批: {result.get('requires_approval')}") 156 | print(f" 风险等级: {result.get('risk_level')}") 157 | print() 158 | print(f" 响应: {result.get('message')[:200]}...") 159 | except Exception as e: 160 | print(f" ❌ 错误: {e}") 161 | finally: 162 | await client.close() 163 | 164 | 165 | async def demo_high_risk(): 166 | """演示高风险任务(需要审批)""" 167 | print_header("场景 2: 高风险任务 - 需要审批") 168 | print(" 请求: 删除所有用户数据") 169 | print("-" * 60) 170 | 171 | client = HITLClient() 172 | try: 173 | # 创建任务 174 | result = await client.create_task("请帮我删除所有用户数据") 175 | task_id = result.get('task_id') 176 | 177 | print(f"\n 任务创建成功!") 178 | print(f" Task ID: {task_id[:8]}...") 179 | print(f" 状态: {result.get('status')}") 180 | print(f" 需要审批: {result.get('requires_approval')}") 181 | print(f" 风险等级: {result.get('risk_level')}") 182 | print(f" 待执行操作: {result.get('action_description')}") 183 | print() 184 | print(f" 消息: {result.get('message')}") 185 | 186 | if result.get('status') == 'waiting_approval': 187 | print() 188 | print("-" * 60) 189 | print(" 任务等待审批中...") 190 | print("-" * 60) 191 | 192 | # 模拟人工审批决策 193 | print() 194 | print(" [模拟审批] 这是一个危险操作,拒绝执行") 195 | 196 | # 拒绝任务 197 | reject_result = await client.reject_task( 198 | task_id, 199 | comment="操作过于危险,不允许批量删除用户数据", 200 | approver="security_admin", 201 | ) 202 | 203 | print() 204 | print(f" 审批结果: {reject_result.get('status')}") 205 | print(f" 响应: {reject_result.get('result', 'N/A')[:200]}...") 206 | 207 | except Exception as e: 208 | print(f" ❌ 错误: {e}") 209 | finally: 210 | await client.close() 211 | 212 | 213 | async def demo_approved_task(): 214 | """演示审批通过的任务""" 215 | print_header("场景 3: 审批通过的任务") 216 | print(" 请求: 发送一封邮件给用户") 217 | print("-" * 60) 218 | 219 | client = HITLClient() 220 | try: 221 | # 创建任务 222 | result = await client.create_task("发送一封欢迎邮件给新注册的用户 test@example.com") 223 | task_id = result.get('task_id') 224 | 225 | print(f"\n 任务创建成功!") 226 | print(f" Task ID: {task_id[:8]}...") 227 | print(f" 状态: {result.get('status')}") 228 | print(f" 需要审批: {result.get('requires_approval')}") 229 | print(f" 风险等级: {result.get('risk_level')}") 230 | 231 | if result.get('status') == 'waiting_approval': 232 | print() 233 | print("-" * 60) 234 | print(" 任务等待审批中...") 235 | print("-" * 60) 236 | 237 | # 模拟人工审批决策 238 | print() 239 | print(" [模拟审批] 邮件内容合规,批准发送") 240 | 241 | # 批准任务 242 | approve_result = await client.approve_task( 243 | task_id, 244 | comment="邮件内容已审核,批准发送", 245 | approver="content_reviewer", 246 | ) 247 | 248 | print() 249 | print(f" 审批结果: {approve_result.get('status')}") 250 | print(f" 执行结果: {approve_result.get('result', 'N/A')[:200]}...") 251 | 252 | except Exception as e: 253 | print(f" ❌ 错误: {e}") 254 | finally: 255 | await client.close() 256 | 257 | 258 | async def demo_list_tasks(): 259 | """演示任务列表查询""" 260 | print_header("任务列表查询") 261 | 262 | client = HITLClient() 263 | try: 264 | # 获取所有任务 265 | result = await client.list_tasks() 266 | tasks = result.get('tasks', []) 267 | 268 | print(f"\n 共 {result.get('total', 0)} 个任务:") 269 | print("-" * 60) 270 | 271 | for task in tasks[:5]: # 只显示前 5 个 272 | print() 273 | print_task(task) 274 | 275 | if len(tasks) > 5: 276 | print(f"\n ... 还有 {len(tasks) - 5} 个任务") 277 | 278 | # 获取待审批任务 279 | pending = await client.get_pending_approvals() 280 | pending_tasks = pending.get('tasks', []) 281 | 282 | if pending_tasks: 283 | print() 284 | print("-" * 60) 285 | print(f" 待审批任务: {len(pending_tasks)} 个") 286 | print("-" * 60) 287 | for task in pending_tasks: 288 | print() 289 | print_task(task) 290 | 291 | except Exception as e: 292 | print(f" ❌ 错误: {e}") 293 | finally: 294 | await client.close() 295 | 296 | 297 | async def interactive_mode(): 298 | """交互模式""" 299 | print_header("交互模式") 300 | print(" 输入命令与 HITL Agent 交互") 301 | print() 302 | print(" 可用命令:") 303 | print(" send - 发送请求") 304 | print(" list - 查看任务列表") 305 | print(" pending - 查看待审批任务") 306 | print(" approve - 批准任务") 307 | print(" reject - 拒绝任务") 308 | print(" detail - 查看任务详情") 309 | print(" quit - 退出") 310 | print() 311 | 312 | client = HITLClient() 313 | 314 | try: 315 | while True: 316 | try: 317 | user_input = input("HITL> ").strip() 318 | if not user_input: 319 | continue 320 | 321 | parts = user_input.split(maxsplit=1) 322 | command = parts[0].lower() 323 | args = parts[1] if len(parts) > 1 else "" 324 | 325 | if command == "quit" or command == "exit": 326 | print("再见!") 327 | break 328 | 329 | elif command == "send": 330 | if not args: 331 | print("请输入消息内容") 332 | continue 333 | result = await client.create_task(args) 334 | print(f"\n任务 ID: {result.get('task_id')}") 335 | print(f"状态: {result.get('status')}") 336 | print(f"风险等级: {result.get('risk_level')}") 337 | print(f"响应: {result.get('message')[:300]}...") 338 | 339 | elif command == "list": 340 | result = await client.list_tasks() 341 | tasks = result.get('tasks', []) 342 | print(f"\n共 {len(tasks)} 个任务:") 343 | for task in tasks[:10]: 344 | print(f" [{task.get('status'):15}] {task.get('task_id')[:8]}... - {task.get('user_input', '')[:30]}...") 345 | 346 | elif command == "pending": 347 | result = await client.get_pending_approvals() 348 | tasks = result.get('tasks', []) 349 | if tasks: 350 | print(f"\n待审批任务 ({len(tasks)} 个):") 351 | for task in tasks: 352 | print(f" {task.get('task_id')[:8]}... - {task.get('pending_action', '')[:50]}...") 353 | else: 354 | print("\n没有待审批的任务") 355 | 356 | elif command == "approve": 357 | if not args: 358 | print("请输入任务 ID") 359 | continue 360 | comment = input("审批意见 (可选): ").strip() 361 | result = await client.approve_task(args, comment) 362 | print(f"\n审批结果: {result.get('status')}") 363 | print(f"响应: {result.get('result', 'N/A')[:300]}...") 364 | 365 | elif command == "reject": 366 | if not args: 367 | print("请输入任务 ID") 368 | continue 369 | comment = input("拒绝原因: ").strip() 370 | result = await client.reject_task(args, comment) 371 | print(f"\n审批结果: {result.get('status')}") 372 | print(f"响应: {result.get('result', 'N/A')[:300]}...") 373 | 374 | elif command == "detail": 375 | if not args: 376 | print("请输入任务 ID") 377 | continue 378 | result = await client.get_task(args) 379 | task = result.get('task', {}) 380 | print(f"\n任务详情:") 381 | print(f" ID: {task.get('task_id')}") 382 | print(f" 状态: {task.get('status')}") 383 | print(f" 用户输入: {task.get('user_input')}") 384 | print(f" 待执行: {task.get('pending_action')}") 385 | print(f" 创建时间: {task.get('created_at')}") 386 | if result.get('state'): 387 | state = result['state'] 388 | print(f" 风险等级: {state.get('risk_level')}") 389 | print(f" 分析: {state.get('analysis', '')[:100]}...") 390 | 391 | else: 392 | print(f"未知命令: {command}") 393 | 394 | except KeyboardInterrupt: 395 | print("\n再见!") 396 | break 397 | except Exception as e: 398 | print(f"错误: {e}") 399 | 400 | finally: 401 | await client.close() 402 | 403 | 404 | async def main(): 405 | """主程序""" 406 | print("=" * 60) 407 | print(" Day 16: Human-in-the-Loop Client") 408 | print("=" * 60) 409 | print() 410 | print(f"服务地址: {HITL_SERVER_URL}") 411 | 412 | # 健康检查 413 | client = HITLClient() 414 | try: 415 | is_healthy = await client.health_check() 416 | if not is_healthy: 417 | print() 418 | print("❌ 无法连接到 HITL 服务") 419 | print(" 请确保服务正在运行:") 420 | print(" python -m langgraph_agent.server") 421 | return 422 | print("✅ 服务连接正常") 423 | finally: 424 | await client.close() 425 | 426 | print() 427 | print("选择演示模式:") 428 | print(" 1. 低风险任务演示 (自动执行)") 429 | print(" 2. 高风险任务演示 (需要审批)") 430 | print(" 3. 审批通过演示") 431 | print(" 4. 查看任务列表") 432 | print(" 5. 交互模式") 433 | print(" 6. 运行所有演示") 434 | print(" q. 退出") 435 | print() 436 | 437 | choice = input("请选择 (1-6/q): ").strip() 438 | 439 | if choice == "1": 440 | await demo_low_risk() 441 | elif choice == "2": 442 | await demo_high_risk() 443 | elif choice == "3": 444 | await demo_approved_task() 445 | elif choice == "4": 446 | await demo_list_tasks() 447 | elif choice == "5": 448 | await interactive_mode() 449 | elif choice == "6": 450 | await demo_low_risk() 451 | await asyncio.sleep(1) 452 | await demo_high_risk() 453 | await asyncio.sleep(1) 454 | await demo_approved_task() 455 | await asyncio.sleep(1) 456 | await demo_list_tasks() 457 | elif choice.lower() == "q": 458 | print("再见!") 459 | else: 460 | print("无效选择") 461 | 462 | print() 463 | print("=" * 60) 464 | print(" 演示结束") 465 | print("=" * 60) 466 | 467 | 468 | if __name__ == "__main__": 469 | asyncio.run(main()) 470 | -------------------------------------------------------------------------------- /day-16/langgraph_agent/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Human-in-the-Loop Server - 生产级 API 服务 3 | 4 | 提供完整的 REST API: 5 | - POST /tasks - 创建新任务 6 | - GET /tasks - 获取任务列表 7 | - GET /tasks/{id} - 获取任务详情 8 | - POST /tasks/{id}/approve - 批准任务 9 | - POST /tasks/{id}/reject - 拒绝任务 10 | - GET /tasks/pending - 获取待审批任务 11 | 12 | 同时提供 A2A 协议支持。 13 | 14 | 运行方式: 15 | python -m langgraph_agent.server 16 | """ 17 | 18 | import os 19 | import sys 20 | import asyncio 21 | import logging 22 | from typing import Optional 23 | from datetime import datetime, timedelta 24 | from contextlib import asynccontextmanager 25 | 26 | from fastapi import FastAPI, HTTPException, BackgroundTasks 27 | from fastapi.middleware.cors import CORSMiddleware 28 | from fastapi.staticfiles import StaticFiles 29 | from fastapi.responses import FileResponse 30 | from pydantic import BaseModel, Field 31 | 32 | # 添加项目根目录到路径 33 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 34 | 35 | from dotenv import load_dotenv 36 | project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 37 | load_dotenv(os.path.join(project_root, ".env")) 38 | 39 | from langgraph_agent.hitl_agent import ( 40 | run_hitl_agent, 41 | resume_after_approval, 42 | create_hitl_graph, 43 | ) 44 | from langgraph_agent.checkpointer import ( 45 | get_task_store, 46 | get_checkpointer, 47 | TaskMetadata, 48 | ) 49 | 50 | # 配置日志 51 | logging.basicConfig( 52 | level=logging.INFO, 53 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 54 | ) 55 | logger = logging.getLogger(__name__) 56 | 57 | 58 | # ============================================================ 59 | # Pydantic Models 60 | # ============================================================ 61 | 62 | class TaskCreateRequest(BaseModel): 63 | """创建任务请求""" 64 | message: str = Field(..., description="用户输入的消息") 65 | metadata: Optional[dict] = Field(default=None, description="额外元数据") 66 | 67 | 68 | class TaskCreateResponse(BaseModel): 69 | """创建任务响应""" 70 | task_id: str 71 | thread_id: str 72 | status: str 73 | requires_approval: bool 74 | risk_level: Optional[str] = None 75 | action_description: Optional[str] = None 76 | message: str 77 | 78 | 79 | class ApprovalRequest(BaseModel): 80 | """审批请求""" 81 | approved: bool = Field(..., description="是否批准") 82 | comment: Optional[str] = Field(default="", description="审批意见") 83 | approver: Optional[str] = Field(default="system", description="审批人") 84 | 85 | 86 | class ApprovalResponse(BaseModel): 87 | """审批响应""" 88 | task_id: str 89 | status: str 90 | result: Optional[str] = None 91 | message: str 92 | 93 | 94 | class TaskInfo(BaseModel): 95 | """任务信息""" 96 | task_id: str 97 | thread_id: str 98 | status: str 99 | created_at: str 100 | updated_at: str 101 | current_node: Optional[str] = None 102 | pending_action: Optional[str] = None 103 | user_input: Optional[str] = None 104 | error_message: Optional[str] = None 105 | 106 | 107 | class TaskListResponse(BaseModel): 108 | """任务列表响应""" 109 | tasks: list[TaskInfo] 110 | total: int 111 | 112 | 113 | class TaskDetailResponse(BaseModel): 114 | """任务详情响应""" 115 | task: TaskInfo 116 | state: Optional[dict] = None 117 | 118 | 119 | # ============================================================ 120 | # 超时检查后台任务 121 | # ============================================================ 122 | 123 | # 审批超时时间(秒) 124 | APPROVAL_TIMEOUT_SECONDS = int(os.getenv("APPROVAL_TIMEOUT_SECONDS", "300")) # 默认 5 分钟 125 | 126 | async def check_approval_timeouts(): 127 | """ 128 | 后台任务:检查审批超时 129 | 130 | 每隔一段时间检查等待审批的任务,超时的自动拒绝 131 | """ 132 | while True: 133 | try: 134 | task_store = get_task_store() 135 | pending_tasks = task_store.get_pending_approvals() 136 | 137 | for task in pending_tasks: 138 | # 检查是否超时 139 | created_at = datetime.fromisoformat(task.created_at) 140 | timeout_at = created_at + timedelta(seconds=APPROVAL_TIMEOUT_SECONDS) 141 | 142 | if datetime.utcnow() > timeout_at: 143 | logger.warning(f"Task {task.task_id} approval timeout, auto-rejecting") 144 | 145 | # 自动拒绝 146 | try: 147 | resume_after_approval( 148 | task_id=task.task_id, 149 | thread_id=task.thread_id, 150 | approved=False, 151 | comment="审批超时,自动拒绝", 152 | approver="system_timeout", 153 | ) 154 | task_store.update_task(task.task_id, status="timeout") 155 | except Exception as e: 156 | logger.error(f"Failed to auto-reject task {task.task_id}: {e}") 157 | 158 | except Exception as e: 159 | logger.error(f"Error in timeout check: {e}") 160 | 161 | # 每 30 秒检查一次 162 | await asyncio.sleep(30) 163 | 164 | 165 | # ============================================================ 166 | # FastAPI App 167 | # ============================================================ 168 | 169 | @asynccontextmanager 170 | async def lifespan(app: FastAPI): 171 | """应用生命周期管理""" 172 | # 启动时 173 | logger.info("Starting HITL Server...") 174 | logger.info(f"Approval timeout: {APPROVAL_TIMEOUT_SECONDS} seconds") 175 | 176 | # 启动超时检查后台任务 177 | timeout_task = asyncio.create_task(check_approval_timeouts()) 178 | 179 | yield 180 | 181 | # 关闭时 182 | logger.info("Shutting down HITL Server...") 183 | timeout_task.cancel() 184 | try: 185 | await timeout_task 186 | except asyncio.CancelledError: 187 | pass 188 | 189 | 190 | app = FastAPI( 191 | title="Human-in-the-Loop Agent API", 192 | description="生产级 Human-in-the-Loop Agent 服务", 193 | version="1.0.0", 194 | lifespan=lifespan, 195 | ) 196 | 197 | # CORS 配置 198 | app.add_middleware( 199 | CORSMiddleware, 200 | allow_origins=["*"], # 生产环境应该限制 201 | allow_credentials=True, 202 | allow_methods=["*"], 203 | allow_headers=["*"], 204 | ) 205 | 206 | # 静态文件服务(Web UI) 207 | STATIC_DIR = os.path.join(os.path.dirname(__file__), "static") 208 | if os.path.exists(STATIC_DIR): 209 | app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") 210 | 211 | 212 | # ============================================================ 213 | # Web UI 入口 214 | # ============================================================ 215 | 216 | @app.get("/ui", tags=["Web UI"]) 217 | async def web_ui(): 218 | """Web UI 入口页面""" 219 | index_path = os.path.join(STATIC_DIR, "index.html") 220 | if os.path.exists(index_path): 221 | return FileResponse(index_path) 222 | raise HTTPException(status_code=404, detail="Web UI not found") 223 | 224 | 225 | # ============================================================ 226 | # API 端点 227 | # ============================================================ 228 | 229 | @app.post("/tasks", response_model=TaskCreateResponse, tags=["Tasks"]) 230 | async def create_task(request: TaskCreateRequest, background_tasks: BackgroundTasks): 231 | """ 232 | 创建新任务 233 | 234 | 流程: 235 | 1. 分析用户请求 236 | 2. 评估风险等级 237 | 3. 如果需要审批,返回等待审批状态 238 | 4. 如果不需要审批,直接执行并返回结果 239 | """ 240 | try: 241 | logger.info(f"Creating task for: {request.message[:50]}...") 242 | 243 | # 运行 Agent 244 | result = run_hitl_agent(request.message) 245 | 246 | task_id = result["task_id"] 247 | thread_id = result["thread_id"] 248 | status = result["status"] 249 | agent_result = result.get("result", {}) 250 | 251 | # 提取关键信息 252 | requires_approval = agent_result.get("requires_approval", False) 253 | risk_level = agent_result.get("risk_level", "") 254 | action_plan = agent_result.get("action_plan", {}) 255 | action_description = action_plan.get("description", "") 256 | final_answer = agent_result.get("final_answer", "") 257 | 258 | # 根据状态返回不同消息 259 | if status == "waiting_approval": 260 | message = f"任务需要审批。风险等级: {risk_level},操作: {action_description}" 261 | elif status == "completed": 262 | message = final_answer or "任务已完成" 263 | else: 264 | message = f"任务状态: {status}" 265 | 266 | return TaskCreateResponse( 267 | task_id=task_id, 268 | thread_id=thread_id, 269 | status=status, 270 | requires_approval=requires_approval, 271 | risk_level=risk_level, 272 | action_description=action_description, 273 | message=message, 274 | ) 275 | 276 | except Exception as e: 277 | logger.error(f"Failed to create task: {e}") 278 | raise HTTPException(status_code=500, detail=str(e)) 279 | 280 | 281 | @app.get("/tasks", response_model=TaskListResponse, tags=["Tasks"]) 282 | async def list_tasks(status: Optional[str] = None, limit: int = 50): 283 | """ 284 | 获取任务列表 285 | 286 | 可选参数: 287 | - status: 按状态筛选 (pending, waiting_approval, approved, rejected, completed, failed) 288 | - limit: 返回数量限制 289 | """ 290 | try: 291 | task_store = get_task_store() 292 | 293 | if status: 294 | tasks = task_store.get_tasks_by_status(status) 295 | else: 296 | # 获取所有任务(通过获取各状态的任务合并) 297 | all_tasks = [] 298 | for s in ["pending", "waiting_approval", "approved", "rejected", "completed", "failed", "timeout"]: 299 | all_tasks.extend(task_store.get_tasks_by_status(s)) 300 | # 按创建时间排序 301 | tasks = sorted(all_tasks, key=lambda t: t.created_at, reverse=True) 302 | 303 | # 限制数量 304 | tasks = tasks[:limit] 305 | 306 | return TaskListResponse( 307 | tasks=[ 308 | TaskInfo( 309 | task_id=t.task_id, 310 | thread_id=t.thread_id, 311 | status=t.status, 312 | created_at=t.created_at, 313 | updated_at=t.updated_at, 314 | current_node=t.current_node, 315 | pending_action=t.pending_action, 316 | user_input=t.user_input, 317 | error_message=t.error_message, 318 | ) 319 | for t in tasks 320 | ], 321 | total=len(tasks), 322 | ) 323 | 324 | except Exception as e: 325 | logger.error(f"Failed to list tasks: {e}") 326 | raise HTTPException(status_code=500, detail=str(e)) 327 | 328 | 329 | @app.get("/tasks/pending", response_model=TaskListResponse, tags=["Tasks"]) 330 | async def get_pending_approvals(): 331 | """获取所有待审批的任务""" 332 | try: 333 | task_store = get_task_store() 334 | tasks = task_store.get_pending_approvals() 335 | 336 | return TaskListResponse( 337 | tasks=[ 338 | TaskInfo( 339 | task_id=t.task_id, 340 | thread_id=t.thread_id, 341 | status=t.status, 342 | created_at=t.created_at, 343 | updated_at=t.updated_at, 344 | current_node=t.current_node, 345 | pending_action=t.pending_action, 346 | user_input=t.user_input, 347 | error_message=t.error_message, 348 | ) 349 | for t in tasks 350 | ], 351 | total=len(tasks), 352 | ) 353 | 354 | except Exception as e: 355 | logger.error(f"Failed to get pending approvals: {e}") 356 | raise HTTPException(status_code=500, detail=str(e)) 357 | 358 | 359 | @app.get("/tasks/{task_id}", response_model=TaskDetailResponse, tags=["Tasks"]) 360 | async def get_task(task_id: str): 361 | """获取任务详情""" 362 | try: 363 | task_store = get_task_store() 364 | task = task_store.get_task(task_id) 365 | 366 | if not task: 367 | raise HTTPException(status_code=404, detail=f"Task {task_id} not found") 368 | 369 | # 获取图状态(如果有) 370 | state = None 371 | try: 372 | checkpointer = get_checkpointer() 373 | config = {"configurable": {"thread_id": task.thread_id}} 374 | graph = create_hitl_graph(checkpointer) 375 | state_snapshot = graph.get_state(config) 376 | if state_snapshot: 377 | state = dict(state_snapshot.values) if state_snapshot.values else None 378 | except Exception as e: 379 | logger.warning(f"Failed to get state for task {task_id}: {e}") 380 | 381 | return TaskDetailResponse( 382 | task=TaskInfo( 383 | task_id=task.task_id, 384 | thread_id=task.thread_id, 385 | status=task.status, 386 | created_at=task.created_at, 387 | updated_at=task.updated_at, 388 | current_node=task.current_node, 389 | pending_action=task.pending_action, 390 | user_input=task.user_input, 391 | error_message=task.error_message, 392 | ), 393 | state=state, 394 | ) 395 | 396 | except HTTPException: 397 | raise 398 | except Exception as e: 399 | logger.error(f"Failed to get task {task_id}: {e}") 400 | raise HTTPException(status_code=500, detail=str(e)) 401 | 402 | 403 | @app.post("/tasks/{task_id}/approve", response_model=ApprovalResponse, tags=["Approval"]) 404 | async def approve_task(task_id: str, request: ApprovalRequest): 405 | """ 406 | 审批任务(批准) 407 | 408 | 批准后,任务将继续执行 409 | """ 410 | if not request.approved: 411 | raise HTTPException( 412 | status_code=400, 413 | detail="Use /tasks/{task_id}/reject for rejection" 414 | ) 415 | 416 | return await _process_approval(task_id, True, request.comment, request.approver) 417 | 418 | 419 | @app.post("/tasks/{task_id}/reject", response_model=ApprovalResponse, tags=["Approval"]) 420 | async def reject_task(task_id: str, request: ApprovalRequest): 421 | """ 422 | 审批任务(拒绝) 423 | 424 | 拒绝后,任务将终止并返回拒绝原因 425 | """ 426 | return await _process_approval(task_id, False, request.comment, request.approver) 427 | 428 | 429 | async def _process_approval( 430 | task_id: str, 431 | approved: bool, 432 | comment: str, 433 | approver: str, 434 | ) -> ApprovalResponse: 435 | """处理审批""" 436 | try: 437 | task_store = get_task_store() 438 | task = task_store.get_task(task_id) 439 | 440 | if not task: 441 | raise HTTPException(status_code=404, detail=f"Task {task_id} not found") 442 | 443 | if task.status != "waiting_approval": 444 | raise HTTPException( 445 | status_code=400, 446 | detail=f"Task is not waiting for approval. Current status: {task.status}" 447 | ) 448 | 449 | logger.info(f"Processing approval for task {task_id}: approved={approved}") 450 | 451 | # 恢复执行 452 | result = resume_after_approval( 453 | task_id=task_id, 454 | thread_id=task.thread_id, 455 | approved=approved, 456 | comment=comment or "", 457 | approver=approver or "system", 458 | ) 459 | 460 | # 获取最终结果 461 | agent_result = result.get("result", {}) 462 | final_answer = agent_result.get("final_answer", "") 463 | 464 | action = "批准" if approved else "拒绝" 465 | return ApprovalResponse( 466 | task_id=task_id, 467 | status=result["status"], 468 | result=final_answer, 469 | message=f"任务已{action}", 470 | ) 471 | 472 | except HTTPException: 473 | raise 474 | except Exception as e: 475 | logger.error(f"Failed to process approval for {task_id}: {e}") 476 | raise HTTPException(status_code=500, detail=str(e)) 477 | 478 | 479 | # ============================================================ 480 | # 健康检查 481 | # ============================================================ 482 | 483 | @app.get("/health", tags=["System"]) 484 | async def health_check(): 485 | """健康检查""" 486 | return { 487 | "status": "healthy", 488 | "timestamp": datetime.utcnow().isoformat(), 489 | "approval_timeout_seconds": APPROVAL_TIMEOUT_SECONDS, 490 | } 491 | 492 | 493 | @app.get("/", tags=["System"]) 494 | async def root(): 495 | """API 根路径""" 496 | return { 497 | "name": "Human-in-the-Loop Agent API", 498 | "version": "1.0.0", 499 | "web_ui": "/ui", 500 | "docs": "/docs", 501 | "endpoints": { 502 | "create_task": "POST /tasks", 503 | "list_tasks": "GET /tasks", 504 | "pending_approvals": "GET /tasks/pending", 505 | "get_task": "GET /tasks/{task_id}", 506 | "approve": "POST /tasks/{task_id}/approve", 507 | "reject": "POST /tasks/{task_id}/reject", 508 | } 509 | } 510 | 511 | 512 | # ============================================================ 513 | # 主程序 514 | # ============================================================ 515 | 516 | if __name__ == "__main__": 517 | import uvicorn 518 | 519 | print("=" * 60) 520 | print("Day 16: Human-in-the-Loop Agent Server") 521 | print("=" * 60) 522 | print() 523 | print("访问地址:") 524 | print(" Web UI: http://localhost:8016/ui <- 推荐") 525 | print(" API Docs: http://localhost:8016/docs") 526 | print() 527 | print("API 端点:") 528 | print(" POST /tasks - 创建新任务") 529 | print(" GET /tasks - 获取任务列表") 530 | print(" GET /tasks/pending - 获取待审批任务") 531 | print(" POST /tasks/{id}/approve - 批准任务") 532 | print(" POST /tasks/{id}/reject - 拒绝任务") 533 | print() 534 | print(f"审批超时: {APPROVAL_TIMEOUT_SECONDS} 秒") 535 | print("=" * 60) 536 | print() 537 | 538 | uvicorn.run(app, host="localhost", port=8016) 539 | --------------------------------------------------------------------------------