feat: add all application source code
- apps/api: FastAPI backend with Dockerfile - apps/web: Next.js frontend with Dockerfile - apps/sensor: Signal collection agent - packages: shared packages Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
184
apps/api/src/routes/agent.py
Normal file
184
apps/api/src/routes/agent.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
Agent (ClawBot) Endpoints
|
||||
ADR-005: BFF 架構 - 所有 AI 調用經過 BFF
|
||||
Phase 1.2: 真實 Ollama 串接
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ==================== Ollama Config ====================
|
||||
OLLAMA_BASE_URL = "http://192.168.0.188:11434"
|
||||
OLLAMA_MODEL = "llama3.2:latest" # 可根據實際部署調整
|
||||
OLLAMA_TIMEOUT = 120.0 # 串流超時
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
conversation_id: UUID | None = None
|
||||
context: dict | None = None
|
||||
|
||||
|
||||
class SuggestedAction(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
description: str | None = None
|
||||
risk_level: Literal["low", "medium", "high", "critical"]
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
message: str
|
||||
conversation_id: UUID
|
||||
actions: list[SuggestedAction] | None = None
|
||||
requires_approval: bool = False
|
||||
approval_id: UUID | None = None
|
||||
|
||||
|
||||
class AgentStatus(BaseModel):
|
||||
status: Literal["idle", "thinking", "executing", "waiting_approval"]
|
||||
active_conversations: int
|
||||
current_task: str | None = None
|
||||
last_activity: datetime | None = None
|
||||
|
||||
|
||||
@router.post("/chat", response_model=ChatResponse)
|
||||
async def chat_with_agent(request: ChatRequest) -> ChatResponse:
|
||||
"""與 ClawBot 對話"""
|
||||
conversation_id = request.conversation_id or uuid4()
|
||||
|
||||
# TODO: 實際調用 ClawBot
|
||||
return ChatResponse(
|
||||
message=f"收到訊息: {request.message}",
|
||||
conversation_id=conversation_id,
|
||||
requires_approval=False,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/chat/stream")
|
||||
async def chat_with_agent_stream(request: ChatRequest) -> StreamingResponse:
|
||||
"""與 ClawBot 對話 (SSE 串流)"""
|
||||
|
||||
async def generate():
|
||||
# TODO: 實際串流
|
||||
yield "data: Hello from ClawBot\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
generate(),
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/status", response_model=AgentStatus)
|
||||
async def get_agent_status() -> AgentStatus:
|
||||
"""ClawBot 狀態"""
|
||||
return AgentStatus(
|
||||
status="idle",
|
||||
active_conversations=0,
|
||||
current_task=None,
|
||||
last_activity=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/thinking")
|
||||
async def get_agent_thinking(
|
||||
prompt: str = Query(
|
||||
default="你是 AWOOOI 智能運維助手。請簡短分析一下目前系統的健康狀態,用中文回答。",
|
||||
description="發送給 AI 的提示詞",
|
||||
),
|
||||
model: str = Query(default=OLLAMA_MODEL, description="Ollama 模型名稱"),
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
ClawBot 思考軌跡 (SSE 串流)
|
||||
Phase 1.2: 真實串接 Ollama at 192.168.0.188:11434
|
||||
"""
|
||||
|
||||
async def generate_thinking_stream():
|
||||
"""串接 Ollama 並轉換為 SSE 格式"""
|
||||
# 1. 開始思考
|
||||
yield f"data: {json.dumps({'type': 'thinking', 'content': '正在連接 AI 模型...'}, ensure_ascii=False)}\n\n"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=OLLAMA_TIMEOUT) as client:
|
||||
# 2. 發送請求到 Ollama
|
||||
yield f"data: {json.dumps({'type': 'thinking', 'content': f'模型: {model}'}, ensure_ascii=False)}\n\n"
|
||||
|
||||
async with client.stream(
|
||||
"POST",
|
||||
f"{OLLAMA_BASE_URL}/api/generate",
|
||||
json={
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": True,
|
||||
},
|
||||
) as response:
|
||||
if response.status_code != 200:
|
||||
yield f"data: {json.dumps({'type': 'error', 'content': f'Ollama 錯誤: HTTP {response.status_code}'}, ensure_ascii=False)}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
return
|
||||
|
||||
yield f"data: {json.dumps({'type': 'thinking', 'content': '開始接收 AI 回應...'}, ensure_ascii=False)}\n\n"
|
||||
|
||||
# 3. 串流讀取 Ollama 回應
|
||||
buffer = ""
|
||||
async for line in response.aiter_lines():
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
chunk = json.loads(line)
|
||||
token = chunk.get("response", "")
|
||||
done = chunk.get("done", False)
|
||||
|
||||
if token:
|
||||
# 累積 token,每 10 字符或遇到標點符號時發送
|
||||
buffer += token
|
||||
if len(buffer) >= 10 or any(p in buffer for p in "。!?,、\n"):
|
||||
yield f"data: {json.dumps({'type': 'thinking', 'content': buffer}, ensure_ascii=False)}\n\n"
|
||||
buffer = ""
|
||||
|
||||
if done:
|
||||
# 發送剩餘 buffer
|
||||
if buffer:
|
||||
yield f"data: {json.dumps({'type': 'thinking', 'content': buffer}, ensure_ascii=False)}\n\n"
|
||||
# 發送完成訊息
|
||||
yield f"data: {json.dumps({'type': 'result', 'content': '分析完成'}, ensure_ascii=False)}\n\n"
|
||||
break
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"JSON 解析失敗: {line[:100]}... - {e}")
|
||||
continue
|
||||
|
||||
except httpx.ConnectError as e:
|
||||
logger.error(f"無法連接 Ollama: {e}")
|
||||
yield f"data: {json.dumps({'type': 'error', 'content': f'無法連接 Ollama ({OLLAMA_BASE_URL})'}, ensure_ascii=False)}\n\n"
|
||||
except httpx.TimeoutException as e:
|
||||
logger.error(f"Ollama 超時: {e}")
|
||||
yield f"data: {json.dumps({'type': 'error', 'content': '請求超時'}, ensure_ascii=False)}\n\n"
|
||||
except Exception as e:
|
||||
logger.error(f"未知錯誤: {e}")
|
||||
yield f"data: {json.dumps({'type': 'error', 'content': f'未知錯誤: {str(e)}'}, ensure_ascii=False)}\n\n"
|
||||
|
||||
# 4. 結束標記
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
generate_thinking_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no", # 禁用 Nginx 緩衝
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user