Files
NotesAgentic/backend/app/providers/mock.py
admin b71984d951 实现 AI Core 与 Agent Core 基础功能
- 更新 README 描述从后端壳子到 AI Core/Agent Core
- 添加 ToolCall 和 ToolResult 数据结构定义
- 扩展 AgentRun 模型增加输出、错误码、工具调用结果等字段
- 添加 mock 提供商类型支持
- 实现聊天、代理运行、工具调用和提供商管理的核心路由逻辑
- 集成容器化依赖注入和错误处理机制
- 更新 API 接口契约和文档说明
2026-08-27 13:55:00 +08:00

128 lines
4.4 KiB
Python

import json
import re
from collections.abc import AsyncIterator
from datetime import datetime, timezone
from uuid import uuid4
from app.contracts import (
MessageRole,
ModelCapability,
ModelEvent,
ModelEventType,
ModelInfo,
ModelRequest,
)
from app.providers.base import ProviderToolCall, ProviderTurn
_TOOL_PATTERN = re.compile(r"^/tool\s+([\w.-]+)(?:\s+(\{.*\}))?\s*$", re.DOTALL)
class MockProvider:
"""离线开发 Provider,用于验证聊天、Tool Calling 和 Agent Loop。"""
async def complete(self, request: ModelRequest) -> ProviderTurn:
if not request.messages:
return ProviderTurn(text="Mock provider received an empty conversation.")
last_message = request.messages[-1]
if last_message.role == MessageRole.tool:
return ProviderTurn(
text=f"Tool result received: {last_message.content}",
input_tokens=len(last_message.content.split()),
output_tokens=4,
)
match = _TOOL_PATTERN.match(last_message.content.strip())
if match:
raw_arguments = match.group(2) or "{}"
try:
arguments = json.loads(raw_arguments)
except json.JSONDecodeError:
return ProviderTurn(text="Mock tool arguments must be valid JSON.")
if not isinstance(arguments, dict):
return ProviderTurn(text="Mock tool arguments must be a JSON object.")
return ProviderTurn(
tool_calls=[
ProviderToolCall(
tool_call_id=f"call_{uuid4().hex}",
name=match.group(1),
arguments=arguments,
)
],
input_tokens=len(last_message.content.split()),
)
text = f"Mock response: {last_message.content}"
return ProviderTurn(
text=text,
input_tokens=len(last_message.content.split()),
output_tokens=len(text.split()),
)
async def stream(self, request: ModelRequest) -> AsyncIterator[ModelEvent]:
turn = await self.complete(request)
sequence = 0
if turn.tool_calls:
for call in turn.tool_calls:
yield ModelEvent(
event=ModelEventType.tool_call_start,
sequence=sequence,
data={
"tool_call_id": call.tool_call_id,
"name": call.name,
"arguments": call.arguments,
},
timestamp=datetime.now(timezone.utc),
)
sequence += 1
yield ModelEvent(
event=ModelEventType.tool_call_end,
sequence=sequence,
data={"tool_call_id": call.tool_call_id},
timestamp=datetime.now(timezone.utc),
)
sequence += 1
elif turn.text:
words = turn.text.split(" ")
for index, word in enumerate(words):
yield ModelEvent(
event=ModelEventType.text_delta,
sequence=sequence,
data={"text": word + (" " if index < len(words) - 1 else "")},
timestamp=datetime.now(timezone.utc),
)
sequence += 1
yield ModelEvent(
event=ModelEventType.usage,
sequence=sequence,
data={
"input_tokens": turn.input_tokens,
"output_tokens": turn.output_tokens,
},
timestamp=datetime.now(timezone.utc),
)
yield ModelEvent(
event=ModelEventType.done,
sequence=sequence + 1,
timestamp=datetime.now(timezone.utc),
)
async def list_models(self) -> list[ModelInfo]:
return [
ModelInfo(
model="mock-1",
display_name="Mock Provider (Development)",
capabilities=[
ModelCapability.chat,
ModelCapability.tool_calling,
ModelCapability.streaming,
],
)
]
async def test_connection(self, model: str | None = None) -> tuple[bool, str]:
if model not in (None, "mock-1"):
return False, f"Unknown mock model: {model}"
return True, "Mock provider is ready."