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:
OG T
2026-03-22 18:57:44 +08:00
parent a840bf975b
commit 196d269b92
245 changed files with 42207 additions and 6 deletions

View 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 緩衝
},
)