refactor: 精简agent代码,移除冗余的main入口,添加API文档和note_agent

- root_agent/note_agent: 移除main()函数及不再需要的import(asyncio/Runner/SessionService/types)
- root_agent: 更新agent名称为huiyu_agent,优化instruction
- agents/__init__.py: 添加root_agent导出,供adk web/api_server注册
- 新增 API_DOC.md: 完整的API接口文档,供外部项目集成参考
- 新增 note_agent: 多模态笔记助手agent
This commit is contained in:
2026-04-07 22:15:14 +08:00
parent 7210ad9c53
commit f5e8de559e
6 changed files with 982 additions and 66 deletions

View File

@@ -3,3 +3,8 @@ Agent 模块
每个 Agent 是一个独立的子目录,包含 agent.py 和 __init__.py。
"""
# 导出所有 Agent供 adk web 服务注册
from .root_agent import agent as root_agent
__all__ = ["root_agent"]

View File

@@ -0,0 +1,8 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from . import agent
__all__ = ["agent"]

View File

@@ -0,0 +1,77 @@
import os
from base.agent import HuiYuBaseAgent
from google.adk.models.lite_llm import LiteLlm
from .note_formatter import NoteFormatter
# ============================================================
# MiniMax 模型配置(从 .env 文件读取)
# ============================================================
MINIMAX_API_KEY = os.getenv("MINIMAX_API_KEY")
MINIMAX_API_BASE = os.getenv("MINIMAX_API_BASE")
MINIMAX_MODEL = os.getenv("MINIMAX_MODEL")
# ============================================================
# 创建 LiteLlm 模型适配器
# ============================================================
model = LiteLlm(
model=MINIMAX_MODEL,
api_base=MINIMAX_API_BASE,
api_key=MINIMAX_API_KEY,
)
# ============================================================
# 笔记格式化器
# ============================================================
_formatter = NoteFormatter()
# ============================================================
# Tool: 生成文本笔记
# ============================================================
def generate_text_note(title: str, content: str) -> str:
"""将文本内容整理为 Markdown 笔记。
Args:
title: 笔记标题
content: 笔记正文内容
"""
return _formatter.format_to_markdown(title=title, text_content=content)
# ============================================================
# Tool: 生成图片笔记
# ============================================================
def generate_image_note(title: str, description: str) -> str:
"""将图片描述整理为 Markdown 笔记。
Args:
title: 笔记标题
description: 图片描述或相关文字说明
"""
return _formatter.format_to_markdown(title=title, text_content=description)
# ============================================================
# 定义 Agent继承 HuiYuBaseAgent
# ============================================================
note_agent = HuiYuBaseAgent(
name="note_agent",
model=model,
description="一个多模态笔记助手,能够将图片、文字等内容转换为结构化的 Markdown 笔记。",
instruction=(
"你是一个专业的笔记助手,帮助用户将各种内容整理成 Markdown 笔记。\n"
"你的能力包括:\n"
"1. 接收用户输入的文字内容,整理成结构化的笔记\n"
"2. 接收用户上传的图片,生成图片描述并整理为笔记\n"
"3. 将混合内容(文字+图片)整合为一份完整的笔记\n\n"
"工作流程:\n"
"- 当用户发送文字时,整理内容并调用 generate_text_note 生成笔记\n"
"- 当用户发送图片时,描述图片内容并调用 generate_image_note 生成笔记\n"
"- 当用户发送混合内容时,整合所有内容后生成笔记\n"
"- 笔记标题由你根据内容自动生成,要简洁准确\n"
"- 生成笔记后,直接将 Markdown 内容输出给用户\n"
"- 回复时使用中文"
),
tools=[generate_text_note, generate_image_note],
)

View File

@@ -0,0 +1,208 @@
"""
笔记格式化模块
将多模态内容(图片、文本等)转换为 Markdown 笔记格式。
支持可扩展的处理器架构方便后续添加音频、视频、PDF 等格式。
"""
import base64
import hashlib
import logging
from abc import ABC, abstractmethod
from datetime import datetime
from pathlib import Path
from typing import Optional
logger = logging.getLogger("adk.agent")
# ============================================================
# 内容处理器基类
# ============================================================
class BaseContentHandler(ABC):
"""内容处理器基类,所有格式处理器继承此类。"""
@abstractmethod
def can_handle(self, mime_type: str) -> bool:
"""判断是否能处理该 MIME 类型"""
...
@abstractmethod
def process(self, data: bytes, mime_type: str, filename: str = "") -> str:
"""
处理内容并返回 Markdown 片段。
Args:
data: 原始字节数据
mime_type: MIME 类型
filename: 文件名(可选)
Returns:
Markdown 格式的字符串片段
"""
...
# ============================================================
# 图片处理器
# ============================================================
class ImageHandler(BaseContentHandler):
"""处理图片类型,保存到本地并生成 Markdown 图片引用。"""
SUPPORTED_TYPES = {"image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp", "image/svg+xml"}
def can_handle(self, mime_type: str) -> bool:
return mime_type.lower() in self.SUPPORTED_TYPES
def process(self, data: bytes, mime_type: str, filename: str = "") -> str:
# 确保输出目录存在
NOTES_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 生成文件名
if not filename:
ext = self._mime_to_ext(mime_type)
filename = f"image_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{hashlib.md5(data).hexdigest()[:8]}.{ext}"
filepath = NOTES_OUTPUT_DIR / filename
filepath.write_bytes(data)
logger.info("图片已保存: %s", filepath)
return f"![{filename}](notes/{filename})"
@staticmethod
def _mime_to_ext(mime_type: str) -> str:
return {
"image/png": "png",
"image/jpeg": "jpg",
"image/jpg": "jpg",
"image/gif": "gif",
"image/webp": "webp",
"image/svg+xml": "svg",
}.get(mime_type.lower(), "bin")
# ============================================================
# 文本处理器
# ============================================================
class TextHandler(BaseContentHandler):
"""处理纯文本内容。"""
SUPPORTED_TYPES = {"text/plain", "text/markdown", "text/csv"}
def can_handle(self, mime_type: str) -> bool:
return mime_type.lower() in self.SUPPORTED_TYPES
def process(self, data: bytes, mime_type: str, filename: str = "") -> str:
text = data.decode("utf-8", errors="replace")
if mime_type.lower() == "text/markdown":
return text # 已经是 Markdown直接返回
return text # 纯文本直接返回
# ============================================================
# 占位处理器(用于尚未支持的格式)
# ============================================================
class PlaceholderHandler(BaseContentHandler):
"""占位处理器,为尚未支持的格式生成占位标记。"""
def can_handle(self, mime_type: str) -> bool:
return True # 兜底,处理所有未匹配的类型
def process(self, data: bytes, mime_type: str, filename: str = "") -> str:
name = filename or "unsupported_file"
return f"<!-- [{name}] 格式 {mime_type} 暂不支持,待后续扩展 -->"
# ============================================================
# 笔记格式化器
# ============================================================
class NoteFormatter:
"""
笔记格式化器,管理所有内容处理器。
用法:
formatter = NoteFormatter()
md_content = formatter.format_to_markdown(
title="会议笔记",
text_content="讨论了项目进度",
files=[(image_bytes, "image/png", "screenshot.png")],
)
"""
def __init__(self):
self._handlers: list[BaseContentHandler] = [
ImageHandler(),
TextHandler(),
PlaceholderHandler(), # 必须放最后,作为兜底
]
def add_handler(self, handler: BaseContentHandler):
"""注册新的内容处理器(插入到占位处理器之前)"""
# 移除末尾的 PlaceholderHandler
if self._handlers and isinstance(self._handlers[-1], PlaceholderHandler):
self._handlers.pop()
self._handlers.append(handler)
# 重新添加 PlaceholderHandler 到末尾
self._handlers.append(PlaceholderHandler())
def _find_handler(self, mime_type: str) -> BaseContentHandler:
for handler in self._handlers:
if handler.can_handle(mime_type):
return handler
return self._handlers[-1] # PlaceholderHandler
def format_to_markdown(
self,
title: str = "",
text_content: str = "",
files: Optional[list[tuple[bytes, str, str]]] = None,
) -> str:
"""
将多模态内容格式化为 Markdown 笔记。
Args:
title: 笔记标题
text_content: 文本内容
files: 文件列表,每项为 (data, mime_type, filename)
Returns:
完整的 Markdown 笔记字符串
"""
lines: list[str] = []
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 标题
if title:
lines.append(f"# {title}")
lines.append("")
else:
lines.append(f"# 笔记 - {now}")
lines.append("")
# 元信息
lines.append(f"> 创建时间:{now}")
lines.append("")
# 文本内容
if text_content:
lines.append("## 内容")
lines.append("")
lines.append(text_content)
lines.append("")
# 附件
if files:
lines.append("## 附件")
lines.append("")
for data, mime_type, filename in files:
handler = self._find_handler(mime_type)
try:
md_fragment = handler.process(data, mime_type, filename)
lines.append(md_fragment)
lines.append("")
except Exception as e:
logger.error("处理文件失败: %s, 错误: %s", filename, e)
lines.append(f"<!-- 文件 {filename} 处理失败: {e} -->")
lines.append("")
return "\n".join(lines)

View File

@@ -1,11 +1,7 @@
import os
import asyncio
from base.agent import HuiYuBaseAgent
from google.adk.models.lite_llm import LiteLlm
from google.adk.sessions import InMemorySessionService
from google.adk.runners import Runner
from google.genai import types
# ============================================================
# MiniMax 模型配置(从 .env 文件读取)
@@ -27,67 +23,8 @@ model = LiteLlm(
# 定义 Agent继承 HuiYuBaseAgent自动获得防注入 + 日志能力)
# ============================================================
root_agent = HuiYuBaseAgent(
name="minimax_agent",
name="huiyu_agent",
model=model,
description="一个使用 MiniMax 模型的智能助手,能够回答用户的各种问题。",
instruction="你是一个乐于助人的中文 AI 助手,由 MiniMax 模型驱动。请用中文回答用户的问题,回答要准确、简洁、友好。",
description="一个智能助手,能够回答用户的各种问题。",
instruction="你是一个乐于助人的中文 AI 助手,你的名字叫小慧,请用中文回答用户的问题,回答要准确、简洁、友好。",
)
# ============================================================
# 运行入口(用于 adk run / adk web
# ============================================================
APP_NAME = "慧遇agent app"
USER_ID = "user_001"
SESSION_ID = "session_001"
async def main():
"""主函数:创建会话并运行 Agent 对话"""
# 1. 创建会话服务
session_service = InMemorySessionService()
# 2. 创建会话
session = await session_service.create_session(
app_name=APP_NAME,
user_id=USER_ID,
session_id=SESSION_ID,
)
print(f"会话已创建: App='{APP_NAME}', User='{USER_ID}', Session='{SESSION_ID}'")
# 3. 创建 Runner
runner = Runner(
agent=root_agent,
app_name=APP_NAME,
session_service=session_service,
)
print(f"Runner 已创建Agent: '{runner.agent.name}'")
# 4. 交互式对话循环
print("\n===== MiniMax Agent 对话 (输入 'exit' 退出) =====")
while True:
query = input("\n[user]: ").strip()
if query.lower() in ("exit", "quit", "退出"):
print("再见!")
break
if not query:
continue
# 构造 ADK 消息
content = types.Content(role="user", parts=[types.Part(text=query)])
# 调用 Agent
final_response_text = ""
async for event in runner.run_async(
user_id=USER_ID,
session_id=SESSION_ID,
new_message=content,
):
if event.is_final_response():
if event.content and event.content.parts:
final_response_text = event.content.parts[0].text
print(f"\n[agent]: {final_response_text}")
if __name__ == "__main__":
asyncio.run(main())