├── ui ├── src │ ├── vite-env.d.ts │ ├── lib │ │ ├── utils.ts │ │ ├── mockData.ts │ │ ├── config.ts │ │ ├── auth.ts │ │ └── types.ts │ ├── store │ │ ├── hooks.ts │ │ └── index.ts │ ├── main.tsx │ ├── App.css │ ├── components │ │ ├── ui │ │ │ ├── badge.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── button.tsx │ │ │ └── card.tsx │ │ ├── panel-section.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── conversation-context.tsx │ │ ├── ProtectedRoute.tsx │ │ ├── agent-panel.tsx │ │ ├── agents-list.tsx │ │ ├── person-data-panel.tsx │ │ ├── guardrails.tsx │ │ └── runner-output.tsx │ ├── App.tsx │ ├── index.css │ ├── hooks │ │ ├── useWebSocket.ts │ │ └── useAuthInit.ts │ └── assets │ │ └── react.svg ├── postcss.config.js ├── tsconfig.json ├── index.html ├── eslint.config.js ├── tsconfig.node.json ├── tsconfig.app.json ├── public │ ├── ai-assistant.svg │ └── vite.svg ├── vite.config.ts ├── package.json ├── tailwind.config.js └── README.md ├── backend ├── core │ ├── http_core │ │ ├── __init__.py │ │ └── client.py │ ├── database_core │ │ ├── __init__.py │ │ ├── simple_test.py │ │ ├── config.py │ │ ├── init_db.py │ │ └── models.py │ ├── vector_core │ │ ├── __init__.py │ │ ├── config.py │ │ ├── models.py │ │ ├── utils.py │ │ └── README.md │ ├── auth_core │ │ ├── __init__.py │ │ └── middleware.py │ └── web_socket_core │ │ └── __init__.py ├── remote_api │ ├── news │ │ └── __init__.py │ ├── recipe │ │ ├── __init__.py │ │ ├── models.py │ │ └── client.py │ ├── weather │ │ ├── __init__.py │ │ └── client.py │ └── jsonplaceholder │ │ ├── __init__.py │ │ └── client.py ├── service │ ├── models │ │ ├── __init__.py │ │ ├── user_preference.py │ │ ├── note.py │ │ ├── todo.py │ │ └── conversation.py │ ├── services │ │ ├── __init__.py │ │ └── user_service.py │ └── __init__.py ├── api │ ├── __init__.py │ ├── admin_api.py │ └── auth_api.py ├── env.example ├── mcp-serve │ ├── __init__.py │ └── mcp_server.py ├── requirements.txt └── agent │ └── example_usage.py ├── Dockerfile.frontend ├── .dockerignore ├── Dockerfile ├── zeabur.json ├── docker-compose.yml ├── nginx.conf ├── technical_architecture_guide.md ├── README_GUIDE.md ├── init.sql ├── .gitignore └── 部署说明.md /ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /backend/core/http_core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | HTTP Core module for common HTTP client functionality. 3 | """ 4 | 5 | from .client import APIClient 6 | 7 | __all__ = ['APIClient'] -------------------------------------------------------------------------------- /backend/remote_api/news/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | News API Module 3 | 4 | Author: Andrew Wang 5 | """ 6 | 7 | from .client import NewsClient 8 | from .models import * 9 | 10 | __all__ = ['NewsClient'] -------------------------------------------------------------------------------- /ui/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } -------------------------------------------------------------------------------- /backend/remote_api/recipe/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Recipe API Module 3 | 4 | Author: Andrew Wang 5 | """ 6 | 7 | from .client import RecipeClient 8 | from .models import * 9 | 10 | __all__ = ['RecipeClient'] -------------------------------------------------------------------------------- /backend/remote_api/weather/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Weather API Module 3 | 4 | Author: Andrew Wang 5 | """ 6 | 7 | from .client import WeatherClient 8 | from .models import * 9 | 10 | __all__ = ['WeatherClient'] -------------------------------------------------------------------------------- /backend/remote_api/jsonplaceholder/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | JSONPlaceholder API Module 3 | 4 | Author: Andrew Wang 5 | """ 6 | 7 | from .client import JSONPlaceholderClient 8 | from .models import * 9 | 10 | __all__ = ['JSONPlaceholderClient'] -------------------------------------------------------------------------------- /Dockerfile.frontend: -------------------------------------------------------------------------------- 1 | # 前端开发环境 Dockerfile 2 | FROM node:20-alpine 3 | 4 | WORKDIR /app 5 | 6 | # 复制依赖文件 7 | COPY ui/package*.json ./ 8 | 9 | # 安装依赖 10 | RUN npm ci 11 | 12 | # 复制源代码 13 | COPY ui/ . 14 | 15 | # 暴露端口 16 | EXPOSE 3000 17 | 18 | # 启动开发服务器 19 | CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] -------------------------------------------------------------------------------- /backend/core/database_core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Database Core module for MySQL database operations. 3 | """ 4 | 5 | from .client import DatabaseClient 6 | from .models import BaseModel 7 | from .config import DatabaseConfig 8 | from .utils import DatabaseUtils 9 | 10 | __all__ = ['DatabaseClient', 'BaseModel', 'DatabaseConfig', 'DatabaseUtils'] -------------------------------------------------------------------------------- /ui/src/store/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import type { TypedUseSelectorHook } from 'react-redux'; 3 | import type { RootState, AppDispatch } from './index'; 4 | 5 | // 使用预定义的类型 6 | export const useAppDispatch = () => useDispatch(); 7 | export const useAppSelector: TypedUseSelectorHook = useSelector; -------------------------------------------------------------------------------- /backend/service/models/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 数据模型层 3 | 4 | 定义用户偏好设置、笔记、待办事项等数据模型 5 | """ 6 | 7 | from .user_preference import UserPreference 8 | from .note import Note 9 | from .todo import Todo 10 | from .conversation import Conversation 11 | from .chat_message import ChatMessage 12 | 13 | __all__ = [ 14 | 'UserPreference', 15 | 'Note', 16 | 'Todo', 17 | 'Conversation', 18 | 'ChatMessage' 19 | ] -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AI个人助手 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ui/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import authReducer from './slices/authSlice'; 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | auth: authReducer, 7 | }, 8 | middleware: (getDefaultMiddleware) => 9 | getDefaultMiddleware({ 10 | serializableCheck: { 11 | ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'], 12 | }, 13 | }), 14 | }); 15 | 16 | export type RootState = ReturnType; 17 | export type AppDispatch = typeof store.dispatch; -------------------------------------------------------------------------------- /ui/src/lib/mockData.ts: -------------------------------------------------------------------------------- 1 | // Mock数据定义 2 | export const mockChatResponse = { 3 | conversation_id: 'mock-conversation-1', 4 | current_agent: 'Customer Service Agent', 5 | messages: [], 6 | events: [], 7 | context: {}, 8 | agents: [], 9 | guardrails: [], 10 | raw_response: '', 11 | is_finished: false 12 | }; 13 | 14 | export const mockEvents = [ 15 | { 16 | id: 'mock-event-1', 17 | type: 'message', 18 | agent: 'Customer Service Agent', 19 | content: 'Mock event content', 20 | timestamp: new Date(), 21 | metadata: {} 22 | } 23 | ]; -------------------------------------------------------------------------------- /backend/service/services/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 业务服务层 3 | 4 | 提供用户、偏好设置、笔记、待办事项等业务服务 5 | """ 6 | 7 | from .user_service import UserService 8 | from .preference_service import PreferenceService 9 | from .note_service import NoteService 10 | from .todo_service import TodoService 11 | from .conversation_service import ConversationService 12 | from .chat_message_service import ChatMessageService 13 | 14 | __all__ = [ 15 | 'UserService', 16 | 'PreferenceService', 17 | 'NoteService', 18 | 'TodoService', 19 | 'ConversationService', 20 | 'ChatMessageService' 21 | ] -------------------------------------------------------------------------------- /backend/api/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | API模块 3 | 4 | 包含所有按功能分类的API端点 5 | """ 6 | 7 | from .auth_api import auth_router 8 | from .admin_api import admin_router 9 | from .conversation_api import conversation_router 10 | from .websocket_api import websocket_router 11 | from .system_api import system_router 12 | from .note_api import note_router 13 | from .todo_api import todo_router 14 | 15 | __all__ = [ 16 | "auth_router", 17 | "admin_router", 18 | "conversation_router", 19 | "websocket_router", 20 | "system_router", 21 | "note_router", 22 | "todo_router" 23 | ] -------------------------------------------------------------------------------- /backend/env.example: -------------------------------------------------------------------------------- 1 | # AI个人日常助手 - 环境变量配置示例 2 | # 复制此文件为 .env 并填写真实的配置值 3 | 4 | # OpenAI API 配置 5 | OPENAI_API_KEY=your_openai_api_key_here 6 | OPENAI_CHAT_MODEL=gpt-3.5-turbo 7 | OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 8 | 9 | # 新闻API配置 10 | NEWS_API_TOKEN=your_news_api_token_here 11 | 12 | # 数据库配置 13 | DB_HOST=mysql 14 | DB_PORT=3306 15 | DB_USERNAME=root 16 | DB_PASSWORD=password 17 | DB_DATABASE=ai_assistant 18 | DB_CHARSET=utf8mb4 19 | 20 | # ChromaDB向量数据库配置 21 | CHROMA_CLIENT_MODE=http 22 | CHROMA_HOST=chromadb 23 | CHROMA_PORT=8001 24 | 25 | # 应用配置 26 | NODE_ENV=production -------------------------------------------------------------------------------- /backend/service/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Service层 - 业务逻辑层 3 | 4 | 提供用户偏好设置、笔记、待办事项等业务服务 5 | """ 6 | 7 | from .services.user_service import UserService 8 | from .services.preference_service import PreferenceService 9 | from .services.note_service import NoteService 10 | from .services.todo_service import TodoService 11 | from .services.conversation_service import ConversationService 12 | from .services.chat_message_service import ChatMessageService 13 | 14 | __all__ = [ 15 | 'UserService', 16 | 'PreferenceService', 17 | 'NoteService', 18 | 'TodoService', 19 | 'ConversationService', 20 | 'ChatMessageService' 21 | ] -------------------------------------------------------------------------------- /ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { Provider } from 'react-redux' 4 | import { BrowserRouter } from 'react-router-dom' 5 | import './index.css' 6 | import App from './App.tsx' 7 | import { store } from './store' 8 | import { ToastProvider } from './components/ui/toast' 9 | 10 | createRoot(document.getElementById('root')!).render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | , 20 | ) 21 | -------------------------------------------------------------------------------- /backend/core/vector_core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Vector Core Module for Chroma Database with OpenAI Embeddings 3 | """ 4 | 5 | from .client import ChromaVectorClient 6 | from .config import VectorConfig 7 | from .models import VectorDocument, VectorQuery, VectorQueryResult, VectorDeleteFilter, VectorStats 8 | from .utils import create_collection_name, validate_metadata, generate_document_id 9 | 10 | __all__ = [ 11 | "ChromaVectorClient", 12 | "VectorConfig", 13 | "VectorDocument", 14 | "VectorQuery", 15 | "VectorQueryResult", 16 | "VectorDeleteFilter", 17 | "VectorStats", 18 | "create_collection_name", 19 | "validate_metadata", 20 | "generate_document_id" 21 | ] -------------------------------------------------------------------------------- /ui/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { globalIgnores } from 'eslint/config' 7 | 8 | export default tseslint.config([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs['recommended-latest'], 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /backend/mcp-serve/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCP Service Module Package 3 | AI Personal Daily Assistant MCP Tools Service 4 | 5 | Author: Andrew Wang 6 | """ 7 | 8 | __version__ = "1.0.0" 9 | __author__ = "Andrew Wang" 10 | __description__ = "MCP Tools Service Module" 11 | 12 | # Export main modules 13 | from .mcp_server import create_mcp_server, main 14 | from .weather_tools import register_weather_tools 15 | from .news_tools import register_news_tools 16 | from .recipe_tools import register_recipe_tools 17 | from .data_tools import register_data_tools 18 | 19 | __all__ = [ 20 | "create_mcp_server", 21 | "main", 22 | "register_weather_tools", 23 | "register_news_tools", 24 | "register_recipe_tools", 25 | "register_data_tools" 26 | ] -------------------------------------------------------------------------------- /ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | width: 100%; 3 | height: 100vh; 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | } 8 | 9 | .logo { 10 | height: 6em; 11 | padding: 1.5em; 12 | will-change: filter; 13 | transition: filter 300ms; 14 | } 15 | .logo:hover { 16 | filter: drop-shadow(0 0 2em #646cffaa); 17 | } 18 | .logo.react:hover { 19 | filter: drop-shadow(0 0 2em #61dafbaa); 20 | } 21 | 22 | @keyframes logo-spin { 23 | from { 24 | transform: rotate(0deg); 25 | } 26 | to { 27 | transform: rotate(360deg); 28 | } 29 | } 30 | 31 | @media (prefers-reduced-motion: no-preference) { 32 | a:nth-of-type(2) .logo { 33 | animation: logo-spin infinite 20s linear; 34 | } 35 | } 36 | 37 | .card { 38 | padding: 2em; 39 | } 40 | 41 | .read-the-docs { 42 | color: #888; 43 | } 44 | -------------------------------------------------------------------------------- /ui/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": false, 22 | "erasableSyntaxOnly": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /ui/public/ai-assistant.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | openai-agents 2 | openai-agents[litellm] 3 | fastmcp 4 | requests 5 | python-dotenv 6 | fastapi 7 | uvicorn[standard] 8 | websockets 9 | pydantic>=2.0.0 10 | typing-extensions 11 | sqlalchemy>=2.0.0 12 | pymysql>=1.0.0 13 | cryptography>=3.4.0 14 | alembic>=1.12.0 15 | 16 | # Vector Database Dependencies (OpenAI only) 17 | chromadb==1.0.15 18 | numpy>=1.26.0 19 | openai>=1.0.0 20 | # Add required dependencies for the newer version of ChromaDB 21 | #duckdb<0.10.4 22 | pyarrow 23 | pydantic<3 24 | # bcrypt is needed for newer Chroma versions 25 | bcrypt 26 | # protobuf is sometimes needed for newer Chroma versions 27 | protobuf==4.25.3 28 | # opentelemetry-api, opentelemetry-exporter-otlp-proto-http, opentelemetry-sdk are needed by chromadb 29 | opentelemetry-api 30 | opentelemetry-exporter-otlp-proto-http 31 | opentelemetry-sdk 32 | posthog 33 | 34 | # JWT Authentication Dependencies 35 | python-jose[cryptography] 36 | passlib[bcrypt] 37 | 38 | # Testing Dependencies 39 | requests>=2.31.0 40 | websockets>=11.0.0 41 | python-socks[asyncio]>=2.3.0 42 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Python 2 | backend/__pycache__/ 3 | backend/*.pyc 4 | backend/*.pyo 5 | backend/*.pyd 6 | backend/.Python 7 | backend/env/ 8 | backend/venv/ 9 | backend/.venv/ 10 | backend/.env 11 | backend/pip-log.txt 12 | backend/pip-delete-this-directory.txt 13 | backend/.pytest_cache/ 14 | backend/htmlcov/ 15 | backend/.coverage 16 | backend/.coverage.* 17 | backend/coverage.xml 18 | backend/*.cover 19 | backend/.hypothesis/ 20 | 21 | # Node.js 22 | ui/node_modules/ 23 | ui/.npm 24 | ui/.eslintcache 25 | ui/npm-debug.log* 26 | ui/yarn-debug.log* 27 | ui/yarn-error.log* 28 | 29 | # IDE 30 | .vscode/ 31 | .idea/ 32 | *.swp 33 | *.swo 34 | *~ 35 | 36 | # OS 37 | .DS_Store 38 | .DS_Store? 39 | ._* 40 | .Spotlight-V100 41 | .Trashes 42 | ehthumbs.db 43 | Thumbs.db 44 | 45 | # Git 46 | .git/ 47 | .gitignore 48 | 49 | # Logs 50 | *.log 51 | logs/ 52 | 53 | # Runtime data 54 | pids/ 55 | *.pid 56 | *.seed 57 | *.pid.lock 58 | 59 | # Documentation 60 | README*.md 61 | *.md 62 | docs/ 63 | 64 | # Test files 65 | *test*/ 66 | *__test__*/ 67 | *.test.* 68 | *.spec.* 69 | 70 | # Development files 71 | .env.local 72 | .env.development 73 | .env.test 74 | docker-compose.override.yml -------------------------------------------------------------------------------- /ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | port: 3000, 9 | proxy: { 10 | '/api': { 11 | target: 'http://localhost:8000', 12 | changeOrigin: true, 13 | secure: false, 14 | timeout: 10000, 15 | headers: { 16 | 'Access-Control-Allow-Origin': '*', 17 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 18 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 19 | } 20 | }, 21 | '/ws': { 22 | target: 'ws://localhost:8000', 23 | ws: true, 24 | changeOrigin: true, 25 | secure: false, 26 | timeout: 10000 27 | } 28 | } 29 | }, 30 | build: { 31 | outDir: 'dist', 32 | sourcemap: false, 33 | minify: true, 34 | target: 'es2015', 35 | chunkSizeWarningLimit: 2000, 36 | rollupOptions: { 37 | output: { 38 | manualChunks: { 39 | vendor: ['react', 'react-dom'], 40 | router: ['react-router-dom'] 41 | } 42 | } 43 | } 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /ui/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "../../lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; -------------------------------------------------------------------------------- /ui/src/components/panel-section.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronDown, ChevronUp } from "lucide-react"; 2 | import { useState } from "react"; 3 | 4 | interface PanelSectionProps { 5 | title: string; 6 | children: React.ReactNode; 7 | defaultOpen?: boolean; 8 | collapsible?: boolean; 9 | } 10 | 11 | export function PanelSection({ 12 | title, 13 | children, 14 | defaultOpen = true, 15 | collapsible = true, 16 | }: PanelSectionProps) { 17 | const [isOpen, setIsOpen] = useState(defaultOpen); 18 | 19 | return ( 20 |
21 |
collapsible && setIsOpen(!isOpen)} 26 | > 27 |

{title}

28 | {collapsible && ( 29 | 36 | )} 37 |
38 | {isOpen &&
{children}
} 39 |
40 | ); 41 | } -------------------------------------------------------------------------------- /ui/src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, type ErrorInfo, type ReactNode } from 'react'; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | } 6 | 7 | interface State { 8 | hasError: boolean; 9 | error?: Error; 10 | } 11 | 12 | class ErrorBoundary extends Component { 13 | public state: State = { 14 | hasError: false, 15 | }; 16 | 17 | public static getDerivedStateFromError(error: Error): State { 18 | return { hasError: true, error }; 19 | } 20 | 21 | public componentDidCatch(error: Error, errorInfo: ErrorInfo) { 22 | console.error("Uncaught error:", error, errorInfo); 23 | } 24 | 25 | public render() { 26 | if (this.state.hasError) { 27 | return ( 28 |
29 |

糟糕,渲染时出错了

30 |

我们已经记录了这个问题,请尝试刷新页面。

31 | {this.state.error && ( 32 |
33 |               {this.state.error.toString()}
34 |             
35 | )} 36 |
37 | ); 38 | } 39 | 40 | return this.props.children; 41 | } 42 | } 43 | 44 | export default ErrorBoundary; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 多阶段构建 Dockerfile 2 | # 阶段1: 构建前端 3 | FROM node:20-alpine AS frontend-builder 4 | 5 | # 设置工作目录 6 | WORKDIR /app/ui 7 | 8 | # 安装必要的系统依赖 9 | RUN apk add --no-cache python3 make g++ 10 | 11 | # 复制前端依赖文件 12 | COPY ui/package*.json ./ 13 | 14 | # 清理 npm 缓存并安装所有依赖 15 | RUN npm cache clean --force && \ 16 | npm ci --no-audit --no-fund && \ 17 | npm ls vite 18 | 19 | # 复制前端源代码 20 | COPY ui/ . 21 | 22 | # 构建前端应用(跳过 TypeScript 检查以避免生产构建失败) 23 | RUN npm run build:prod || npm run build:simple 24 | 25 | # 验证构建结果 26 | RUN ls -la dist/ 27 | 28 | # 阶段2: 构建后端 29 | FROM python:3.11-slim AS backend 30 | 31 | # 设置工作目录 32 | WORKDIR /app 33 | 34 | # 安装系统依赖 35 | RUN apt-get update && apt-get install -y \ 36 | gcc \ 37 | g++ \ 38 | curl \ 39 | && rm -rf /var/lib/apt/lists/* 40 | 41 | # 复制并安装Python依赖 42 | COPY backend/requirements.txt . 43 | RUN pip install --no-cache-dir -r requirements.txt 44 | 45 | # 复制后端代码 46 | COPY backend/ . 47 | 48 | # 从前端构建阶段复制构建产物 49 | COPY --from=frontend-builder /app/ui/dist ./static 50 | 51 | # 创建必要的目录 52 | RUN mkdir -p logs data 53 | 54 | # 暴露端口 55 | EXPOSE 8000 56 | 57 | # 设置环境变量 58 | ENV PYTHONPATH=/app 59 | ENV PYTHONUNBUFFERED=1 60 | 61 | # 健康检查 62 | HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ 63 | CMD curl -f http://localhost:8000/api/health || exit 1 64 | 65 | # 启动命令 66 | CMD ["python", "main.py"] -------------------------------------------------------------------------------- /ui/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/components/conversation-context.tsx: -------------------------------------------------------------------------------- 1 | import { Database } from "lucide-react"; 2 | 3 | interface ConversationContextProps { 4 | context: Record; 5 | } 6 | 7 | export function ConversationContext({ context }: ConversationContextProps) { 8 | const contextEntries = Object.entries(context).filter(([_, value]) => value); 9 | 10 | return ( 11 |
12 |
13 | 14 |

15 | 对话上下文 16 |

17 |
18 | 19 | {contextEntries.length === 0 && ( 20 |
21 | 暂无上下文信息 22 |
23 | )} 24 | 25 |
26 |
27 | {contextEntries.map(([key, value], index) => ( 28 |
29 | 30 | {key.replace(/_/g, ' ')}: 31 | 32 | 33 | {typeof value === 'string' ? value : JSON.stringify(value)} 34 | 35 |
36 | ))} 37 |
38 |
39 | 40 |
41 | 上下文信息会在对话过程中动态更新 42 |
43 |
44 | ); 45 | } -------------------------------------------------------------------------------- /backend/service/models/user_preference.py: -------------------------------------------------------------------------------- 1 | """ 2 | 用户偏好设置数据模型 3 | """ 4 | 5 | from sqlalchemy import Column, Integer, String, Text, DateTime, Index, UniqueConstraint 6 | from sqlalchemy.sql import func 7 | from core.database_core import BaseModel 8 | 9 | 10 | class UserPreference(BaseModel): 11 | """ 12 | 用户偏好设置模型 13 | 14 | 存储用户的个人偏好设置,用户信息来自JSONPlaceholder API 15 | """ 16 | __tablename__ = 'user_preferences' 17 | 18 | # 用户ID(来自JSONPlaceholder API,不存储用户信息) 19 | user_id = Column(Integer, nullable=False, comment='用户ID(来自JSONPlaceholder)') 20 | 21 | # 偏好设置内容(JSON字符串) 22 | preferences = Column(Text, nullable=False, comment='偏好设置内容(JSON字符串)') 23 | 24 | # 偏好设置类型/分类(可选) 25 | category = Column(String(50), default='general', comment='偏好设置类型') 26 | 27 | # 最后更新时间(自动更新) 28 | last_updated = Column(DateTime, default=func.now(), onupdate=func.now(), comment='最后更新时间') 29 | 30 | # 创建索引和唯一约束 31 | __table_args__ = ( 32 | UniqueConstraint('user_id', 'category', name='uk_user_category'), 33 | Index('idx_user_preferences_user_id', 'user_id'), 34 | Index('idx_user_preferences_category', 'category'), 35 | Index('idx_user_preferences_last_updated', 'last_updated'), 36 | ) 37 | 38 | def __repr__(self): 39 | return f"" 40 | 41 | def to_dict(self): 42 | """转换为字典格式""" 43 | # 直接使用基类的to_dict方法,它已经安全处理了datetime字段 44 | return super().to_dict() 45 | 46 | @classmethod 47 | def create_from_dict(cls, data): 48 | """从字典创建实例""" 49 | return cls( 50 | user_id=data.get('user_id'), 51 | preferences=data.get('preferences', '{}'), 52 | category=data.get('category', 'general') 53 | ) -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "build:prod": "vite build --mode production", 10 | "build:simple": "vite build --mode production --minify false", 11 | "build:check": "tsc -b --noEmit && vite build", 12 | "lint": "eslint .", 13 | "preview": "vite preview", 14 | "test:ws": "node test-websocket.js" 15 | }, 16 | "dependencies": { 17 | "@radix-ui/react-scroll-area": "^1.2.9", 18 | "@radix-ui/react-slot": "^1.1.2", 19 | "@reduxjs/toolkit": "^2.8.2", 20 | "axios": "^1.10.0", 21 | "class-variance-authority": "^0.7.1", 22 | "clsx": "^2.1.1", 23 | "lucide-react": "^0.484.0", 24 | "openai": "^4.87.3", 25 | "react": "^19.1.0", 26 | "react-dom": "^19.1.0", 27 | "react-hook-form": "^7.60.0", 28 | "react-markdown": "^10.1.0", 29 | "react-redux": "^9.2.0", 30 | "react-router-dom": "^7.6.3", 31 | "react-syntax-highlighter": "^15.6.1", 32 | "tailwind-merge": "^3.0.2", 33 | "tailwindcss-animate": "^1.0.7", 34 | "wavtools": "^0.1.5" 35 | }, 36 | "devDependencies": { 37 | "@eslint/js": "^9.30.1", 38 | "@types/node": "^22", 39 | "@types/react": "^19.1.8", 40 | "@types/react-dom": "^19.1.6", 41 | "@types/react-syntax-highlighter": "^15.5.0", 42 | "@vitejs/plugin-react": "^4.6.0", 43 | "autoprefixer": "^10.4.20", 44 | "eslint": "^9.30.1", 45 | "eslint-plugin-react-hooks": "^5.2.0", 46 | "eslint-plugin-react-refresh": "^0.4.20", 47 | "globals": "^16.3.0", 48 | "postcss": "^8.4.49", 49 | "tailwindcss": "^3.4.17", 50 | "typescript": "~5.8.3", 51 | "typescript-eslint": "^8.35.1", 52 | "vite": "^7.0.4", 53 | "ws": "^8.18.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ui/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 3 | 4 | import { cn } from "../../lib/utils" 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 15 | 16 | {children} 17 | 18 | 19 | 20 | 21 | )) 22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 23 | 24 | const ScrollBar = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, orientation = "vertical", ...props }, ref) => ( 28 | 41 | 42 | 43 | )) 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 45 | 46 | export { ScrollArea, ScrollBar } -------------------------------------------------------------------------------- /backend/api/admin_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | 管理员API模块 3 | 4 | 包含缓存管理等管理员功能的API端点 5 | """ 6 | 7 | import logging 8 | from typing import Dict, Any 9 | from fastapi import APIRouter 10 | 11 | # 导入认证核心模块 12 | from core.auth_core import CurrentUser 13 | 14 | # 导入服务管理器 15 | from service.service_manager import service_manager 16 | 17 | # 配置日志 18 | logger = logging.getLogger(__name__) 19 | 20 | # 创建管理员API路由器 21 | admin_router = APIRouter(prefix="/admin", tags=["管理员"]) 22 | 23 | # ========================= 24 | # API端点 25 | # ========================= 26 | 27 | @admin_router.post("/cache/clear") 28 | async def clear_cache(current_user: Dict[str, Any] = CurrentUser): 29 | """清理所有缓存(管理员功能)""" 30 | try: 31 | # 清理服务管理器缓存 32 | service_manager.clear_cache() 33 | 34 | # 获取清理后的统计信息 35 | stats = service_manager.get_stats() 36 | 37 | return { 38 | "success": True, 39 | "message": "缓存已清理", 40 | "stats": stats 41 | } 42 | except Exception as e: 43 | logger.error(f"清理缓存失败: {e}") 44 | return { 45 | "success": False, 46 | "message": f"清理缓存失败: {str(e)}" 47 | } 48 | 49 | 50 | @admin_router.post("/cache/cleanup") 51 | async def cleanup_expired_cache(current_user: Dict[str, Any] = CurrentUser): 52 | """清理过期缓存(管理员功能)""" 53 | try: 54 | # 清理过期缓存 55 | service_manager.clear_expired_cache() 56 | 57 | # 获取清理后的统计信息 58 | stats = service_manager.get_stats() 59 | 60 | return { 61 | "success": True, 62 | "message": "过期缓存已清理", 63 | "stats": stats 64 | } 65 | except Exception as e: 66 | logger.error(f"清理过期缓存失败: {e}") 67 | return { 68 | "success": False, 69 | "message": f"清理过期缓存失败: {str(e)}" 70 | } -------------------------------------------------------------------------------- /ui/src/components/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Navigate, useLocation } from 'react-router-dom'; 3 | import { useAppSelector, useAppDispatch } from '../store/hooks'; 4 | import { getCurrentUser } from '../store/slices/authSlice'; 5 | import type { ProtectedRouteProps } from '../lib/types'; 6 | import { Loader2 } from 'lucide-react'; 7 | 8 | const ProtectedRoute: React.FC = ({ 9 | children, 10 | requireAuth = true 11 | }) => { 12 | const dispatch = useAppDispatch(); 13 | const location = useLocation(); 14 | const { isAuthenticated, isLoading, token, user } = useAppSelector((state) => state.auth); 15 | 16 | // 如果有token但没有用户信息,尝试获取用户信息 17 | useEffect(() => { 18 | if (requireAuth && token && !user && !isLoading && !isAuthenticated) { 19 | console.log('📱 ProtectedRoute: 有token但无用户信息,尝试获取用户信息...'); 20 | dispatch(getCurrentUser()); 21 | } 22 | }, [requireAuth, token, user, isLoading, isAuthenticated, dispatch]); 23 | 24 | // 正在加载中 25 | if (isLoading) { 26 | return ( 27 |
28 |
29 | 30 |

正在验证身份...

31 |
32 |
33 | ); 34 | } 35 | 36 | // 需要认证但未认证,重定向到登录页面 37 | if (requireAuth && !isAuthenticated) { 38 | console.log('🚫 ProtectedRoute: 需要认证但未认证,重定向到登录页面'); 39 | return ; 40 | } 41 | 42 | // 已认证但访问登录页面,重定向到主页 43 | if (!requireAuth && isAuthenticated && location.pathname === '/login') { 44 | console.log('🏠 ProtectedRoute: 已认证用户访问登录页面,重定向到主页'); 45 | return ; 46 | } 47 | 48 | return <>{children}; 49 | }; 50 | 51 | export default ProtectedRoute; -------------------------------------------------------------------------------- /ui/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "../../lib/utils"; 5 | 6 | const buttonVariants = cva( 7 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-9 px-4 py-2", 24 | sm: "h-8 rounded-md px-3 text-xs", 25 | lg: "h-10 rounded-md px-8", 26 | icon: "h-9 w-9", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | return ( 45 | 53 | 64 |
65 | 66 | 67 | {/* 内容区域 */} 68 |
69 | {activeTab === 'notes' && } 70 | {activeTab === 'todos' && } 71 |
72 | 73 | ); 74 | } -------------------------------------------------------------------------------- /ui/src/components/guardrails.tsx: -------------------------------------------------------------------------------- 1 | import { Shield, CheckCircle, XCircle } from "lucide-react"; 2 | import type { GuardrailCheck } from "../lib/types"; 3 | 4 | interface GuardrailsProps { 5 | guardrails: GuardrailCheck[]; 6 | inputGuardrails: string[]; 7 | } 8 | 9 | export function Guardrails({ guardrails, inputGuardrails }: GuardrailsProps) { 10 | const activeGuardrails = guardrails.filter(g => 11 | inputGuardrails.includes(g.name) || inputGuardrails.includes(g.id) 12 | ); 13 | 14 | return ( 15 |
16 |
17 | 18 |

19 | 安全护栏检查 20 |

21 |
22 | 23 | {activeGuardrails.length === 0 && ( 24 |
25 | 当前代理没有激活的护栏检查 26 |
27 | )} 28 | 29 | {activeGuardrails.map((check) => ( 30 |
38 |
39 | {check.passed ? ( 40 | 41 | ) : ( 42 | 43 | )} 44 |
45 |

46 | {check.name} 47 |

48 |

49 | {check.reasoning} 50 |

51 |
52 | 输入: {check.input} 53 |
54 |
55 |
56 |
57 | 检查时间: {check.timestamp.toLocaleTimeString()} 58 |
59 |
60 | ))} 61 | 62 | {guardrails.length > activeGuardrails.length && ( 63 |
64 |

65 | 所有护栏检查历史: 66 |

67 |
68 | {guardrails.map((check) => ( 69 |
73 |
74 | {check.passed ? ( 75 | 76 | ) : ( 77 | 78 | )} 79 | {check.name} 80 | 81 | {check.timestamp.toLocaleTimeString()} 82 | 83 |
84 |
85 | ))} 86 |
87 |
88 | )} 89 |
90 | ); 91 | } -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | include /etc/nginx/mime.types; 7 | default_type application/octet-stream; 8 | 9 | # 日志格式 10 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 11 | '$status $body_bytes_sent "$http_referer" ' 12 | '"$http_user_agent" "$http_x_forwarded_for"'; 13 | 14 | access_log /var/log/nginx/access.log main; 15 | error_log /var/log/nginx/error.log; 16 | 17 | # 基本设置 18 | sendfile on; 19 | tcp_nopush on; 20 | tcp_nodelay on; 21 | keepalive_timeout 65; 22 | types_hash_max_size 2048; 23 | 24 | # 压缩设置 25 | gzip on; 26 | gzip_vary on; 27 | gzip_min_length 10240; 28 | gzip_proxied any; 29 | gzip_comp_level 6; 30 | gzip_types 31 | text/plain 32 | text/css 33 | text/xml 34 | text/javascript 35 | application/json 36 | application/javascript 37 | application/xml+rss 38 | application/atom+xml 39 | image/svg+xml; 40 | 41 | # WebSocket 升级设置 42 | map $http_upgrade $connection_upgrade { 43 | default upgrade; 44 | '' close; 45 | } 46 | 47 | # 上游服务器 48 | upstream app_backend { 49 | server app:8000; 50 | } 51 | 52 | server { 53 | listen 80; 54 | server_name _; 55 | 56 | # 设置最大文件上传大小 57 | client_max_body_size 50M; 58 | 59 | # 根路径处理 60 | location / { 61 | proxy_pass http://app_backend; 62 | proxy_set_header Host $host; 63 | proxy_set_header X-Real-IP $remote_addr; 64 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 65 | proxy_set_header X-Forwarded-Proto $scheme; 66 | 67 | # 超时设置 68 | proxy_connect_timeout 60s; 69 | proxy_send_timeout 60s; 70 | proxy_read_timeout 60s; 71 | } 72 | 73 | # API 路由 74 | location /api/ { 75 | proxy_pass http://app_backend; 76 | proxy_set_header Host $host; 77 | proxy_set_header X-Real-IP $remote_addr; 78 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 79 | proxy_set_header X-Forwarded-Proto $scheme; 80 | } 81 | 82 | # WebSocket 路由 83 | location /ws { 84 | proxy_pass http://app_backend; 85 | proxy_http_version 1.1; 86 | proxy_set_header Upgrade $http_upgrade; 87 | proxy_set_header Connection $connection_upgrade; 88 | proxy_set_header Host $host; 89 | proxy_set_header X-Real-IP $remote_addr; 90 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 91 | proxy_set_header X-Forwarded-Proto $scheme; 92 | 93 | # WebSocket 特殊设置 94 | proxy_cache_bypass $http_upgrade; 95 | proxy_read_timeout 86400; 96 | } 97 | 98 | # 静态文件缓存 99 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { 100 | proxy_pass http://app_backend; 101 | proxy_set_header Host $host; 102 | expires 1y; 103 | add_header Cache-Control "public, immutable"; 104 | } 105 | 106 | # 健康检查 107 | location /health { 108 | proxy_pass http://app_backend/api/health; 109 | access_log off; 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /backend/service/models/note.py: -------------------------------------------------------------------------------- 1 | """ 2 | 笔记数据模型 3 | """ 4 | 5 | from sqlalchemy import Column, Integer, String, Text, DateTime, Index 6 | from sqlalchemy.sql import func 7 | from core.database_core import BaseModel 8 | 9 | 10 | class InvalidTagError(Exception): 11 | """标签验证异常""" 12 | pass 13 | 14 | 15 | class Note(BaseModel): 16 | """ 17 | 笔记模型 18 | 19 | 存储用户的笔记内容,用户信息来自JSONPlaceholder API 20 | """ 21 | __tablename__ = 'notes' 22 | 23 | # 允许的标签选项 24 | ALLOWED_TAGS = [ 25 | 'lifestyle tips', 26 | 'cooking advice', 27 | 'weather interpretation', 28 | 'news context' 29 | ] 30 | 31 | # 用户ID(来自JSONPlaceholder API) 32 | user_id = Column(Integer, nullable=False, comment='用户ID(来自JSONPlaceholder)') 33 | 34 | # 笔记标题 35 | title = Column(String(200), nullable=False, comment='笔记标题') 36 | 37 | # 笔记内容 38 | content = Column(Text, comment='笔记内容') 39 | 40 | # 笔记标签(单个标签,只允许指定选项) 41 | tag = Column(String(50), comment='笔记标签') 42 | 43 | # 笔记状态(草稿、已发布、已归档等) 44 | status = Column(String(20), default='draft', comment='笔记状态') 45 | 46 | # 最后更新时间 47 | last_updated = Column(DateTime, default=func.now(), onupdate=func.now(), comment='最后更新时间') 48 | 49 | # 创建索引优化查询 50 | __table_args__ = ( 51 | Index('idx_notes_user_id', 'user_id'), 52 | Index('idx_notes_title', 'title'), 53 | Index('idx_notes_status', 'status'), 54 | Index('idx_notes_tag', 'tag'), 55 | Index('idx_notes_last_updated', 'last_updated'), 56 | Index('idx_notes_user_status', 'user_id', 'status'), 57 | ) 58 | 59 | def __repr__(self): 60 | return f"" 61 | 62 | def to_dict(self): 63 | """转换为字典格式""" 64 | return super().to_dict() 65 | 66 | @classmethod 67 | def validate_tag(cls, tag): 68 | """验证标签是否有效""" 69 | if tag is None: 70 | return True # 允许空标签 71 | 72 | if tag not in cls.ALLOWED_TAGS: 73 | raise InvalidTagError( 74 | f"Invalid tag '{tag}'. Allowed tags are: {', '.join(cls.ALLOWED_TAGS)}" 75 | ) 76 | return True 77 | 78 | @classmethod 79 | def create_from_dict(cls, data): 80 | """从字典创建实例""" 81 | tag = data.get('tag', '') 82 | 83 | # 验证标签 84 | if tag: 85 | cls.validate_tag(tag) 86 | 87 | return cls( 88 | user_id=data.get('user_id'), 89 | title=data.get('title', ''), 90 | content=data.get('content', ''), 91 | tag=tag, 92 | status=data.get('status', 'draft') 93 | ) 94 | 95 | def set_tag(self, tag): 96 | """设置标签""" 97 | if tag: 98 | self.validate_tag(tag) 99 | self.tag = tag 100 | 101 | def get_tag(self): 102 | """获取标签""" 103 | return getattr(self, 'tag', None) 104 | 105 | def get_summary(self, max_length=100): 106 | """获取笔记摘要""" 107 | content_value = getattr(self, 'content', None) 108 | if not content_value: 109 | return '' 110 | 111 | # 移除多余的空白字符 112 | content = ' '.join(content_value.split()) 113 | 114 | if len(content) <= max_length: 115 | return content 116 | 117 | return content[:max_length] + '...' -------------------------------------------------------------------------------- /technical_architecture_guide.md: -------------------------------------------------------------------------------- 1 | # Technical Architecture Guide: AI-Powered Personal Daily Assistant 2 | 3 | ## System Overview 4 | 5 | The Personal Daily Assistant is a multi-agent AI system that combines several modern technologies to create an intelligent conversational interface. The system integrates the OpenAI Agents SDK for agent orchestration, Model Context Protocol (MCP) for external API access, and Retrieval-Augmented Generation (RAG) for knowledge enhancement. 6 | 7 | ## Key Components 8 | 9 | ### Multi-Agent System 10 | The system uses specialized AI agents that collaborate to handle different types of user requests. Each agent has specific capabilities and can hand off conversations to other agents when needed. The agents work together to provide comprehensive assistance across various daily life topics. 11 | 12 | ### Model Context Protocol Integration 13 | MCP servers provide standardized access to external APIs and data sources. This protocol-based approach enables clean separation between agent logic and external resource access, making the system more maintainable and scalable. 14 | 15 | ### RAG Knowledge System 16 | A knowledge base enhances agent responses with curated information about daily life topics, best practices, and contextual guidance. The system uses vector search to retrieve relevant information that agents can incorporate into their responses. 17 | 18 | ### Real-Time Interface 19 | The frontend provides a responsive chat interface that communicates with the backend through both REST APIs and WebSocket connections for real-time updates and agent status information. 20 | 21 | ## Technology Integration Points 22 | 23 | ### OpenAI Agents SDK 24 | - Agent creation and management 25 | - Handoff/Tooling mechanisms between agents 26 | - Tool integration for external capabilities 27 | - Conversation context preservation 28 | 29 | ### MCP Protocol 30 | - Server implementation for different API categories 31 | - Client usage flexible for various agents 32 | - Resource and tool definitions 33 | - Protocol message handling 34 | - Error handling and retry logic 35 | 36 | ### RAG Implementation 37 | - Vector database for semantic search 38 | - Knowledge base organization 39 | - Retrieval integration with agent responses 40 | - Content curation and management 41 | 42 | ### Frontend-Backend Communication 43 | - RESTful API endpoints for standard operations 44 | - WebSocket connections for real-time updates 45 | - State management for conversation history 46 | - Error handling and user feedback 47 | 48 | ## Development Considerations 49 | 50 | ### Agent Design 51 | Consider how to structure agent responsibilities, implement effective handoff mechanisms, and maintain conversation context across agent interactions. Think about error handling and fallback strategies when agents encounter issues. 52 | 53 | ### API Integration 54 | Plan how to organize MCP servers for different types of external resources. Consider rate limiting, error handling, and data transformation requirements for each API integration. 55 | 56 | ### Knowledge Management 57 | Design strategies for organizing and retrieving knowledge that enhances agent responses. Consider how to balance retrieval performance with response relevance and accuracy. 58 | 59 | ### User Experience 60 | Plan how to provide clear feedback about agent activities, handle real-time updates, and maintain responsive performance during complex multi-agent workflows. 61 | 62 | This guide provides the conceptual framework for understanding the system components and their interactions. The specific implementation details and architectural decisions are part of what you'll design and implement as part of this project. 63 | 64 | -------------------------------------------------------------------------------- /README_GUIDE.md: -------------------------------------------------------------------------------- 1 | # AI-Powered Personal Daily Assistant - Technical Interview Project 2 | 3 | ## Welcome! 4 | 5 | This technical interview project challenges you to build an intelligent personal assistant that helps users manage their daily activities through natural conversation. You'll demonstrate your expertise in multi-agent AI systems, modern web development, and API integration. 6 | 7 | ## What You'll Build 8 | 9 | An AI assistant system featuring: 10 | - **Real-time Chat Interface** built with React and TypeScript 11 | - **Intent Analysis** with proper and smart intent analysis logic / algorithm / agent 12 | - **Specialized AI Agents** working together using OpenAI Agents SDK 13 | - **Multi-Protocol Architecture, Building and Integration** using Model Context Protocol (MCP) for tools among the application 14 | - **Knowledge-Enhanced Responses** with RAG (Retrieval-Augmented Generation) 15 | - **Free API Integrations** for weather, recipes, news, and more 16 | 17 | ## Project Files 18 | 19 | This package contains everything you need to get started: 20 | 21 | - **`candidate_project_requirements.md`** - Complete project requirements and specifications 22 | - **`technical_architecture_guide.md`** - System overview and key concepts 23 | - **`api_documentation_guide.md`** - Comprehensive documentation for all free APIs you'll use 24 | - **`README.md`** - This file with getting started information 25 | 26 | ## Quick Start 27 | 28 | ### 1. Review the Requirements 29 | Start by reading `candidate_project_requirements.md` to understand the full scope and expectations. 30 | 31 | ### 2. Study the System Overview 32 | Review `technical_architecture_guide.md` to understand the key system components and concepts. 33 | 34 | ### 3. Explore the APIs 35 | Check `api_documentation_guide.md` for detailed information about the free APIs you'll integrate. 36 | 37 | ### 4. Set Up Your Environment 38 | You'll need: 39 | - Python 3.8+ for backend development 40 | - Node.js 16+ for frontend development 41 | - OpenAI API key (will be provided) 42 | - Git for version control 43 | 44 | ### 5. Plan Your Implementation 45 | Design your own approach and timeline for implementing the system. Consider how you'll structure the multi-agent system, organize the MCP servers, and create an effective user interface. 46 | 47 | ## Key Technologies 48 | 49 | ### Required Technologies 50 | - **OpenAI Agents SDK** - For multi-agent system implementation 51 | - **Model Context Protocol (MCP)** - For standardized API integration 52 | - **React + TypeScript** - For frontend development 53 | - **Fast API** - For backend API development 54 | - **Vector Database** - For RAG implementation (Chroma recommended) 55 | 56 | ### Free APIs Included 57 | - **Open-Meteo** - Weather data (no API key required) 58 | - **TheMealDB** - Recipe database (completely free) 59 | - **The News API** - News aggregation (free tier) 60 | - **JSONPlaceholder** - Development/testing data 61 | 62 | ## Success Criteria 63 | 64 | Your implementation will be evaluated on: 65 | - **Technical Implementation** - Proper use of required technologies 66 | - **System Integration** - How well components work together 67 | - **User Experience** - Quality of the chat interface and interactions 68 | - **Code Quality** - Organization, documentation, and best practices 69 | 70 | ## Getting Help 71 | 72 | ### Documentation Resources 73 | - OpenAI Agents SDK: https://openai.github.io/openai-agents-python/ 74 | - Model Context Protocol: https://modelcontextprotocol.io/ 75 | - All API documentation is included in this package 76 | 77 | ## Questions? 78 | 79 | If you have questions about the requirements or need clarification on any aspect of the project, don't hesitate to reach out. 80 | 81 | Good luck! We're excited to see what you build. 82 | 83 | -------------------------------------------------------------------------------- /backend/core/vector_core/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Vector Database Models 3 | """ 4 | 5 | from typing import Dict, List, Optional, Any 6 | from pydantic import BaseModel, Field 7 | from datetime import datetime 8 | 9 | 10 | class VectorDocument(BaseModel): 11 | """Vector document model for storing text with metadata""" 12 | 13 | id: str = Field(..., description="Unique document identifier") 14 | text: str = Field(..., description="Document text content") 15 | metadata: Dict[str, Any] = Field(default_factory=dict, description="Custom metadata") 16 | user_id: str = Field(..., description="User identifier for isolation") 17 | source: Optional[str] = Field(None, description="Source identifier for filtering") 18 | created_at: datetime = Field(default_factory=datetime.now, description="Creation timestamp") 19 | 20 | class Config: 21 | json_encoders = { 22 | datetime: lambda v: v.isoformat() 23 | } 24 | 25 | 26 | class VectorQuery(BaseModel): 27 | """Vector query model for searching documents""" 28 | 29 | query_text: str = Field(..., description="Query text to search for") 30 | user_id: str = Field(..., description="User identifier for isolation") 31 | limit: int = Field(10, ge=1, le=100, description="Maximum number of results") 32 | similarity_threshold: Optional[float] = Field(None, ge=0.0, le=1.0, description="Similarity threshold") 33 | metadata_filter: Optional[Dict[str, Any]] = Field(None, description="Metadata filter conditions") 34 | source_filter: Optional[str] = Field(None, description="Source filter") 35 | include_metadata: bool = Field(True, description="Whether to include metadata in results") 36 | include_distances: bool = Field(True, description="Whether to include similarity distances") 37 | 38 | 39 | class VectorQueryResult(BaseModel): 40 | """Vector query result model""" 41 | 42 | id: str = Field(..., description="Document identifier") 43 | text: str = Field(..., description="Document text content") 44 | metadata: Optional[Dict[str, Any]] = Field(None, description="Document metadata") 45 | distance: Optional[float] = Field(None, description="Similarity distance") 46 | score: Optional[float] = Field(None, description="Similarity score (1 - distance)") 47 | 48 | @classmethod 49 | def from_chroma_result( 50 | cls, 51 | document_id: str, 52 | text: str, 53 | metadata: Optional[Dict[str, Any]] = None, 54 | distance: Optional[float] = None 55 | ) -> "VectorQueryResult": 56 | """Create result from Chroma query response""" 57 | score = None 58 | if distance is not None: 59 | score = 1.0 - distance # Convert distance to similarity score 60 | 61 | return cls( 62 | id=document_id, 63 | text=text, 64 | metadata=metadata, 65 | distance=distance, 66 | score=score 67 | ) 68 | 69 | 70 | class VectorDeleteFilter(BaseModel): 71 | """Vector delete filter model""" 72 | 73 | user_id: str = Field(..., description="User identifier for isolation") 74 | source_filter: Optional[str] = Field(None, description="Source filter for deletion") 75 | metadata_filter: Optional[Dict[str, Any]] = Field(None, description="Metadata filter for deletion") 76 | document_ids: Optional[List[str]] = Field(None, description="Specific document IDs to delete") 77 | 78 | 79 | class VectorStats(BaseModel): 80 | """Vector database statistics""" 81 | 82 | total_documents: int = Field(..., description="Total number of documents") 83 | user_id: str = Field(..., description="User identifier") 84 | sources: List[str] = Field(..., description="Available sources") 85 | collection_name: str = Field(..., description="Collection name") -------------------------------------------------------------------------------- /ui/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/service/models/todo.py: -------------------------------------------------------------------------------- 1 | """ 2 | 待办事项数据模型 3 | """ 4 | 5 | from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, Index 6 | from sqlalchemy.orm import relationship 7 | from sqlalchemy.sql import func 8 | from core.database_core import BaseModel 9 | 10 | 11 | class Todo(BaseModel): 12 | """ 13 | 待办事项模型 14 | 15 | 存储用户的待办事项,可以关联到笔记 16 | """ 17 | __tablename__ = 'todos' 18 | 19 | # 用户ID(来自JSONPlaceholder API) 20 | user_id = Column(Integer, nullable=False, comment='用户ID(来自JSONPlaceholder)') 21 | 22 | # 待办事项标题 23 | title = Column(String(200), nullable=False, comment='待办事项标题') 24 | 25 | # 待办事项描述 26 | description = Column(Text, comment='待办事项描述') 27 | 28 | # 完成状态 29 | completed = Column(Boolean, default=False, comment='是否完成') 30 | 31 | # 优先级(high, medium, low) 32 | priority = Column(String(10), default='medium', comment='优先级') 33 | 34 | # 关联的笔记ID(业务逻辑关联,无外键约束) 35 | note_id = Column(Integer, comment='关联的笔记ID(业务逻辑关联)') 36 | 37 | # 截止日期 38 | due_date = Column(DateTime, comment='截止日期') 39 | 40 | # 完成时间 41 | completed_at = Column(DateTime, comment='完成时间') 42 | 43 | # 最后更新时间 44 | last_updated = Column(DateTime, default=func.now(), onupdate=func.now(), comment='最后更新时间') 45 | 46 | # 注释:移除外键关联关系,改为使用业务逻辑查询 47 | # note = relationship("Note", backref="todos") 48 | 49 | # 创建索引优化查询 50 | __table_args__ = ( 51 | Index('idx_todos_user_id', 'user_id'), 52 | Index('idx_todos_completed', 'completed'), 53 | Index('idx_todos_priority', 'priority'), 54 | Index('idx_todos_due_date', 'due_date'), 55 | Index('idx_todos_note_id', 'note_id'), 56 | Index('idx_todos_user_completed', 'user_id', 'completed'), 57 | Index('idx_todos_user_priority', 'user_id', 'priority'), 58 | ) 59 | 60 | def __repr__(self): 61 | return f"" 62 | 63 | def to_dict(self): 64 | """转换为字典格式""" 65 | return super().to_dict() 66 | 67 | @classmethod 68 | def create_from_dict(cls, data): 69 | """从字典创建实例""" 70 | return cls( 71 | user_id=data.get('user_id'), 72 | title=data.get('title', ''), 73 | description=data.get('description', ''), 74 | completed=data.get('completed', False), 75 | priority=data.get('priority', 'medium'), 76 | note_id=data.get('note_id'), 77 | due_date=data.get('due_date') 78 | ) 79 | 80 | def mark_completed(self): 81 | """标记为完成""" 82 | self.completed = True 83 | self.completed_at = func.now() 84 | 85 | def mark_pending(self): 86 | """标记为待完成""" 87 | self.completed = False 88 | self.completed_at = None 89 | 90 | def is_overdue(self): 91 | """检查是否过期""" 92 | due_date_value = getattr(self, 'due_date', None) 93 | completed_value = getattr(self, 'completed', False) 94 | 95 | if not due_date_value or completed_value: 96 | return False 97 | 98 | from datetime import datetime 99 | return datetime.now() > due_date_value 100 | 101 | def get_priority_level(self): 102 | """获取优先级数值(用于排序)""" 103 | priority_map = { 104 | 'high': 3, 105 | 'medium': 2, 106 | 'low': 1 107 | } 108 | priority_value = getattr(self, 'priority', 'medium') 109 | return priority_map.get(priority_value, 2) 110 | 111 | def get_status_display(self): 112 | """获取状态显示文本""" 113 | completed_value = getattr(self, 'completed', False) 114 | 115 | if completed_value: 116 | return '已完成' 117 | elif self.is_overdue(): 118 | return '已过期' 119 | else: 120 | return '进行中' -------------------------------------------------------------------------------- /ui/src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 认证相关的工具函数 3 | * 统一管理token的存储、获取和验证 4 | */ 5 | 6 | import type { AuthToken, User } from './types'; 7 | 8 | // 存储keys 9 | const AUTH_KEYS = { 10 | TOKEN: 'auth_token', 11 | USER: 'auth_user', 12 | EXPIRES_AT: 'auth_expires_at', 13 | } as const; 14 | 15 | /** 16 | * Token管理类 17 | */ 18 | export class AuthManager { 19 | /** 20 | * 保存认证信息 21 | */ 22 | static saveAuth(tokenData: AuthToken): void { 23 | try { 24 | localStorage.setItem(AUTH_KEYS.TOKEN, tokenData.access_token); 25 | 26 | if (tokenData.user_info) { 27 | localStorage.setItem(AUTH_KEYS.USER, JSON.stringify(tokenData.user_info)); 28 | } 29 | 30 | // 计算过期时间 31 | if (tokenData.expires_in) { 32 | const expiresAt = Date.now() + (tokenData.expires_in * 1000); 33 | localStorage.setItem(AUTH_KEYS.EXPIRES_AT, expiresAt.toString()); 34 | } 35 | 36 | console.log('✅ 认证信息已保存到localStorage'); 37 | } catch (error) { 38 | console.error('❌ 保存认证信息失败:', error); 39 | } 40 | } 41 | 42 | /** 43 | * 获取token 44 | */ 45 | static getToken(): string | null { 46 | try { 47 | return localStorage.getItem(AUTH_KEYS.TOKEN); 48 | } catch (error) { 49 | console.error('❌ 获取token失败:', error); 50 | return null; 51 | } 52 | } 53 | 54 | /** 55 | * 获取用户信息 56 | */ 57 | static getUser(): User | null { 58 | try { 59 | const userStr = localStorage.getItem(AUTH_KEYS.USER); 60 | return userStr ? JSON.parse(userStr) : null; 61 | } catch (error) { 62 | console.error('❌ 获取用户信息失败:', error); 63 | return null; 64 | } 65 | } 66 | 67 | /** 68 | * 获取token过期时间 69 | */ 70 | static getExpiresAt(): number | null { 71 | try { 72 | const expiresAtStr = localStorage.getItem(AUTH_KEYS.EXPIRES_AT); 73 | return expiresAtStr ? parseInt(expiresAtStr, 10) : null; 74 | } catch (error) { 75 | console.error('❌ 获取过期时间失败:', error); 76 | return null; 77 | } 78 | } 79 | 80 | /** 81 | * 检查token是否过期 82 | */ 83 | static isTokenExpired(): boolean { 84 | const expiresAt = this.getExpiresAt(); 85 | if (!expiresAt) return true; 86 | 87 | // 提前5分钟认为过期,给刷新token留时间 88 | const now = Date.now(); 89 | const fiveMinutes = 5 * 60 * 1000; 90 | return now >= (expiresAt - fiveMinutes); 91 | } 92 | 93 | /** 94 | * 检查是否有有效的认证状态 95 | */ 96 | static hasValidAuth(): boolean { 97 | const token = this.getToken(); 98 | const user = this.getUser(); 99 | 100 | if (!token || !user) return false; 101 | if (this.isTokenExpired()) return false; 102 | 103 | return true; 104 | } 105 | 106 | /** 107 | * 获取完整的认证状态 108 | */ 109 | static getAuthState(): { token: string | null; user: User | null; isAuthenticated: boolean } { 110 | const token = this.getToken(); 111 | const user = this.getUser(); 112 | const isAuthenticated = this.hasValidAuth(); 113 | 114 | return { token, user, isAuthenticated }; 115 | } 116 | 117 | /** 118 | * 清除认证信息 119 | */ 120 | static clearAuth(): void { 121 | try { 122 | localStorage.removeItem(AUTH_KEYS.TOKEN); 123 | localStorage.removeItem(AUTH_KEYS.USER); 124 | localStorage.removeItem(AUTH_KEYS.EXPIRES_AT); 125 | console.log('✅ 认证信息已清除'); 126 | } catch (error) { 127 | console.error('❌ 清除认证信息失败:', error); 128 | } 129 | } 130 | 131 | /** 132 | * 检查token是否即将过期(用于自动刷新) 133 | */ 134 | static isTokenExpiringSoon(): boolean { 135 | const expiresAt = this.getExpiresAt(); 136 | if (!expiresAt) return true; 137 | 138 | // 30分钟内过期就认为即将过期 139 | const now = Date.now(); 140 | const thirtyMinutes = 30 * 60 * 1000; 141 | return now >= (expiresAt - thirtyMinutes); 142 | } 143 | } 144 | 145 | // 便捷的导出函数 146 | export const { 147 | saveAuth, 148 | getToken, 149 | getUser, 150 | hasValidAuth, 151 | getAuthState, 152 | clearAuth, 153 | isTokenExpired, 154 | isTokenExpiringSoon, 155 | } = AuthManager; -------------------------------------------------------------------------------- /ui/src/hooks/useAuthInit.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import { useAppDispatch, useAppSelector } from '../store/hooks'; 4 | import { validateToken, restoreAuth } from '../store/slices/authSlice'; 5 | import { AuthManager } from '../lib/auth'; 6 | 7 | /** 8 | * 认证初始化Hook 9 | * 在应用启动时检查和恢复认证状态 10 | */ 11 | export const useAuthInit = () => { 12 | const dispatch = useAppDispatch(); 13 | const { isAuthenticated, isLoading, token } = useAppSelector((state) => state.auth); 14 | 15 | useEffect(() => { 16 | console.log('🔐 开始认证初始化...'); 17 | 18 | // 如果已经认证,不需要再次初始化 19 | if (isAuthenticated) { 20 | console.log('✅ 已认证,跳过初始化'); 21 | return; 22 | } 23 | 24 | // 检查是否有本地存储的认证信息 25 | const hasValidAuth = AuthManager.hasValidAuth(); 26 | 27 | if (hasValidAuth) { 28 | console.log('🔄 发现有效的本地认证信息,验证token...'); 29 | 30 | // 验证token是否仍然有效 31 | dispatch(validateToken()) 32 | .unwrap() 33 | .then((user) => { 34 | console.log('✅ 初始化验证成功,用户已登录:', user.username); 35 | }) 36 | .catch((error) => { 37 | console.log('❌ 初始化验证失败:', error.message); 38 | // 认证失败的处理已经在store中完成,不需要额外操作 39 | }); 40 | } else { 41 | console.log('ℹ️ 没有有效的本地认证信息'); 42 | } 43 | }, [dispatch, isAuthenticated]); 44 | 45 | return { 46 | isInitialized: !isLoading, 47 | isAuthenticated, 48 | }; 49 | }; 50 | 51 | /** 52 | * 路由变化时的token验证Hook 53 | * 在路由变化时检查token有效性,防止后端重启导致内存失效 54 | */ 55 | export const useRouteTokenValidation = () => { 56 | const location = useLocation(); 57 | const dispatch = useAppDispatch(); 58 | const { isAuthenticated, token, isLoading } = useAppSelector((state) => state.auth); 59 | const lastValidationTime = useRef(0); 60 | const validationCooldown = 10000; // 10秒冷却时间,避免频繁验证 61 | 62 | useEffect(() => { 63 | // 只在有token的情况下验证 64 | if (!token) { 65 | console.log('🔍 路由变化但无token,跳过验证:', location.pathname); 66 | return; 67 | } 68 | 69 | // 如果正在加载中,跳过验证(避免与正在进行的认证流程冲突) 70 | if (isLoading) { 71 | console.log('🔍 正在加载中,跳过路由验证:', location.pathname); 72 | return; 73 | } 74 | 75 | // 冷却时间检查,避免频繁验证 76 | const now = Date.now(); 77 | if (now - lastValidationTime.current < validationCooldown) { 78 | console.log('🔍 验证冷却中,跳过验证:', location.pathname); 79 | return; 80 | } 81 | 82 | console.log('🔍 路由变化,验证token有效性...', location.pathname); 83 | lastValidationTime.current = now; 84 | 85 | // 验证token是否仍然有效(防止后端重启导致内存失效) 86 | dispatch(validateToken()) 87 | .unwrap() 88 | .then((user) => { 89 | console.log('✅ 路由验证:Token仍然有效,用户:', user.username, '继续访问:', location.pathname); 90 | // token有效,用户保持在当前页面或继续访问目标页面 91 | }) 92 | .catch((error) => { 93 | console.log('❌ 路由验证:Token验证失败:', error.message); 94 | // 认证失败的处理(跳转到登录页)已经在store和ProtectedRoute中完成 95 | // 这里不需要额外操作 96 | }); 97 | }, [location.pathname, dispatch, token, isLoading]); 98 | }; 99 | 100 | /** 101 | * 自动token刷新Hook 102 | * 监控token过期状态,自动刷新即将过期的token 103 | */ 104 | export const useAutoTokenRefresh = () => { 105 | const dispatch = useAppDispatch(); 106 | const { isAuthenticated } = useAppSelector((state) => state.auth); 107 | 108 | useEffect(() => { 109 | if (!isAuthenticated) return; 110 | 111 | const checkTokenExpiry = () => { 112 | if (AuthManager.isTokenExpiringSoon()) { 113 | console.log('⏰ Token即将过期,尝试刷新...'); 114 | dispatch(validateToken()) 115 | .unwrap() 116 | .then(() => { 117 | console.log('✅ Token刷新成功'); 118 | }) 119 | .catch((error) => { 120 | console.log('❌ Token刷新失败:', error.message); 121 | }); 122 | } 123 | }; 124 | 125 | // 每5分钟检查一次token状态 126 | const interval = setInterval(checkTokenExpiry, 5 * 60 * 1000); 127 | 128 | // 组件挂载时立即检查一次 129 | checkTokenExpiry(); 130 | 131 | return () => clearInterval(interval); 132 | }, [dispatch, isAuthenticated]); 133 | }; -------------------------------------------------------------------------------- /backend/remote_api/recipe/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Recipe API Data Models 3 | 4 | Author: Andrew Wang 5 | """ 6 | 7 | from pydantic import BaseModel, Field, ConfigDict 8 | from typing import Optional, List, Dict, Any 9 | 10 | 11 | class RecipeSearchRequest(BaseModel): 12 | """Recipe search request parameters""" 13 | search_term: str 14 | search_type: str = "name" # name, ingredient, category, area 15 | 16 | 17 | class Meal(BaseModel): 18 | """Universal meal entity for all API endpoints""" 19 | model_config = ConfigDict(extra='allow', use_enum_values=True) 20 | 21 | # Required fields (present in all responses) 22 | idMeal: str 23 | strMeal: str 24 | 25 | # Optional fields (present in detailed responses) 26 | strMealAlternate: Optional[str] = None 27 | strCategory: Optional[str] = None 28 | strArea: Optional[str] = None 29 | strInstructions: Optional[str] = None 30 | strMealThumb: Optional[str] = None 31 | strTags: Optional[str] = None 32 | strYoutube: Optional[str] = None 33 | 34 | # Ingredients (1-20) 35 | strIngredient1: Optional[str] = None 36 | strIngredient2: Optional[str] = None 37 | strIngredient3: Optional[str] = None 38 | strIngredient4: Optional[str] = None 39 | strIngredient5: Optional[str] = None 40 | strIngredient6: Optional[str] = None 41 | strIngredient7: Optional[str] = None 42 | strIngredient8: Optional[str] = None 43 | strIngredient9: Optional[str] = None 44 | strIngredient10: Optional[str] = None 45 | strIngredient11: Optional[str] = None 46 | strIngredient12: Optional[str] = None 47 | strIngredient13: Optional[str] = None 48 | strIngredient14: Optional[str] = None 49 | strIngredient15: Optional[str] = None 50 | strIngredient16: Optional[str] = None 51 | strIngredient17: Optional[str] = None 52 | strIngredient18: Optional[str] = None 53 | strIngredient19: Optional[str] = None 54 | strIngredient20: Optional[str] = None 55 | 56 | # Measures (1-20) 57 | strMeasure1: Optional[str] = None 58 | strMeasure2: Optional[str] = None 59 | strMeasure3: Optional[str] = None 60 | strMeasure4: Optional[str] = None 61 | strMeasure5: Optional[str] = None 62 | strMeasure6: Optional[str] = None 63 | strMeasure7: Optional[str] = None 64 | strMeasure8: Optional[str] = None 65 | strMeasure9: Optional[str] = None 66 | strMeasure10: Optional[str] = None 67 | strMeasure11: Optional[str] = None 68 | strMeasure12: Optional[str] = None 69 | strMeasure13: Optional[str] = None 70 | strMeasure14: Optional[str] = None 71 | strMeasure15: Optional[str] = None 72 | strMeasure16: Optional[str] = None 73 | strMeasure17: Optional[str] = None 74 | strMeasure18: Optional[str] = None 75 | strMeasure19: Optional[str] = None 76 | strMeasure20: Optional[str] = None 77 | 78 | # Additional fields (present in detailed responses) 79 | strSource: Optional[str] = None 80 | strImageSource: Optional[str] = None 81 | strCreativeCommonsConfirmed: Optional[str] = None 82 | dateModified: Optional[str] = None 83 | 84 | 85 | class ApiResponse(BaseModel): 86 | """Universal API response for all endpoints""" 87 | model_config = ConfigDict(extra='allow', use_enum_values=True) 88 | 89 | meals: Optional[List[Meal]] = None 90 | 91 | @classmethod 92 | def from_dict(cls, data: Dict[str, Any]) -> "ApiResponse": 93 | """Create ApiResponse from dictionary using Pydantic's automatic mapping""" 94 | return cls(**data) 95 | 96 | 97 | # Available recipe categories 98 | RECIPE_CATEGORIES = [ 99 | "Beef", "Breakfast", "Chicken", "Dessert", "Goat", "Lamb", 100 | "Miscellaneous", "Pasta", "Pork", "Seafood", "Side", "Starter", 101 | "Vegan", "Vegetarian" 102 | ] 103 | 104 | # Available areas/cuisines 105 | RECIPE_AREAS = [ 106 | "American", "British", "Canadian", "Chinese", "Croatian", "Dutch", 107 | "Egyptian", "French", "Greek", "Indian", "Irish", "Italian", 108 | "Jamaican", "Japanese", "Kenyan", "Malaysian", "Mexican", "Moroccan", 109 | "Polish", "Portuguese", "Russian", "Spanish", "Thai", "Tunisian", 110 | "Turkish", "Vietnamese" 111 | ] -------------------------------------------------------------------------------- /init.sql: -------------------------------------------------------------------------------- 1 | -- AI个人日常助手数据库初始化脚本 2 | 3 | -- 设置字符集 4 | SET NAMES utf8mb4; 5 | SET character_set_client = utf8mb4; 6 | 7 | -- 创建数据库(如果不存在) 8 | CREATE DATABASE IF NOT EXISTS ai_assistant 9 | CHARACTER SET utf8mb4 10 | COLLATE utf8mb4_unicode_ci; 11 | 12 | USE ai_assistant; 13 | 14 | -- 用户表 15 | CREATE TABLE IF NOT EXISTS users ( 16 | id INT AUTO_INCREMENT PRIMARY KEY, 17 | username VARCHAR(50) UNIQUE NOT NULL, 18 | email VARCHAR(255) UNIQUE NOT NULL, 19 | password_hash VARCHAR(255) NOT NULL, 20 | is_active BOOLEAN DEFAULT TRUE, 21 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 22 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 23 | INDEX idx_username (username), 24 | INDEX idx_email (email) 25 | ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 26 | 27 | -- 会话表 28 | CREATE TABLE IF NOT EXISTS conversations ( 29 | id INT AUTO_INCREMENT PRIMARY KEY, 30 | user_id INT NOT NULL, 31 | title VARCHAR(255) NOT NULL, 32 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 33 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 34 | FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, 35 | INDEX idx_user_id (user_id), 36 | INDEX idx_created_at (created_at) 37 | ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 38 | 39 | -- 聊天消息表 40 | CREATE TABLE IF NOT EXISTS chat_messages ( 41 | id INT AUTO_INCREMENT PRIMARY KEY, 42 | conversation_id INT NOT NULL, 43 | role ENUM('user', 'assistant', 'system') NOT NULL, 44 | content TEXT NOT NULL, 45 | timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 46 | FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE, 47 | INDEX idx_conversation_id (conversation_id), 48 | INDEX idx_timestamp (timestamp) 49 | ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 50 | 51 | -- 用户偏好设置表 52 | CREATE TABLE IF NOT EXISTS user_preferences ( 53 | id INT AUTO_INCREMENT PRIMARY KEY, 54 | user_id INT NOT NULL, 55 | preference_key VARCHAR(100) NOT NULL, 56 | preference_value TEXT, 57 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 58 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 59 | FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, 60 | UNIQUE KEY unique_user_preference (user_id, preference_key), 61 | INDEX idx_user_id (user_id) 62 | ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 63 | 64 | -- 待办事项表 65 | CREATE TABLE IF NOT EXISTS todos ( 66 | id INT AUTO_INCREMENT PRIMARY KEY, 67 | user_id INT NOT NULL, 68 | title VARCHAR(255) NOT NULL, 69 | description TEXT, 70 | is_completed BOOLEAN DEFAULT FALSE, 71 | priority ENUM('low', 'medium', 'high') DEFAULT 'medium', 72 | due_date DATETIME NULL, 73 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 74 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 75 | FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, 76 | INDEX idx_user_id (user_id), 77 | INDEX idx_is_completed (is_completed), 78 | INDEX idx_due_date (due_date) 79 | ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 80 | 81 | -- 笔记表 82 | CREATE TABLE IF NOT EXISTS notes ( 83 | id INT AUTO_INCREMENT PRIMARY KEY, 84 | user_id INT NOT NULL, 85 | title VARCHAR(255) NOT NULL, 86 | content TEXT NOT NULL, 87 | tags JSON, 88 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 89 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 90 | FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, 91 | INDEX idx_user_id (user_id), 92 | INDEX idx_title (title), 93 | INDEX idx_created_at (created_at) 94 | ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 95 | 96 | -- 插入默认管理员用户(密码: admin123) 97 | INSERT IGNORE INTO users (username, email, password_hash) VALUES 98 | ('admin', 'admin@example.com', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewthSk7wvdj9uNvO'); 99 | 100 | -- 创建索引来优化查询性能 101 | CREATE INDEX IF NOT EXISTS idx_chat_messages_role ON chat_messages(role); 102 | CREATE INDEX IF NOT EXISTS idx_todos_priority ON todos(priority); 103 | CREATE INDEX IF NOT EXISTS idx_conversations_updated_at ON conversations(updated_at); 104 | 105 | COMMIT; -------------------------------------------------------------------------------- /backend/core/http_core/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | 通用HTTP客户端实现 3 | """ 4 | 5 | import requests 6 | import time 7 | from typing import Optional, Dict, Any 8 | 9 | 10 | class APIClient: 11 | """通用API客户端,提供HTTP请求的基础功能""" 12 | 13 | def __init__(self, base_url: str, timeout: int = 10): 14 | """ 15 | 初始化API客户端 16 | 17 | Args: 18 | base_url: API的基础URL 19 | timeout: 请求超时时间(秒) 20 | """ 21 | self.base_url = base_url.rstrip('/') 22 | self.timeout = timeout 23 | self.session = requests.Session() 24 | 25 | # 设置默认headers 26 | self.session.headers.update({ 27 | 'User-Agent': 'Personal-Daily-Assistant/1.0', 28 | 'Accept': 'application/json', 29 | 'Content-Type': 'application/json' 30 | }) 31 | 32 | def make_request(self, endpoint: str, params: Optional[Dict] = None, 33 | method: str = 'GET', data: Optional[Dict] = None, 34 | max_retries: int = 3) -> Optional[Dict[Any, Any]]: 35 | """ 36 | 发送HTTP请求 37 | 38 | Args: 39 | endpoint: API端点 40 | params: URL参数 41 | method: HTTP方法 42 | data: 请求体数据 43 | max_retries: 最大重试次数 44 | 45 | Returns: 46 | 响应数据或None 47 | """ 48 | url = f"{self.base_url}/{endpoint.lstrip('/')}" 49 | 50 | for attempt in range(max_retries): 51 | try: 52 | response = self.session.request( 53 | method=method, 54 | url=url, 55 | params=params, 56 | json=data, 57 | timeout=self.timeout 58 | ) 59 | response.raise_for_status() 60 | 61 | # 验证JSON响应 62 | try: 63 | return response.json() 64 | except ValueError as e: 65 | print(f"无效的JSON响应: {e}") 66 | return None 67 | 68 | except requests.exceptions.Timeout: 69 | print(f"请求超时 (尝试 {attempt + 1}/{max_retries})") 70 | if attempt < max_retries - 1: 71 | time.sleep(2 ** attempt) # 指数退避 72 | 73 | except requests.exceptions.ConnectionError: 74 | print(f"连接错误 (尝试 {attempt + 1}/{max_retries})") 75 | if attempt < max_retries - 1: 76 | time.sleep(2 ** attempt) 77 | 78 | except requests.exceptions.HTTPError as e: 79 | if e.response.status_code == 429: # 请求限制 80 | print(f"请求限制 (尝试 {attempt + 1}/{max_retries})") 81 | if attempt < max_retries - 1: 82 | time.sleep(5) # 等待更长时间 83 | else: 84 | print(f"HTTP错误 {e.response.status_code}: {e}") 85 | return None 86 | 87 | except requests.exceptions.RequestException as e: 88 | print(f"请求错误: {e}") 89 | return None 90 | 91 | print(f"经过 {max_retries} 次尝试后仍无法获取响应") 92 | return None 93 | 94 | def get(self, endpoint: str, params: Optional[Dict] = None) -> Optional[Dict[Any, Any]]: 95 | """GET请求快捷方法""" 96 | return self.make_request(endpoint, params=params, method='GET') 97 | 98 | def post(self, endpoint: str, data: Optional[Dict] = None) -> Optional[Dict[Any, Any]]: 99 | """POST请求快捷方法""" 100 | return self.make_request(endpoint, data=data, method='POST') 101 | 102 | def put(self, endpoint: str, data: Optional[Dict] = None) -> Optional[Dict[Any, Any]]: 103 | """PUT请求快捷方法""" 104 | return self.make_request(endpoint, data=data, method='PUT') 105 | 106 | def delete(self, endpoint: str) -> Optional[Dict[Any, Any]]: 107 | """DELETE请求快捷方法""" 108 | return self.make_request(endpoint, method='DELETE') 109 | 110 | def __enter__(self): 111 | return self 112 | 113 | def __exit__(self, exc_type, exc_val, exc_tb): 114 | self.session.close() -------------------------------------------------------------------------------- /backend/service/models/conversation.py: -------------------------------------------------------------------------------- 1 | """ 2 | 会话管理数据模型 3 | """ 4 | 5 | import uuid 6 | from sqlalchemy import Column, Integer, String, Text, DateTime, Index 7 | from sqlalchemy.sql import func 8 | from core.database_core import BaseModel 9 | 10 | 11 | class Conversation(BaseModel): 12 | """ 13 | 会话管理模型 14 | 15 | 存储用户的会话信息 16 | """ 17 | __tablename__ = 'conversations' 18 | 19 | # 会话状态常量 20 | STATUS_ACTIVE = 'active' 21 | STATUS_INACTIVE = 'inactive' 22 | STATUS_ARCHIVED = 'archived' 23 | 24 | ALLOWED_STATUS = [ 25 | STATUS_ACTIVE, 26 | STATUS_INACTIVE, 27 | STATUS_ARCHIVED 28 | ] 29 | 30 | # 用户ID(来自JSONPlaceholder API) 31 | user_id = Column(Integer, nullable=False, comment='用户ID(来自JSONPlaceholder)') 32 | 33 | # 会话字符串标识符(UUID) 34 | id_str = Column(String(36), unique=True, nullable=False, default=lambda: str(uuid.uuid4()), comment='会话字符串标识符(UUID)') 35 | 36 | # 会话标题 37 | title = Column(String(200), nullable=False, comment='会话标题') 38 | 39 | # 会话描述 40 | description = Column(Text, comment='会话描述') 41 | 42 | # 会话状态 43 | status = Column(String(20), default=STATUS_ACTIVE, comment='会话状态') 44 | 45 | # 最后活跃时间 46 | last_active = Column(DateTime, default=func.now(), onupdate=func.now(), comment='最后活跃时间') 47 | 48 | # 创建索引优化查询 49 | __table_args__ = ( 50 | Index('idx_conversation_user_id', 'user_id'), 51 | Index('idx_conversation_id_str', 'id_str'), 52 | Index('idx_conversation_status', 'status'), 53 | Index('idx_conversation_last_active', 'last_active'), 54 | ) 55 | 56 | def __repr__(self): 57 | return f"" 58 | 59 | def to_dict(self): 60 | """转换为字典格式""" 61 | return { 62 | 'id': self.id, 63 | 'id_str': self.id_str, 64 | 'user_id': self.user_id, 65 | 'title': self.title, 66 | 'description': self.description, 67 | 'status': self.status, 68 | 'last_active': self.last_active.isoformat() if self.last_active is not None else None, 69 | 'created_at': self.created_at.isoformat() if self.created_at is not None else None, 70 | 'updated_at': self.updated_at.isoformat() if self.updated_at is not None else None 71 | } 72 | 73 | @classmethod 74 | def validate_status(cls, status): 75 | """验证会话状态""" 76 | if status not in cls.ALLOWED_STATUS: 77 | raise ValueError(f"Invalid status: {status}. Allowed values: {cls.ALLOWED_STATUS}") 78 | return True 79 | 80 | @classmethod 81 | def create_from_dict(cls, data): 82 | """从字典创建会话对象""" 83 | # 验证必填字段 84 | if 'user_id' not in data: 85 | raise ValueError("user_id is required") 86 | if 'title' not in data: 87 | raise ValueError("title is required") 88 | 89 | # 验证状态 90 | status = data.get('status', cls.STATUS_ACTIVE) 91 | cls.validate_status(status) 92 | 93 | # 创建对象 94 | conversation = cls( 95 | user_id=data['user_id'], 96 | title=data['title'], 97 | description=data.get('description'), 98 | status=status, 99 | ) 100 | 101 | # 如果提供了 id_str,使用它,否则会自动生成 102 | if 'id_str' in data: 103 | conversation.id_str = data['id_str'] 104 | 105 | return conversation 106 | 107 | def set_status(self, status): 108 | """设置会话状态""" 109 | self.validate_status(status) 110 | self.status = status 111 | self.last_active = func.now() 112 | 113 | def get_status(self): 114 | """获取会话状态""" 115 | return self.status 116 | 117 | def is_active(self): 118 | """检查会话是否活跃""" 119 | return self.status == self.STATUS_ACTIVE 120 | 121 | def archive(self): 122 | """归档会话""" 123 | self.set_status(self.STATUS_ARCHIVED) 124 | 125 | def activate(self): 126 | """激活会话""" 127 | self.set_status(self.STATUS_ACTIVE) 128 | 129 | def deactivate(self): 130 | """停用会话""" 131 | self.set_status(self.STATUS_INACTIVE) -------------------------------------------------------------------------------- /backend/remote_api/recipe/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Recipe API Client 3 | 4 | Author: Andrew Wang 5 | """ 6 | 7 | from typing import Optional, List 8 | from core.http_core.client import APIClient 9 | from .models import ( 10 | ApiResponse, 11 | RECIPE_CATEGORIES, RECIPE_AREAS 12 | ) 13 | 14 | 15 | class RecipeClient: 16 | """TheMealDB Recipe API Client""" 17 | 18 | def __init__(self): 19 | self.client = APIClient("https://www.themealdb.com/api/json/v1/1") 20 | 21 | def search_by_name(self, name: str) -> Optional[ApiResponse]: 22 | """ 23 | Search recipes by name 24 | 25 | Args: 26 | name: Recipe name 27 | 28 | Returns: 29 | ApiResponse entity or None 30 | """ 31 | params = {"s": name} 32 | data = self.client.get("/search.php", params=params) 33 | 34 | if data: 35 | return ApiResponse.from_dict(data) 36 | return None 37 | 38 | def search_by_ingredient(self, ingredient: str) -> Optional[ApiResponse]: 39 | """ 40 | Search recipes by main ingredient 41 | 42 | Args: 43 | ingredient: Ingredient name 44 | 45 | Returns: 46 | ApiResponse entity or None 47 | """ 48 | params = {"i": ingredient} 49 | data = self.client.get("/filter.php", params=params) 50 | 51 | if data: 52 | return ApiResponse.from_dict(data) 53 | return None 54 | 55 | def search_by_category(self, category: str) -> Optional[ApiResponse]: 56 | """ 57 | Search recipes by category 58 | 59 | Args: 60 | category: Category name 61 | 62 | Returns: 63 | ApiResponse entity or None 64 | """ 65 | if category not in RECIPE_CATEGORIES: 66 | print(f"Invalid category: {category}. Available categories: {RECIPE_CATEGORIES}") 67 | return None 68 | 69 | params = {"c": category} 70 | data = self.client.get("/filter.php", params=params) 71 | 72 | if data: 73 | return ApiResponse.from_dict(data) 74 | return None 75 | 76 | def search_by_area(self, area: str) -> Optional[ApiResponse]: 77 | """ 78 | Search recipes by area/cuisine 79 | 80 | Args: 81 | area: Area/cuisine name 82 | 83 | Returns: 84 | ApiResponse entity or None 85 | """ 86 | if area not in RECIPE_AREAS: 87 | print(f"Invalid area: {area}. Available areas: {RECIPE_AREAS}") 88 | return None 89 | 90 | params = {"a": area} 91 | data = self.client.get("/filter.php", params=params) 92 | 93 | if data: 94 | return ApiResponse.from_dict(data) 95 | return None 96 | 97 | def get_recipe_details(self, meal_id: str) -> Optional[ApiResponse]: 98 | """ 99 | Get recipe detailed information 100 | 101 | Args: 102 | meal_id: Recipe ID 103 | 104 | Returns: 105 | ApiResponse entity or None 106 | """ 107 | params = {"i": meal_id} 108 | data = self.client.get("/lookup.php", params=params) 109 | 110 | if data and data.get("meals"): 111 | return ApiResponse.from_dict(data) 112 | return None 113 | 114 | def get_random_recipe(self) -> Optional[ApiResponse]: 115 | """ 116 | Get random recipe 117 | 118 | Returns: 119 | ApiResponse entity or None 120 | """ 121 | data = self.client.get("/random.php") 122 | 123 | if data and data.get("meals"): 124 | return ApiResponse.from_dict(data) 125 | return None 126 | 127 | def get_categories(self) -> Optional[List[str]]: 128 | """ 129 | Get available recipe categories 130 | 131 | Returns: 132 | Categories list 133 | """ 134 | return RECIPE_CATEGORIES.copy() 135 | 136 | def get_areas(self) -> Optional[List[str]]: 137 | """ 138 | Get available areas/cuisines 139 | 140 | Returns: 141 | Areas list 142 | """ 143 | return RECIPE_AREAS.copy() -------------------------------------------------------------------------------- /backend/core/vector_core/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Vector Database Utilities 3 | """ 4 | 5 | import re 6 | import hashlib 7 | from typing import Dict, Any, Optional, List 8 | from datetime import datetime 9 | 10 | 11 | def create_collection_name(prefix: str, user_id: str) -> str: 12 | """Create a valid collection name for user isolation""" 13 | # Chroma collection names must be alphanumeric with underscores 14 | sanitized_user_id = re.sub(r'[^a-zA-Z0-9_]', '_', user_id) 15 | return f"{prefix}_user_{sanitized_user_id}" 16 | 17 | 18 | def validate_metadata(metadata: Dict[str, Any]) -> Dict[str, Any]: 19 | """Validate and sanitize metadata for Chroma storage""" 20 | if not metadata: 21 | return {} 22 | 23 | validated_metadata = {} 24 | 25 | for key, value in metadata.items(): 26 | # Ensure key is string and valid 27 | if not isinstance(key, str): 28 | continue 29 | 30 | # Sanitize key name 31 | sanitized_key = re.sub(r'[^a-zA-Z0-9_]', '_', key) 32 | 33 | # Validate value types (Chroma supports str, int, float, bool) 34 | if isinstance(value, (str, int, float, bool)): 35 | validated_metadata[sanitized_key] = value 36 | elif isinstance(value, datetime): 37 | validated_metadata[sanitized_key] = value.isoformat() 38 | elif value is None: 39 | validated_metadata[sanitized_key] = "" 40 | else: 41 | # Convert other types to string 42 | validated_metadata[sanitized_key] = str(value) 43 | 44 | return validated_metadata 45 | 46 | 47 | def generate_document_id(text: str, user_id: str, source: Optional[str] = None) -> str: 48 | """Generate a unique document ID based on content and user""" 49 | content = f"{user_id}:{source or 'default'}:{text}" 50 | return hashlib.md5(content.encode()).hexdigest() 51 | 52 | 53 | def build_chroma_filter( 54 | source_filter: Optional[str] = None, 55 | metadata_filter: Optional[Dict[str, Any]] = None 56 | ) -> Optional[Dict[str, Any]]: 57 | """Build a Chroma-compatible filter for handling multiple conditions.""" 58 | conditions = [] 59 | 60 | if source_filter: 61 | conditions.append({"source": {"$eq": source_filter}}) 62 | 63 | if metadata_filter: 64 | validated_metadata = validate_metadata(metadata_filter) 65 | for key, value in validated_metadata.items(): 66 | conditions.append({key: {"$eq": value}}) 67 | 68 | if not conditions: 69 | return None 70 | 71 | if len(conditions) == 1: 72 | return conditions[0] 73 | 74 | return {"$and": conditions} 75 | 76 | 77 | def chunk_text(text: str, chunk_size: int = 1000, overlap: int = 200) -> List[str]: 78 | """Split text into chunks for better vector storage""" 79 | if len(text) <= chunk_size: 80 | return [text] 81 | 82 | chunks = [] 83 | start = 0 84 | 85 | while start < len(text): 86 | end = start + chunk_size 87 | 88 | # Try to find a good break point (sentence end) 89 | if end < len(text): 90 | # Look for sentence endings within the last 100 characters 91 | search_start = max(end - 100, start) 92 | sentence_end = text.rfind('.', search_start, end) 93 | if sentence_end > start: 94 | end = sentence_end + 1 95 | 96 | chunk = text[start:end].strip() 97 | if chunk: 98 | chunks.append(chunk) 99 | 100 | start = end - overlap 101 | if start >= len(text): 102 | break 103 | 104 | return chunks 105 | 106 | 107 | def calculate_similarity_score(distance: float) -> float: 108 | """Convert distance to similarity score (0-1)""" 109 | return max(0.0, 1.0 - distance) 110 | 111 | 112 | def filter_results_by_threshold( 113 | results: List[tuple], 114 | threshold: float, 115 | distance_index: int = 2 116 | ) -> List[tuple]: 117 | """Filter results by similarity threshold""" 118 | filtered = [] 119 | 120 | for result in results: 121 | if len(result) > distance_index and result[distance_index] is not None: 122 | distance = result[distance_index] 123 | score = calculate_similarity_score(distance) 124 | if score >= threshold: 125 | filtered.append(result) 126 | else: 127 | # Include results without distance information 128 | filtered.append(result) 129 | 130 | return filtered -------------------------------------------------------------------------------- /backend/core/database_core/init_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 数据库初始化脚本 4 | """ 5 | 6 | import sys 7 | import os 8 | from pathlib import Path 9 | 10 | # 添加项目根目录到Python路径 11 | project_root = Path(__file__).parent.parent.parent 12 | sys.path.insert(0, str(project_root)) 13 | 14 | from core.database_core import DatabaseClient, DatabaseConfig 15 | from core.database_core.utils import DatabaseUtils 16 | 17 | 18 | def check_mysql_connection(): 19 | """检查MySQL连接""" 20 | print("=== 检查MySQL连接 ===") 21 | 22 | try: 23 | db_client = DatabaseClient() 24 | if db_client.initialize(): 25 | print("✅ MySQL连接成功") 26 | 27 | # 获取数据库信息 28 | db_info = db_client.get_database_info() 29 | if db_info: 30 | print(f"📊 数据库信息:") 31 | print(f" - 引擎: {db_info.get('engine_name', 'unknown')}") 32 | print(f" - 表数量: {db_info.get('table_count', 0)}") 33 | print(f" - 连接URL: {db_info.get('url', 'unknown')}") 34 | 35 | db_client.close() 36 | return True 37 | else: 38 | print("❌ MySQL连接失败") 39 | return False 40 | except Exception as e: 41 | print(f"❌ MySQL连接异常: {e}") 42 | return False 43 | 44 | 45 | def create_database_tables(): 46 | """创建数据库表""" 47 | print("\n=== 创建数据库表 ===") 48 | 49 | try: 50 | db_client = DatabaseClient() 51 | if not db_client.initialize(): 52 | print("❌ 数据库初始化失败") 53 | return False 54 | 55 | # 创建表 56 | if db_client.create_tables(): 57 | print("✅ 数据库表创建成功") 58 | 59 | # 获取表信息 60 | db_info = db_client.get_database_info() 61 | if db_info and db_info.get('tables'): 62 | print(f"📋 已创建的表:") 63 | for table in db_info['tables']: 64 | print(f" - {table}") 65 | 66 | db_client.close() 67 | return True 68 | else: 69 | print("❌ 数据库表创建失败") 70 | db_client.close() 71 | return False 72 | 73 | except Exception as e: 74 | print(f"❌ 创建表时发生异常: {e}") 75 | return False 76 | 77 | 78 | def show_configuration(): 79 | """显示数据库配置""" 80 | print("\n=== 数据库配置 ===") 81 | 82 | try: 83 | config = DatabaseConfig() 84 | print(f"🔧 配置信息:") 85 | print(f" - 主机: {config.host}") 86 | print(f" - 端口: {config.port}") 87 | print(f" - 用户名: {config.username}") 88 | print(f" - 数据库: {config.database}") 89 | print(f" - 字符集: {config.charset}") 90 | print(f" - 连接池大小: {config.pool_size}") 91 | print(f" - 最大溢出: {config.max_overflow}") 92 | print(f" - 连接超时: {config.pool_timeout}") 93 | print(f" - 连接回收: {config.pool_recycle}") 94 | print(f" - 启用回显: {config.echo}") 95 | 96 | if config.validate(): 97 | print("✅ 配置验证通过") 98 | else: 99 | print("❌ 配置验证失败") 100 | 101 | except Exception as e: 102 | print(f"❌ 配置检查异常: {e}") 103 | 104 | 105 | def main(): 106 | """主函数""" 107 | print("🚀 MySQL数据库组件初始化") 108 | print("=" * 50) 109 | 110 | # 显示配置 111 | show_configuration() 112 | 113 | # 检查连接 114 | if not check_mysql_connection(): 115 | print("\n❌ 初始化失败:无法连接到MySQL数据库") 116 | print("请检查以下项目:") 117 | print("1. MySQL服务是否正在运行") 118 | print("2. 数据库配置是否正确") 119 | print("3. 用户权限是否足够") 120 | print("4. 环境变量是否设置正确") 121 | return False 122 | 123 | # 创建表 124 | if not create_database_tables(): 125 | print("\n❌ 初始化失败:无法创建数据库表") 126 | return False 127 | 128 | print("\n🎉 数据库初始化完成!") 129 | print("现在您可以使用MySQL组件进行数据库操作了。") 130 | 131 | return True 132 | 133 | 134 | if __name__ == "__main__": 135 | """ 136 | 运行此脚本前,请确保: 137 | 1. 已安装依赖:pip install -r requirements.txt 138 | 2. 已配置环境变量或创建.env文件 139 | 3. MySQL服务正在运行 140 | 4. 数据库用户有足够的权限 141 | """ 142 | 143 | try: 144 | success = main() 145 | if success: 146 | print("\n✅ 初始化成功!") 147 | sys.exit(0) 148 | else: 149 | print("\n❌ 初始化失败!") 150 | sys.exit(1) 151 | except KeyboardInterrupt: 152 | print("\n⚠️ 初始化被用户中断") 153 | sys.exit(1) 154 | except Exception as e: 155 | print(f"\n💥 初始化过程中发生未知错误: {e}") 156 | sys.exit(1) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 项目 .gitignore 文件 2 | # Python project .gitignore file 3 | 4 | # 字节码文件和编译后的 Python 文件 (Byte-compiled / optimized / DLL files) 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | *.so 9 | 10 | # 分发打包文件 (Distribution / packaging) 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | !ui/src/lib 32 | 33 | # PyInstaller 文件 (PyInstaller) 34 | # 通常这些文件由 PyInstaller 生成,用于创建可执行文件 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # 安装器日志 (Installer logs) 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # 单元测试 / 覆盖率报告 (Unit test / coverage reports) 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # 翻译文件 (Translations) 59 | *.mo 60 | *.pot 61 | 62 | # Django 相关文件 (Django stuff) 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask 相关文件 (Flask stuff) 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy 相关文件 (Scrapy stuff) 73 | .scrapy 74 | 75 | # Sphinx 文档 (Sphinx documentation) 76 | docs/_build/ 77 | 78 | # PyBuilder 文件 (PyBuilder) 79 | target/ 80 | 81 | # Jupyter Notebook 文件 (Jupyter Notebook) 82 | .ipynb_checkpoints 83 | 84 | # IPython 文件 (IPython) 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 版本文件 (pyenv) 89 | .python-version 90 | 91 | # pipenv 相关文件 (pipenv) 92 | # 根据 pypa/pipenv#598,建议包含 Pipfile.lock 在版本控制中 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # 但是,如果你的项目协作开发,你可能想要排除它: 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582 相关文件 (PEP 582) 101 | __pypackages__/ 102 | 103 | # Celery 相关文件 (Celery stuff) 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath 解析文件 (SageMath parsed files) 108 | *.sage.py 109 | 110 | # 环境变量文件 (Environments) 111 | .env 112 | .env.* 113 | .env_* 114 | *.env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder 项目设置 (Spyder project settings) 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope 项目设置 (Rope project settings) 127 | .ropeproject 128 | 129 | # mkdocs 文档 (mkdocs documentation) 130 | /site 131 | 132 | # mypy 类型检查缓存 (mypy) 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre 类型检查器 (Pyre type checker) 138 | .pyre/ 139 | 140 | # 操作系统生成的文件 (Operating System Files) 141 | # macOS 142 | .DS_Store 143 | .AppleDouble 144 | .LSOverride 145 | Thumbs.db 146 | 147 | # Windows 148 | Thumbs.db 149 | ehthumbs.db 150 | Desktop.ini 151 | $RECYCLE.BIN/ 152 | 153 | # Linux 154 | *~ 155 | 156 | # IDE 和编辑器文件 (IDE and Editor files) 157 | # PyCharm 158 | .idea/ 159 | *.swp 160 | *.swo 161 | 162 | # Visual Studio Code 163 | .vscode/ 164 | *.code-workspace 165 | 166 | # Sublime Text 167 | *.sublime-project 168 | *.sublime-workspace 169 | 170 | # Vim 171 | *.swp 172 | *.swo 173 | *~ 174 | 175 | # 日志文件 (Log files) 176 | *.log 177 | 178 | # 临时文件 (Temporary files) 179 | *.tmp 180 | *.temp 181 | 182 | # 本地配置文件 (Local configuration files) 183 | config.local.py 184 | settings.local.py 185 | 186 | # 数据库文件 (Database files) 187 | *.db 188 | *.sqlite 189 | *.sqlite3 190 | 191 | # 密钥和凭证文件 (Keys and credentials) 192 | *.pem 193 | *.key 194 | *.crt 195 | *.csr 196 | secrets.py 197 | credentials.json 198 | token.json 199 | 200 | # 备份文件 (Backup files) 201 | *.bak 202 | *.backup 203 | *.old 204 | 205 | # 压缩文件 (Archive files) 206 | *.zip 207 | *.tar.gz 208 | *.rar 209 | *.7z 210 | 211 | # Node.js 相关文件(如果项目包含前端部分)(Node.js related files if project includes frontend) 212 | node_modules/ 213 | npm-debug.log* 214 | yarn-debug.log* 215 | yarn-error.log* 216 | 217 | 218 | # Logs 219 | logs 220 | *.log 221 | npm-debug.log* 222 | yarn-debug.log* 223 | yarn-error.log* 224 | pnpm-debug.log* 225 | lerna-debug.log* 226 | 227 | node_modules 228 | dist 229 | dist-ssr 230 | *.local 231 | 232 | # Editor directories and files 233 | .vscode/* 234 | !.vscode/extensions.json 235 | .idea 236 | .DS_Store 237 | *.suo 238 | *.ntvs* 239 | *.njsproj 240 | *.sln 241 | *.sw? 242 | -------------------------------------------------------------------------------- /backend/core/auth_core/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | 认证中间件模块 3 | 4 | 提供FastAPI的认证中间件和依赖注入装饰器 5 | """ 6 | 7 | from typing import Optional, Dict, Any 8 | from fastapi import Depends, HTTPException, status, Request, Cookie 9 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials 10 | 11 | from .auth import AuthUtils, auth_service 12 | 13 | # HTTP Bearer 安全方案 14 | security = HTTPBearer(auto_error=False) 15 | 16 | 17 | class AuthenticationError(HTTPException): 18 | """认证错误异常""" 19 | def __init__(self, detail: str = "认证失败"): 20 | super().__init__( 21 | status_code=status.HTTP_401_UNAUTHORIZED, 22 | detail=detail, 23 | headers={"WWW-Authenticate": "Bearer"}, 24 | ) 25 | 26 | 27 | class PermissionError(HTTPException): 28 | """权限错误异常""" 29 | def __init__(self, detail: str = "权限不足"): 30 | super().__init__( 31 | status_code=status.HTTP_403_FORBIDDEN, 32 | detail=detail, 33 | ) 34 | 35 | 36 | def get_token_from_request(request: Request, 37 | credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), 38 | access_token: Optional[str] = Cookie(None)) -> Optional[str]: 39 | """ 40 | 从请求中获取JWT令牌 41 | 42 | 优先级: 43 | 1. Authorization头 44 | 2. Cookie中的access_token 45 | 3. 查询参数中的token 46 | 47 | Args: 48 | request: FastAPI请求对象 49 | credentials: HTTP Bearer认证凭据 50 | access_token: Cookie中的令牌 51 | 52 | Returns: 53 | JWT令牌字符串或None 54 | """ 55 | # 1. 从Authorization头获取 56 | if credentials and credentials.credentials: 57 | return credentials.credentials 58 | 59 | # 2. 从Cookie获取 60 | if access_token: 61 | return access_token 62 | 63 | # 3. 从查询参数获取 (主要用于WebSocket) 64 | token = request.query_params.get("token") 65 | if token: 66 | return token 67 | 68 | return None 69 | 70 | 71 | def get_current_user(request: Request, 72 | credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), 73 | access_token: Optional[str] = Cookie(None)) -> Dict[str, Any]: 74 | """ 75 | 获取当前用户信息(必需认证) 76 | 77 | Args: 78 | request: FastAPI请求对象 79 | credentials: HTTP Bearer认证凭据 80 | access_token: Cookie中的令牌 81 | 82 | Returns: 83 | 用户信息字典 84 | 85 | Raises: 86 | AuthenticationError: 认证失败 87 | """ 88 | token = get_token_from_request(request, credentials, access_token) 89 | 90 | if not token: 91 | raise AuthenticationError("缺少认证令牌") 92 | 93 | user = auth_service.verify_token(token) 94 | if not user: 95 | raise AuthenticationError("无效的认证令牌") 96 | 97 | return user 98 | 99 | 100 | def get_current_user_optional(request: Request, 101 | credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), 102 | access_token: Optional[str] = Cookie(None)) -> Optional[Dict[str, Any]]: 103 | """ 104 | 获取当前用户信息(可选认证) 105 | 106 | Args: 107 | request: FastAPI请求对象 108 | credentials: HTTP Bearer认证凭据 109 | access_token: Cookie中的令牌 110 | 111 | Returns: 112 | 用户信息字典或None 113 | """ 114 | token = get_token_from_request(request, credentials, access_token) 115 | 116 | if not token: 117 | return None 118 | 119 | user = auth_service.verify_token(token) 120 | return user 121 | 122 | 123 | def verify_user_permission(user: Dict[str, Any], required_permission: Optional[str] = None) -> bool: 124 | """ 125 | 验证用户权限 126 | 127 | Args: 128 | user: 用户信息 129 | required_permission: 所需权限 130 | 131 | Returns: 132 | 是否有权限 133 | """ 134 | # 目前所有认证用户都有权限 135 | # 可以在这里添加更复杂的权限验证逻辑 136 | return True 137 | 138 | 139 | def require_permission(permission: Optional[str] = None): 140 | """ 141 | 权限验证装饰器 142 | 143 | Args: 144 | permission: 所需权限 145 | 146 | Returns: 147 | 依赖注入函数 148 | """ 149 | def permission_checker(user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]: 150 | if not verify_user_permission(user, permission): 151 | raise PermissionError(f"需要权限: {permission}") 152 | return user 153 | 154 | return permission_checker 155 | 156 | 157 | # 预定义的依赖注入装饰器 158 | CurrentUser = Depends(get_current_user) 159 | CurrentUserOptional = Depends(get_current_user_optional) 160 | AdminUser = Depends(require_permission("admin")) -------------------------------------------------------------------------------- /ui/src/components/runner-output.tsx: -------------------------------------------------------------------------------- 1 | import { Terminal, ArrowRight, Wrench, FileText } from "lucide-react"; 2 | import type { AgentEvent } from "../lib/types"; 3 | 4 | interface RunnerOutputProps { 5 | runnerEvents: AgentEvent[]; 6 | } 7 | 8 | export function RunnerOutput({ runnerEvents }: RunnerOutputProps) { 9 | const getEventIcon = (type: string) => { 10 | switch (type) { 11 | case "handoff": 12 | return ; 13 | case "tool_call": 14 | return ; 15 | case "tool_output": 16 | return ; 17 | case "context_update": 18 | return ; 19 | default: 20 | return ; 21 | } 22 | }; 23 | 24 | const getEventColor = (type: string) => { 25 | switch (type) { 26 | case "handoff": 27 | return "border-blue-200 bg-blue-50"; 28 | case "tool_call": 29 | return "border-green-200 bg-green-50"; 30 | case "tool_output": 31 | return "border-purple-200 bg-purple-50"; 32 | case "context_update": 33 | return "border-orange-200 bg-orange-50"; 34 | default: 35 | return "border-gray-200 bg-gray-50"; 36 | } 37 | }; 38 | 39 | const formatTimestamp = (timestamp: number | string | Date): string => { 40 | try { 41 | if (!timestamp) return ''; 42 | // 确保 timestamp 是一个 Date 对象 43 | const date = new Date(timestamp); 44 | // 检查转换后的日期是否有效 45 | if (isNaN(date.getTime())) { 46 | return 'Invalid Date'; 47 | } 48 | return date.toLocaleTimeString(); 49 | } catch (error) { 50 | console.error('Error formatting timestamp:', error); 51 | return 'Time Error'; 52 | } 53 | }; 54 | 55 | return ( 56 |
57 |
58 | 59 |

60 | 运行输出 61 |

62 |
63 | 64 | {runnerEvents.length === 0 && ( 65 |
66 | 暂无运行事件 67 |
68 | )} 69 | 70 |
71 | {runnerEvents.map((event) => ( 72 |
76 |
77 | {getEventIcon(event.type)} 78 |
79 |
80 | 81 | {event.type} 82 | 83 | 84 | by {event.agent} 85 | 86 |
87 |

88 | {event.content} 89 |

90 | 91 | {event.metadata && ( 92 |
93 |
元数据:
94 | {Object.entries(event.metadata).map(([key, value], index) => ( 95 |
96 | {key}: 97 | 98 | {typeof value === 'string' ? value : JSON.stringify(value)} 99 | 100 |
101 | ))} 102 |
103 | )} 104 |
105 |
106 |
107 | {formatTimestamp(event.timestamp)} 108 |
109 |
110 | ))} 111 |
112 |
113 | ); 114 | } 115 | 116 | function Database({ className }: { className?: string }) { 117 | return ( 118 | 119 | 120 | 121 | 122 | 123 | ); 124 | } -------------------------------------------------------------------------------- /backend/mcp-serve/mcp_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | AI Personal Daily Assistant MCP Service Main Entry 3 | Unified loading and registration of all MCP tool modules 4 | 5 | Author: Andrew Wang 6 | """ 7 | 8 | import sys 9 | import os 10 | import asyncio 11 | 12 | # Add backend directory to Python path so we can import remote_api modules 13 | backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 14 | sys.path.insert(0, backend_dir) 15 | 16 | from fastmcp import FastMCP 17 | from weather_tools import register_weather_tools 18 | from news_tools import register_news_tools 19 | from recipe_tools import register_recipe_tools 20 | from user_data_tools import register_user_data_tools 21 | 22 | # Import database initialization components 23 | from core.database_core import DatabaseClient 24 | from core.vector_core import ChromaVectorClient, VectorConfig 25 | 26 | # ============================================================================= 27 | # MCP Service Configuration and Initialization 28 | # ============================================================================= 29 | 30 | weather_mcp = FastMCP("Weather") 31 | register_weather_tools(weather_mcp) # Register weather tools 32 | news_mcp = FastMCP("News") 33 | register_news_tools(news_mcp) # Register news tools 34 | recipe_mcp = FastMCP("Recipe") 35 | register_recipe_tools(recipe_mcp) # Register recipe tools 36 | user_data_mcp = FastMCP("UserData") 37 | register_user_data_tools(user_data_mcp) # Register user data tools 38 | 39 | mcp = FastMCP("MainApp") 40 | 41 | def initialize_databases(): 42 | """ 43 | Initialize MySQL and Vector databases 44 | 45 | Returns: 46 | bool: True if initialization successful, False otherwise 47 | """ 48 | try: 49 | print("🔄 Initializing databases...") 50 | 51 | # Initialize MySQL database 52 | print("📊 Initializing MySQL database...") 53 | db_client = DatabaseClient() 54 | if not db_client.initialize(): 55 | print("❌ MySQL database initialization failed") 56 | return False 57 | 58 | # Create tables if they don't exist 59 | if not db_client.create_tables(): 60 | print("❌ Failed to create database tables") 61 | return False 62 | 63 | print("✅ MySQL database initialized successfully") 64 | 65 | # Initialize vector database 66 | print("🔍 Initializing vector database...") 67 | try: 68 | vector_config = VectorConfig.from_env() 69 | vector_client = ChromaVectorClient(vector_config) 70 | health = vector_client.health_check() 71 | if health.get('status') != 'healthy': 72 | print("⚠️ Vector database health check failed") 73 | else: 74 | print("✅ Vector database initialized successfully") 75 | except Exception as e: 76 | print(f"⚠️ Vector database initialization failed: {e}") 77 | print(" MCP service will continue without vector search capabilities") 78 | 79 | return True 80 | 81 | except Exception as e: 82 | print(f"❌ Database initialization failed: {e}") 83 | return False 84 | 85 | async def create_mcp_server(): 86 | """ 87 | Create and configure MCP server 88 | 89 | Returns: 90 | FastMCP: Configured MCP instance 91 | """ 92 | print("🚀 Starting AI Personal Daily Assistant MCP Service...") 93 | 94 | # Initialize databases first 95 | if not initialize_databases(): 96 | print("❌ Database initialization failed, but continuing with MCP service") 97 | 98 | # Create MCP instance 99 | await mcp.import_server(weather_mcp, prefix="weather") 100 | await mcp.import_server(news_mcp, prefix="news") 101 | await mcp.import_server(recipe_mcp, prefix="recipe") 102 | await mcp.import_server(user_data_mcp, prefix="user_data") 103 | 104 | print("📦 Registering tool modules...") 105 | 106 | # Register all tool modules 107 | # All tools are registered during import_server calls 108 | 109 | print("✅ All tool modules registered successfully!") 110 | print("🌟 MCP service is ready to handle requests") 111 | 112 | return mcp 113 | 114 | 115 | def main(): 116 | """ 117 | Main function - Start MCP service 118 | """ 119 | try: 120 | # Start server 121 | print("🔌 Starting MCP server...") 122 | asyncio.run(create_mcp_server()) 123 | mcp.run( 124 | transport="http", 125 | host="127.0.0.1", 126 | port=8002, 127 | path="/mcp" 128 | ) 129 | 130 | except Exception as e: 131 | print(f"❌ Error starting MCP service: {str(e)}") 132 | raise 133 | 134 | 135 | # ============================================================================= 136 | # Program Entry Point 137 | # ============================================================================= 138 | 139 | if __name__ == "__main__": 140 | main() -------------------------------------------------------------------------------- /ui/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | 3 | export interface Message { 4 | id: string 5 | content: string 6 | type: 'user' | 'ai' | 'system' 7 | agent: string 8 | timestamp: Date 9 | metadata?: any 10 | } 11 | 12 | export interface Agent { 13 | name: string 14 | description: string 15 | handoffs: string[] 16 | tools: string[] 17 | /** List of input guardrail identifiers for this agent */ 18 | input_guardrails: string[] 19 | } 20 | 21 | export type EventType = "message" | "handoff" | "tool_call" | "tool_output" | "context_update" 22 | 23 | export interface AgentEvent { 24 | id: string 25 | type: 'tool_call' | 'handoff' | 'context_update' | 'error' | 'message' 26 | agent: string 27 | content: string 28 | timestamp: Date 29 | metadata?: any 30 | } 31 | 32 | export interface GuardrailCheck { 33 | id: string 34 | name: string 35 | input: string 36 | reasoning: string 37 | passed: boolean 38 | timestamp: Date 39 | } 40 | 41 | export interface Conversation { 42 | id: string 43 | title: string 44 | description: string 45 | status: 'active' | 'inactive' | 'archived' 46 | last_active: string 47 | created_at: string 48 | updated_at: string 49 | } 50 | 51 | // ========================= 52 | // 笔记相关类型定义 53 | // ========================= 54 | 55 | export interface Note { 56 | id: number 57 | user_id: number 58 | title: string 59 | content: string 60 | tag: string | null 61 | status: string 62 | created_at: string | null 63 | updated_at: string | null 64 | last_updated: string | null 65 | similarity_score?: number 66 | } 67 | 68 | export interface NoteCreateRequest { 69 | title: string 70 | content: string 71 | tag?: string 72 | status?: string 73 | } 74 | 75 | export interface NoteUpdateRequest { 76 | title?: string 77 | content?: string 78 | tag?: string 79 | status?: string 80 | } 81 | 82 | export interface NoteListResponse { 83 | success: boolean 84 | message: string 85 | data: Note[] 86 | total: number 87 | user_id: number 88 | } 89 | 90 | export interface NoteResponse { 91 | success: boolean 92 | message: string 93 | data: Note | null 94 | } 95 | 96 | export interface NoteSearchResponse { 97 | success: boolean 98 | message: string 99 | data: Note[] 100 | search_query: string 101 | total: number 102 | user_id: number 103 | } 104 | 105 | // ========================= 106 | // 待办事项相关类型定义 107 | // ========================= 108 | 109 | export interface Todo { 110 | id: number 111 | user_id: number 112 | title: string 113 | description: string 114 | completed: boolean 115 | priority: 'high' | 'medium' | 'low' 116 | note_id: number | null 117 | due_date: string | null 118 | completed_at: string | null 119 | created_at: string | null 120 | updated_at: string | null 121 | last_updated: string | null 122 | is_overdue: boolean 123 | status_display: string 124 | } 125 | 126 | export interface TodoCreateRequest { 127 | title: string 128 | description: string 129 | priority: 'high' | 'medium' | 'low' 130 | due_date?: string 131 | note_id?: number 132 | } 133 | 134 | export interface TodoUpdateRequest { 135 | title?: string 136 | description?: string 137 | priority?: 'high' | 'medium' | 'low' 138 | due_date?: string 139 | note_id?: number 140 | completed?: boolean 141 | } 142 | 143 | export interface TodoListResponse { 144 | success: boolean 145 | message: string 146 | data: Todo[] 147 | total: number 148 | user_id: number 149 | } 150 | 151 | export interface TodoResponse { 152 | success: boolean 153 | message: string 154 | data: Todo | null 155 | } 156 | 157 | export interface TodoStatsResponse { 158 | success: boolean 159 | message: string 160 | data: { 161 | total: number 162 | completed: number 163 | pending: number 164 | overdue: number 165 | high_priority: number 166 | medium_priority: number 167 | low_priority: number 168 | } 169 | user_id: number 170 | } 171 | 172 | // ========================= 173 | // Person Data 相关类型定义 174 | // ========================= 175 | 176 | export type PersonDataTab = 'notes' | 'todos' 177 | 178 | export interface PersonDataFilter { 179 | search?: string 180 | tag?: string 181 | status?: string 182 | priority?: 'high' | 'medium' | 'low' 183 | completed?: boolean 184 | overdue?: boolean 185 | } 186 | 187 | export interface User { 188 | user_id: string 189 | username: string 190 | email: string 191 | name?: string 192 | avatar?: string 193 | } 194 | 195 | export interface AuthToken { 196 | access_token: string 197 | token_type: string 198 | expires_in: number 199 | user_info: User 200 | } 201 | 202 | export interface LoginCredentials { 203 | username: string 204 | password: string 205 | } 206 | 207 | export interface AuthState { 208 | user: User | null 209 | token: string | null 210 | isAuthenticated: boolean 211 | isLoading: boolean 212 | error: string | null 213 | } 214 | 215 | export interface ApiResponse { 216 | success: boolean 217 | code: number 218 | message: string 219 | data?: T 220 | timestamp?: string 221 | request_id?: string 222 | } 223 | 224 | export interface ProtectedRouteProps { 225 | children: ReactNode 226 | requireAuth?: boolean 227 | } -------------------------------------------------------------------------------- /backend/core/database_core/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | 数据模型基类 3 | """ 4 | 5 | from datetime import datetime 6 | from typing import Any, Dict, Optional, List 7 | from sqlalchemy import Column, Integer, DateTime, create_engine, MetaData 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy.orm import sessionmaker, DeclarativeBase 10 | from sqlalchemy.sql import func 11 | 12 | 13 | class Base(DeclarativeBase): 14 | """SQLAlchemy声明式基类""" 15 | pass 16 | 17 | 18 | class BaseModel(Base): 19 | """ 20 | 数据模型基类 21 | 提供通用的字段和方法 22 | """ 23 | __abstract__ = True 24 | 25 | # 通用字段 26 | id = Column(Integer, primary_key=True, autoincrement=True, comment='主键ID') 27 | created_at = Column(DateTime, default=func.now(), comment='创建时间') 28 | updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment='更新时间') 29 | 30 | def to_dict(self) -> Dict[str, Any]: 31 | """ 32 | 将模型实例转换为字典 33 | 34 | Returns: 35 | 模型数据字典 36 | """ 37 | result = {} 38 | for column in self.__table__.columns: 39 | value = getattr(self, column.name) 40 | if isinstance(value, datetime): 41 | value = value.isoformat() 42 | result[column.name] = value 43 | return result 44 | 45 | def update_from_dict(self, data: Dict[str, Any]) -> None: 46 | """ 47 | 从字典更新模型实例 48 | 49 | Args: 50 | data: 更新数据字典 51 | """ 52 | for key, value in data.items(): 53 | if hasattr(self, key): 54 | setattr(self, key, value) 55 | 56 | @classmethod 57 | def create_from_dict(cls, data: Dict[str, Any]) -> 'BaseModel': 58 | """ 59 | 从字典创建模型实例 60 | 61 | Args: 62 | data: 创建数据字典 63 | 64 | Returns: 65 | 模型实例 66 | """ 67 | # 过滤掉不存在的字段 68 | filtered_data = { 69 | key: value for key, value in data.items() 70 | if key in cls.__table__.columns.keys() 71 | } 72 | return cls(**filtered_data) 73 | 74 | def __repr__(self) -> str: 75 | """模型实例的字符串表示""" 76 | return f"<{self.__class__.__name__}(id={getattr(self, 'id', None)})>" 77 | 78 | 79 | class ModelManager: 80 | """ 81 | 模型管理器 82 | 提供模型的CRUD操作 83 | """ 84 | 85 | def __init__(self, model_class: type): 86 | """ 87 | 初始化模型管理器 88 | 89 | Args: 90 | model_class: 模型类 91 | """ 92 | self.model_class = model_class 93 | 94 | def create(self, session, **kwargs) -> BaseModel: 95 | """ 96 | 创建新记录 97 | 98 | Args: 99 | session: 数据库会话 100 | **kwargs: 创建参数 101 | 102 | Returns: 103 | 创建的模型实例 104 | """ 105 | instance = self.model_class(**kwargs) 106 | session.add(instance) 107 | session.commit() 108 | session.refresh(instance) 109 | return instance 110 | 111 | def get_by_id(self, session, record_id: int) -> Optional[BaseModel]: 112 | """ 113 | 通过ID获取记录 114 | 115 | Args: 116 | session: 数据库会话 117 | record_id: 记录ID 118 | 119 | Returns: 120 | 模型实例或None 121 | """ 122 | return session.query(self.model_class).filter( 123 | self.model_class.id == record_id 124 | ).first() 125 | 126 | def get_all(self, session, limit: int = 100, offset: int = 0) -> List[BaseModel]: 127 | """ 128 | 获取所有记录 129 | 130 | Args: 131 | session: 数据库会话 132 | limit: 限制数量 133 | offset: 偏移量 134 | 135 | Returns: 136 | 模型实例列表 137 | """ 138 | return session.query(self.model_class).offset(offset).limit(limit).all() 139 | 140 | def update(self, session, record_id: int, **kwargs) -> Optional[BaseModel]: 141 | """ 142 | 更新记录 143 | 144 | Args: 145 | session: 数据库会话 146 | record_id: 记录ID 147 | **kwargs: 更新参数 148 | 149 | Returns: 150 | 更新后的模型实例或None 151 | """ 152 | instance = self.get_by_id(session, record_id) 153 | if instance: 154 | for key, value in kwargs.items(): 155 | if hasattr(instance, key): 156 | setattr(instance, key, value) 157 | session.commit() 158 | session.refresh(instance) 159 | return instance 160 | 161 | def delete(self, session, record_id: int) -> bool: 162 | """ 163 | 删除记录 164 | 165 | Args: 166 | session: 数据库会话 167 | record_id: 记录ID 168 | 169 | Returns: 170 | 是否删除成功 171 | """ 172 | instance = self.get_by_id(session, record_id) 173 | if instance: 174 | session.delete(instance) 175 | session.commit() 176 | return True 177 | return False 178 | 179 | def count(self, session) -> int: 180 | """ 181 | 获取记录总数 182 | 183 | Args: 184 | session: 数据库会话 185 | 186 | Returns: 187 | 记录总数 188 | """ 189 | return session.query(self.model_class).count() -------------------------------------------------------------------------------- /backend/api/auth_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | 认证API模块 3 | 4 | 包含用户登录、登出、令牌刷新等认证相关的API端点 5 | """ 6 | 7 | import logging 8 | from typing import Optional, Dict, Any 9 | from fastapi import APIRouter, HTTPException, Response 10 | from pydantic import BaseModel 11 | 12 | # 导入认证核心模块 13 | from core.auth_core import ( 14 | auth_service, 15 | Token, 16 | CurrentUser, 17 | CurrentUserOptional, 18 | success_response, 19 | invalid_credentials_response, 20 | internal_error_response 21 | ) 22 | 23 | # 配置日志 24 | logger = logging.getLogger(__name__) 25 | 26 | # 创建认证API路由器 27 | auth_router = APIRouter(prefix="/auth", tags=["认证"]) 28 | 29 | # ========================= 30 | # 数据模型 31 | # ========================= 32 | 33 | class LoginRequest(BaseModel): 34 | """登录请求模型""" 35 | username: str 36 | password: str 37 | 38 | 39 | class LoginResponse(BaseModel): 40 | """登录响应模型""" 41 | success: bool 42 | message: str 43 | token: Optional[Token] = None 44 | user_info: Optional[Dict[str, Any]] = None 45 | 46 | 47 | class LogoutResponse(BaseModel): 48 | """退出登录响应模型""" 49 | success: bool 50 | message: str 51 | 52 | # ========================= 53 | # API端点 54 | # ========================= 55 | 56 | @auth_router.post("/login") 57 | async def login(response: Response, login_request: LoginRequest): 58 | """ 59 | 用户登录接口 60 | 61 | Args: 62 | response: FastAPI响应对象 63 | login_request: 登录请求数据 64 | 65 | Returns: 66 | 统一API响应格式 67 | """ 68 | try: 69 | # 验证用户凭据 70 | token = auth_service.login(login_request.username, login_request.password) 71 | 72 | if not token: 73 | # 用户名或密码错误,返回401状态码 74 | return invalid_credentials_response("用户名或密码错误") 75 | 76 | # 设置Cookie 77 | response.set_cookie( 78 | key="access_token", 79 | value=token.access_token, 80 | max_age=token.expires_in, 81 | httponly=True, 82 | secure=False, # 在生产环境中应该设置为True 83 | samesite="lax" 84 | ) 85 | 86 | logger.info(f"用户 {login_request.username} 登录成功") 87 | 88 | # 构建成功响应数据 89 | response_data = { 90 | "access_token": token.access_token, 91 | "token_type": token.token_type, 92 | "expires_in": token.expires_in, 93 | "user_info": token.user_info 94 | } 95 | 96 | return success_response(response_data, "登录成功") 97 | 98 | except Exception as e: 99 | logger.error(f"登录失败: {str(e)}") 100 | return internal_error_response("服务器内部错误") 101 | 102 | 103 | @auth_router.post("/logout", response_model=LogoutResponse) 104 | async def logout(response: Response, current_user: Dict[str, Any] = CurrentUserOptional): 105 | """ 106 | 用户退出登录接口 107 | 108 | Args: 109 | response: FastAPI响应对象 110 | current_user: 当前用户信息(可选) 111 | 112 | Returns: 113 | 退出登录响应 114 | """ 115 | try: 116 | # 清除Cookie 117 | response.delete_cookie(key="access_token") 118 | 119 | username = current_user.get("username", "未知用户") if current_user else "未知用户" 120 | logger.info(f"用户 {username} 退出登录") 121 | 122 | return LogoutResponse( 123 | success=True, 124 | message="退出登录成功" 125 | ) 126 | 127 | except Exception as e: 128 | logger.error(f"退出登录失败: {str(e)}") 129 | return LogoutResponse( 130 | success=False, 131 | message=f"退出登录失败: {str(e)}" 132 | ) 133 | 134 | 135 | @auth_router.post("/refresh", response_model=LoginResponse) 136 | async def refresh_token(response: Response, current_user: Dict[str, Any] = CurrentUser): 137 | """ 138 | 刷新令牌接口 139 | 140 | Args: 141 | response: FastAPI响应对象 142 | current_user: 当前用户信息 143 | 144 | Returns: 145 | 新的令牌信息 146 | """ 147 | try: 148 | # 重新生成令牌 149 | token_data = { 150 | "user_id": current_user["user_id"], 151 | "username": current_user["username"], 152 | "email": current_user["email"] 153 | } 154 | 155 | new_token = auth_service.login(current_user["username"], "admin123456") 156 | 157 | if not new_token: 158 | raise HTTPException(status_code=401, detail="令牌刷新失败") 159 | 160 | # 更新Cookie 161 | response.set_cookie( 162 | key="access_token", 163 | value=new_token.access_token, 164 | max_age=new_token.expires_in, 165 | httponly=True, 166 | secure=False, # 在生产环境中应该设置为True 167 | samesite="lax" 168 | ) 169 | 170 | logger.info(f"用户 {current_user['username']} 刷新令牌成功") 171 | 172 | return LoginResponse( 173 | success=True, 174 | message="令牌刷新成功", 175 | token=new_token, 176 | user_info=new_token.user_info 177 | ) 178 | 179 | except Exception as e: 180 | logger.error(f"令牌刷新失败: {str(e)}") 181 | raise HTTPException(status_code=500, detail=f"令牌刷新失败: {str(e)}") 182 | 183 | 184 | @auth_router.get("/me") 185 | async def get_current_user_info(current_user: Dict[str, Any] = CurrentUser): 186 | """ 187 | 获取当前用户信息接口 188 | 189 | Args: 190 | current_user: 当前用户信息 191 | 192 | Returns: 193 | 用户信息 194 | """ 195 | return { 196 | "success": True, 197 | "message": "获取用户信息成功", 198 | "user_info": current_user 199 | } -------------------------------------------------------------------------------- /部署说明.md: -------------------------------------------------------------------------------- 1 | # AI 个人日常助手 - 部署说明 2 | 3 | 本文档提供了多种部署方式,包括 Docker Compose 本地部署和 Zeabur 云平台部署。 4 | 5 | ## 📋 环境变量配置 6 | 7 | 在部署之前,你需要准备以下环境变量: 8 | 9 | ### 必需的环境变量 10 | - `OPENAI_API_KEY`: OpenAI API 密钥 11 | - `NEWS_API_TOKEN`: 新闻 API 令牌 12 | - `DB_PASSWORD`: 数据库密码 13 | 14 | ### 可选的环境变量(有默认值) 15 | - `OPENAI_CHAT_MODEL`: OpenAI 聊天模型 (默认: gpt-3.5-turbo) 16 | - `OPENAI_EMBEDDING_MODEL`: OpenAI 嵌入模型 (默认: text-embedding-ada-002) 17 | - `DB_HOST`: 数据库主机 (默认: mysql) 18 | - `DB_PORT`: 数据库端口 (默认: 3306) 19 | - `DB_USERNAME`: 数据库用户名 (默认: root) 20 | - `DB_DATABASE`: 数据库名称 (默认: ai_assistant) 21 | - `DB_CHARSET`: 数据库字符集 (默认: utf8mb4) 22 | - `CHROMA_CLIENT_MODE`: ChromaDB 客户端模式 (默认: http) 23 | - `CHROMA_HOST`: ChromaDB 主机 (默认: chromadb) 24 | - `CHROMA_PORT`: ChromaDB 端口 (默认: 8001) 25 | 26 | ## 🐳 Docker Compose 部署 27 | 28 | ### 前提条件 29 | - 已安装 Docker 和 Docker Compose 30 | - 已获取必需的 API 密钥 31 | 32 | ### 部署步骤 33 | 34 | 1. **克隆项目并进入目录** 35 | ```bash 36 | git clone <项目地址> 37 | cd ai-personal-daily-assistant 38 | ``` 39 | 40 | 2. **创建环境变量文件** 41 | ```bash 42 | cp env.example .env 43 | ``` 44 | 45 | 编辑 `.env` 文件,填入你的真实配置: 46 | ```env 47 | OPENAI_API_KEY=sk-your-openai-api-key-here 48 | OPENAI_CHAT_MODEL=gpt-3.5-turbo 49 | OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 50 | NEWS_API_TOKEN=your-news-api-token-here 51 | DB_PASSWORD=your-secure-password 52 | DB_USERNAME=root 53 | DB_DATABASE=ai_assistant 54 | ``` 55 | 56 | 3. **构建和启动服务** 57 | ```bash 58 | # 生产环境部署 59 | docker-compose up -d --build 60 | 61 | # 或者开发环境部署(包含前端开发服务器) 62 | docker-compose -f docker-compose.dev.yml up -d --build 63 | ``` 64 | 65 | 4. **访问应用** 66 | - 直接访问: http://localhost:8000 67 | - 通过 Nginx(如果启用): http://localhost:80 68 | - API 文档: http://localhost:8000/docs 69 | 70 | 5. **查看日志** 71 | ```bash 72 | # 查看所有服务日志 73 | docker-compose logs -f 74 | 75 | # 查看特定服务日志 76 | docker-compose logs -f app 77 | docker-compose logs -f mysql 78 | docker-compose logs -f chromadb 79 | ``` 80 | 81 | ### 服务说明 82 | 83 | - **app**: 主应用服务(后端 + 前端) 84 | - **mysql**: MySQL 数据库 85 | - **chromadb**: ChromaDB 向量数据库 86 | - **nginx**: Nginx 反向代理(可选) 87 | 88 | ### 端口映射 89 | 90 | - `8000`: 主应用端口 91 | - `3306`: MySQL 数据库端口 92 | - `8001`: ChromaDB 端口 93 | - `80`: Nginx 端口(如果启用) 94 | 95 | ## ☁️ Zeabur 云平台部署 96 | 97 | ### 前提条件 98 | - 拥有 Zeabur 账户 99 | - 项目代码推送到 Git 仓库(GitHub、GitLab 等) 100 | 101 | ### 部署步骤 102 | 103 | 1. **登录 Zeabur 控制台** 104 | 访问 [Zeabur](https://zeabur.com) 并登录 105 | 106 | 2. **创建新项目** 107 | - 点击 "New Project" 108 | - 连接你的 Git 仓库 109 | - 选择本项目仓库 110 | 111 | 3. **配置环境变量** 112 | 在 Zeabur 项目设置中添加以下环境变量: 113 | ``` 114 | OPENAI_API_KEY=sk-your-openai-api-key-here 115 | OPENAI_CHAT_MODEL=gpt-3.5-turbo 116 | OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 117 | NEWS_API_TOKEN=your-news-api-token-here 118 | DB_HOST=mysql 119 | DB_PORT=3306 120 | DB_USERNAME=root 121 | DB_PASSWORD=your-secure-password 122 | DB_DATABASE=ai_assistant 123 | DB_CHARSET=utf8mb4 124 | CHROMA_CLIENT_MODE=http 125 | CHROMA_HOST=chromadb 126 | CHROMA_PORT=8000 127 | ``` 128 | 129 | 4. **部署服务** 130 | - Zeabur 会自动检测 `zeabur.json` 配置文件 131 | - 系统将自动部署应用、MySQL 和 ChromaDB 服务 132 | - 等待部署完成 133 | 134 | 5. **访问应用** 135 | 部署完成后,Zeabur 会提供访问 URL 136 | 137 | ### Zeabur 配置说明 138 | 139 | 项目包含 `zeabur.json` 配置文件,定义了: 140 | - 主应用服务(端口 8000) 141 | - MySQL 数据库服务 142 | - ChromaDB 向量数据库服务 143 | - 必要的数据卷和环境变量映射 144 | 145 | ## 🔧 高级配置 146 | 147 | ### 自定义 Nginx 配置 148 | 149 | 如果需要自定义 Nginx 配置,编辑 `nginx.conf` 文件: 150 | 151 | ```nginx 152 | # 自定义配置示例 153 | server { 154 | listen 80; 155 | server_name your-domain.com; 156 | 157 | # 其他配置... 158 | } 159 | ``` 160 | 161 | ### 数据库初始化 162 | 163 | 项目包含 `init.sql` 文件,会在 MySQL 首次启动时自动执行: 164 | - 创建必要的数据库表 165 | - 插入默认管理员用户 166 | - 设置索引优化 167 | 168 | 默认管理员账户: 169 | - 用户名: `admin` 170 | - 密码: `admin123` 171 | 172 | ### 数据持久化 173 | 174 | Docker Compose 配置了以下数据卷: 175 | - `mysql_data`: MySQL 数据 176 | - `chroma_data`: ChromaDB 数据 177 | - `app_data`: 应用数据 178 | - `app_logs`: 应用日志 179 | 180 | ### 性能优化 181 | 182 | 对于生产环境,建议: 183 | 184 | 1. **资源限制** 185 | ```yaml 186 | services: 187 | app: 188 | deploy: 189 | resources: 190 | limits: 191 | memory: 2G 192 | cpus: '1.0' 193 | ``` 194 | 195 | 2. **日志轮转** 196 | ```yaml 197 | logging: 198 | driver: "json-file" 199 | options: 200 | max-size: "10m" 201 | max-file: "3" 202 | ``` 203 | 204 | ## 🛠️ 故障排除 205 | 206 | ### 常见问题 207 | 208 | 1. **前端访问 404** 209 | - 确保前端构建成功 210 | - 检查 `static` 目录是否存在 211 | 212 | 2. **数据库连接失败** 213 | - 检查环境变量配置 214 | - 确保 MySQL 服务正常启动 215 | - 查看数据库日志:`docker-compose logs mysql` 216 | 217 | 3. **ChromaDB 连接失败** 218 | - 检查 ChromaDB 服务状态 219 | - 验证端口配置 220 | - 查看日志:`docker-compose logs chromadb` 221 | 222 | 4. **API 请求失败** 223 | - 检查后端服务日志 224 | - 验证环境变量设置 225 | - 确认 API 路径正确(/api 前缀) 226 | 227 | ### 日志查看 228 | 229 | ```bash 230 | # 查看应用日志 231 | docker-compose logs app 232 | 233 | # 查看数据库日志 234 | docker-compose logs mysql 235 | 236 | # 查看向量数据库日志 237 | docker-compose logs chromadb 238 | 239 | # 实时查看所有日志 240 | docker-compose logs -f 241 | ``` 242 | 243 | ### 重新部署 244 | 245 | ```bash 246 | # 停止所有服务 247 | docker-compose down 248 | 249 | # 重新构建并启动 250 | docker-compose up -d --build 251 | 252 | # 清理旧镜像(可选) 253 | docker image prune -f 254 | ``` 255 | 256 | ## 📚 相关文档 257 | 258 | - [API 文档](api_documentation_guide_ZHCN.md) 259 | - [技术架构](technical_architecture_guide_ZHCN.md) 260 | - [项目需求](candidate_project_requirements_ZHCN.md) 261 | 262 | ## 🆘 获取帮助 263 | 264 | 如果在部署过程中遇到问题: 265 | 266 | 1. 检查日志文件 267 | 2. 验证环境变量配置 268 | 3. 确认网络和端口设置 269 | 4. 查看相关文档 270 | 271 | --- 272 | 273 | 部署完成后,你就可以通过浏览器访问 AI 个人日常助手了!🎉 -------------------------------------------------------------------------------- /backend/remote_api/weather/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Weather API Client 3 | 4 | Author: Andrew Wang 5 | """ 6 | 7 | from typing import Optional, List 8 | from core.http_core.client import APIClient 9 | from .models import ( 10 | WeatherRequest, WeatherApiResponse, 11 | get_weather_condition_description 12 | ) 13 | 14 | 15 | class WeatherClient: 16 | """Open-Meteo Weather API Client""" 17 | 18 | def __init__(self): 19 | self.client = APIClient("https://api.open-meteo.com/v1") 20 | 21 | def get_current_weather(self, latitude: float, longitude: float) -> Optional[WeatherApiResponse]: 22 | """ 23 | Get current weather conditions 24 | 25 | Args: 26 | latitude: Location latitude in decimal degrees 27 | longitude: Location longitude in decimal degrees 28 | 29 | Returns: 30 | WeatherApiResponse entity or None 31 | """ 32 | params = { 33 | "latitude": latitude, 34 | "longitude": longitude, 35 | "current_weather": True 36 | } 37 | 38 | data = self.client.get("/forecast", params=params) 39 | if data: 40 | return WeatherApiResponse.from_dict(data) 41 | return None 42 | 43 | def get_daily_forecast(self, latitude: float, longitude: float, 44 | forecast_days: int = 7) -> Optional[WeatherApiResponse]: 45 | """ 46 | Get daily weather forecast 47 | 48 | Args: 49 | latitude: Location latitude in decimal degrees 50 | longitude: Location longitude in decimal degrees 51 | forecast_days: Number of forecast days (1-16, default 7) 52 | 53 | Returns: 54 | WeatherApiResponse entity or None 55 | """ 56 | params = { 57 | "latitude": latitude, 58 | "longitude": longitude, 59 | "forecast_days": forecast_days, 60 | "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode,sunrise,sunset" 61 | } 62 | 63 | data = self.client.get("/forecast", params=params) 64 | if data: 65 | return WeatherApiResponse.from_dict(data) 66 | return None 67 | 68 | def get_hourly_forecast(self, latitude: float, longitude: float, 69 | forecast_days: int = 3) -> Optional[WeatherApiResponse]: 70 | """ 71 | Get hourly weather forecast 72 | 73 | Args: 74 | latitude: Location latitude in decimal degrees 75 | longitude: Location longitude in decimal degrees 76 | forecast_days: Number of forecast days (1-16, default 3) 77 | 78 | Returns: 79 | WeatherApiResponse entity or None 80 | """ 81 | params = { 82 | "latitude": latitude, 83 | "longitude": longitude, 84 | "forecast_days": forecast_days, 85 | "hourly": "temperature_2m,precipitation,weathercode,windspeed_10m" 86 | } 87 | 88 | data = self.client.get("/forecast", params=params) 89 | if data: 90 | return WeatherApiResponse.from_dict(data) 91 | return None 92 | 93 | def get_weather_forecast(self, latitude: float, longitude: float, 94 | forecast_days: int = 7, 95 | daily_vars: Optional[List[str]] = None, 96 | hourly_vars: Optional[List[str]] = None) -> Optional[WeatherApiResponse]: 97 | """ 98 | Get custom weather forecast with specified variables 99 | 100 | Args: 101 | latitude: Location latitude in decimal degrees 102 | longitude: Location longitude in decimal degrees 103 | forecast_days: Number of forecast days (1-16) 104 | daily_vars: List of daily forecast variables 105 | hourly_vars: List of hourly forecast variables 106 | 107 | Returns: 108 | WeatherApiResponse entity or None 109 | """ 110 | params = { 111 | "latitude": latitude, 112 | "longitude": longitude, 113 | "forecast_days": forecast_days 114 | } 115 | 116 | if daily_vars: 117 | params["daily"] = ",".join(daily_vars) 118 | 119 | if hourly_vars: 120 | params["hourly"] = ",".join(hourly_vars) 121 | 122 | data = self.client.get("/forecast", params=params) 123 | if data: 124 | return WeatherApiResponse.from_dict(data) 125 | return None 126 | 127 | def get_categories(self) -> List[str]: 128 | """ 129 | Get available weather forecast categories 130 | 131 | Returns: 132 | List of forecast categories 133 | """ 134 | return ["current", "daily", "hourly"] 135 | 136 | def get_available_daily_variables(self) -> List[str]: 137 | """ 138 | Get available daily forecast variables 139 | 140 | Returns: 141 | List of daily variables 142 | """ 143 | return [ 144 | "temperature_2m_max", "temperature_2m_min", "precipitation_sum", 145 | "weathercode", "sunrise", "sunset", "windspeed_10m_max", 146 | "windgusts_10m_max", "winddirection_10m_dominant" 147 | ] 148 | 149 | def get_available_hourly_variables(self) -> List[str]: 150 | """ 151 | Get available hourly forecast variables 152 | 153 | Returns: 154 | List of hourly variables 155 | """ 156 | return [ 157 | "temperature_2m", "precipitation", "weathercode", "windspeed_10m", 158 | "winddirection_10m", "cloudcover", "pressure_msl", "surface_pressure" 159 | ] -------------------------------------------------------------------------------- /backend/core/vector_core/README.md: -------------------------------------------------------------------------------- 1 | # Vector Core - Chroma Vector Database with OpenAI Embeddings 2 | 3 | 这个模块提供了一个基于 Chroma 向量数据库的完整解决方案,使用 OpenAI 的嵌入模型进行文本向量化存储和检索。 4 | 5 | ## 功能特性 6 | 7 | 1. **文本向量存储** - 将文本转换为向量并存储在本地数据库中 8 | 2. **自定义元数据支持** - 为每个文档添加自定义元数据信息 9 | 3. **元数据过滤查询** - 基于元数据条件进行精确查询 10 | 4. **语义相似性查询** - 基于文本语义进行相似性搜索 11 | 5. **用户隔离** - 每个用户拥有独立的数据集合 12 | 6. **OpenAI 嵌入模型** - 使用 OpenAI 的 text-embedding-3-small 模型 13 | 7. **源标签筛选删除** - 基于源标签进行批量删除操作 14 | 8. **本地持久化存储** - 数据本地存储,支持重启后数据恢复 15 | 16 | ## 安装依赖 17 | 18 | 确保已安装以下依赖项: 19 | 20 | ```bash 21 | pip install chromadb>=0.4.22 openai>=1.0.0 numpy>=1.26.0 pydantic>=2.0.0 22 | ``` 23 | 24 | ## 环境变量配置 25 | 26 | 创建 `.env` 文件并设置以下环境变量: 27 | 28 | ```env 29 | # 必需配置 30 | OPENAI_API_KEY=your-openai-api-key-here 31 | 32 | # 可选配置 33 | OPENAI_EMBEDDING_MODEL=text-embedding-3-small 34 | CHROMA_PERSIST_DIR=./chroma_db 35 | CHROMA_COLLECTION_PREFIX=ai_assistant 36 | VECTOR_DIMENSION=1536 37 | SIMILARITY_THRESHOLD=0.7 38 | DEFAULT_QUERY_LIMIT=10 39 | ``` 40 | 41 | ## 快速开始 42 | 43 | ### 1. 初始化客户端 44 | 45 | ```python 46 | from backend.core.vector_core import ChromaVectorClient, VectorConfig 47 | 48 | # 从环境变量加载配置 49 | config = VectorConfig.from_env() 50 | client = ChromaVectorClient(config) 51 | ``` 52 | 53 | ### 2. 添加文档 54 | 55 | ```python 56 | from backend.core.vector_core import VectorDocument 57 | 58 | # 创建文档 59 | document = VectorDocument( 60 | id="doc_1", 61 | text="Python is a powerful programming language", 62 | metadata={"category": "programming", "language": "python"}, 63 | user_id="user_123", 64 | source="tutorial_docs" 65 | ) 66 | 67 | # 添加到向量数据库 68 | doc_id = client.add_document(document) 69 | ``` 70 | 71 | ### 3. 查询文档 72 | 73 | ```python 74 | from backend.core.vector_core import VectorQuery 75 | 76 | # 创建查询 77 | query = VectorQuery( 78 | query_text="programming language", 79 | user_id="user_123", 80 | limit=5, 81 | similarity_threshold=0.5 82 | ) 83 | 84 | # 执行查询 85 | results = client.query_documents(query) 86 | for result in results: 87 | print(f"ID: {result.id}, Score: {result.score:.3f}") 88 | print(f"Text: {result.text}") 89 | ``` 90 | 91 | ### 4. 元数据过滤查询 92 | 93 | ```python 94 | # 基于元数据过滤 95 | query_filtered = VectorQuery( 96 | query_text="programming", 97 | user_id="user_123", 98 | metadata_filter={"category": "programming"}, 99 | limit=10 100 | ) 101 | 102 | results = client.query_documents(query_filtered) 103 | ``` 104 | 105 | ### 5. 删除文档 106 | 107 | ```python 108 | from backend.core.vector_core import VectorDeleteFilter 109 | 110 | # 按源标签删除 111 | delete_filter = VectorDeleteFilter( 112 | user_id="user_123", 113 | source_filter="tutorial_docs" 114 | ) 115 | 116 | deleted_count = client.delete_documents(delete_filter) 117 | ``` 118 | 119 | ## 主要类和方法 120 | 121 | ### ChromaVectorClient 122 | 123 | 主要的向量数据库客户端类。 124 | 125 | #### 方法: 126 | 127 | - `add_document(document: VectorDocument) -> str` - 添加单个文档 128 | - `add_documents(documents: List[VectorDocument]) -> List[str]` - 批量添加文档 129 | - `query_documents(query: VectorQuery) -> List[VectorQueryResult]` - 查询文档 130 | - `delete_documents(delete_filter: VectorDeleteFilter) -> int` - 删除文档 131 | - `get_document_by_id(document_id: str, user_id: str) -> Optional[VectorQueryResult]` - 按ID获取文档 132 | - `get_stats(user_id: str) -> VectorStats` - 获取用户统计信息 133 | - `update_document(document_id: str, user_id: str, text: str, metadata: Dict) -> bool` - 更新文档 134 | - `clear_user_data(user_id: str) -> bool` - 清除用户所有数据 135 | - `health_check() -> Dict[str, Any]` - 健康检查 136 | 137 | ### VectorDocument 138 | 139 | 文档数据模型: 140 | 141 | ```python 142 | VectorDocument( 143 | id: str, # 文档ID 144 | text: str, # 文档内容 145 | metadata: Dict[str, Any], # 自定义元数据 146 | user_id: str, # 用户ID 147 | source: Optional[str], # 源标签 148 | created_at: datetime # 创建时间 149 | ) 150 | ``` 151 | 152 | ### VectorQuery 153 | 154 | 查询参数模型: 155 | 156 | ```python 157 | VectorQuery( 158 | query_text: str, # 查询文本 159 | user_id: str, # 用户ID 160 | limit: int = 10, # 结果数量限制 161 | similarity_threshold: float = None, # 相似度阈值 162 | metadata_filter: Dict = None, # 元数据过滤 163 | source_filter: str = None, # 源标签过滤 164 | include_metadata: bool = True, # 是否包含元数据 165 | include_distances: bool = True # 是否包含距离信息 166 | ) 167 | ``` 168 | 169 | ### VectorDeleteFilter 170 | 171 | 删除过滤器模型: 172 | 173 | ```python 174 | VectorDeleteFilter( 175 | user_id: str, # 用户ID 176 | source_filter: str = None, # 源标签过滤 177 | metadata_filter: Dict = None, # 元数据过滤 178 | document_ids: List[str] = None # 特定文档ID列表 179 | ) 180 | ``` 181 | 182 | ## 完整示例 183 | 184 | 查看 `example_usage.py` 文件获取完整的使用示例,包括: 185 | 186 | - 文档添加和批量添加 187 | - 基本查询和过滤查询 188 | - 用户隔离测试 189 | - 文档更新和删除 190 | - 统计信息获取 191 | - 数据清理 192 | 193 | ## 运行示例 194 | 195 | 要运行示例,请从 `backend` 目录执行以下命令: 196 | 197 | ```bash 198 | python -m core.vector_core.example_usage 199 | ``` 200 | 201 | 这会将 `example_usage.py` 作为模块运行,确保所有内部导入都能正常工作。 202 | 203 | ## 注意事项 204 | 205 | 1. **OpenAI API Key** - 确保设置了有效的 OpenAI API Key 206 | 2. **数据持久化** - 数据存储在本地,重启后数据保留 207 | 3. **用户隔离** - 每个用户的数据完全隔离,不会相互影响 208 | 4. **元数据限制** - 元数据值仅支持 str、int、float、bool 类型 209 | 5. **文本长度** - 建议单个文档不超过 8000 个字符以确保最佳性能 210 | 211 | ## 故障排除 212 | 213 | ### 常见问题 214 | 215 | 1. **OpenAI API Key 错误** - 检查环境变量是否正确设置 216 | 2. **权限问题** - 确保有权限创建和写入数据库目录 217 | 3. **依赖版本冲突** - 确保使用推荐的依赖版本 218 | 219 | ### 日志记录 220 | 221 | 模块使用标准的 Python logging 模块。可以通过设置日志级别来调试问题: 222 | 223 | ```python 224 | import logging 225 | logging.basicConfig(level=logging.INFO) 226 | ``` 227 | 228 | ## 性能优化 229 | 230 | 1. **批量操作** - 尽量使用 `add_documents()` 进行批量添加 231 | 2. **合理的查询限制** - 设置合适的 `limit` 值 232 | 3. **相似度阈值** - 使用 `similarity_threshold` 过滤低相关性结果 233 | 4. **定期清理** - 定期删除不需要的文档以保持性能 -------------------------------------------------------------------------------- /backend/remote_api/jsonplaceholder/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | JSONPlaceholder API Client 3 | 4 | Author: Andrew Wang 5 | """ 6 | 7 | import json 8 | from typing import Optional, List 9 | from core.http_core.client import APIClient 10 | from .models import ( 11 | User, Post, Comment, Todo, 12 | UsersApiResponse, UserApiResponse, PostsApiResponse, CommentsApiResponse, TodosApiResponse, 13 | format_user, format_post, format_comment, format_todo, 14 | format_user_summary, format_post_summary, format_comment_summary, format_todo_summary 15 | ) 16 | 17 | 18 | class JSONPlaceholderClient: 19 | """JSONPlaceholder API Client""" 20 | 21 | def __init__(self): 22 | self.client = APIClient("https://jsonplaceholder.typicode.com") 23 | 24 | def get_users(self) -> Optional[UsersApiResponse]: 25 | """Get all users""" 26 | data = self.client.get("/users") 27 | if data and isinstance(data, list): 28 | return UsersApiResponse.from_list(data) 29 | return None 30 | 31 | def get_user(self, user_id: int) -> Optional[UserApiResponse]: 32 | """Get specific user by ID""" 33 | data = self.client.get(f"/users/{user_id}") 34 | if data: 35 | return UserApiResponse.from_dict(data) 36 | return None 37 | 38 | def get_user_posts(self, user_id: int) -> Optional[PostsApiResponse]: 39 | """Get user's posts""" 40 | data = self.client.get(f"/users/{user_id}/posts") 41 | if data and isinstance(data, list): 42 | return PostsApiResponse.from_list(data) 43 | return None 44 | 45 | def get_user_comments(self, user_id: int) -> Optional[CommentsApiResponse]: 46 | """Get user's comments (comments on posts by the user)""" 47 | # First get all posts by this user 48 | posts_data = self.client.get(f"/users/{user_id}/posts") 49 | if not posts_data or not isinstance(posts_data, list): 50 | return None 51 | 52 | # Then get comments for each post 53 | all_comments = [] 54 | for post in posts_data: 55 | post_id = post["id"] 56 | comments_data = self.client.get(f"/posts/{post_id}/comments") 57 | if comments_data and isinstance(comments_data, list): 58 | all_comments.extend(comments_data) 59 | 60 | if all_comments: 61 | return CommentsApiResponse.from_list(all_comments) 62 | return None 63 | 64 | def get_post_comments(self, post_id: int) -> Optional[CommentsApiResponse]: 65 | """Get post's comments""" 66 | data = self.client.get(f"/posts/{post_id}/comments") 67 | if data and isinstance(data, list): 68 | return CommentsApiResponse.from_list(data) 69 | return None 70 | 71 | def get_user_todos(self, user_id: int) -> Optional[TodosApiResponse]: 72 | """Get user's todos""" 73 | data = self.client.get(f"/users/{user_id}/todos") 74 | if data and isinstance(data, list): 75 | return TodosApiResponse.from_list(data) 76 | return None 77 | 78 | def format_users_list(self, users_response: UsersApiResponse, max_items: int = 10) -> str: 79 | """Format users list""" 80 | if not users_response.users: 81 | return "No users found" 82 | 83 | formatted = [] 84 | for i, user in enumerate(users_response.users[:max_items]): 85 | formatted.append(f"{i+1}. {format_user_summary(user)}") 86 | 87 | if len(users_response.users) > max_items: 88 | formatted.append(f"... {len(users_response.users) - max_items} more users") 89 | 90 | return "\n".join(formatted) 91 | 92 | def format_posts_list(self, posts_response: PostsApiResponse, max_items: int = 10) -> str: 93 | """Format posts list""" 94 | if not posts_response.posts: 95 | return "No posts found" 96 | 97 | formatted = [] 98 | for i, post in enumerate(posts_response.posts[:max_items]): 99 | formatted.append(f"{i+1}. {format_post_summary(post)}") 100 | 101 | if len(posts_response.posts) > max_items: 102 | formatted.append(f"... {len(posts_response.posts) - max_items} more posts") 103 | 104 | return "\n".join(formatted) 105 | 106 | def format_comments_list(self, comments_response: CommentsApiResponse, max_items: int = 10) -> str: 107 | """Format comments list""" 108 | if not comments_response.comments: 109 | return "No comments found" 110 | 111 | formatted = [] 112 | for i, comment in enumerate(comments_response.comments[:max_items]): 113 | formatted.append(f"{i+1}. {format_comment_summary(comment)}") 114 | 115 | if len(comments_response.comments) > max_items: 116 | formatted.append(f"... {len(comments_response.comments) - max_items} more comments") 117 | 118 | return "\n".join(formatted) 119 | 120 | def format_todos_list(self, todos_response: TodosApiResponse, max_items: int = 10) -> str: 121 | """Format todos list""" 122 | if not todos_response.todos: 123 | return "No todos found" 124 | 125 | formatted = [] 126 | for i, todo in enumerate(todos_response.todos[:max_items]): 127 | formatted.append(f"{i+1}. {format_todo_summary(todo)}") 128 | 129 | if len(todos_response.todos) > max_items: 130 | formatted.append(f"... {len(todos_response.todos) - max_items} more todos") 131 | 132 | return "\n".join(formatted) 133 | 134 | def get_user_details(self, user_id: int) -> str: 135 | """Get user details""" 136 | user_response = self.get_user(user_id) 137 | if user_response: 138 | return format_user(user_response.user) 139 | return f"User {user_id} not found" -------------------------------------------------------------------------------- /backend/service/services/user_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | 用户服务 3 | 4 | 整合JSONPlaceholder API用户数据,提供用户相关业务逻辑 5 | """ 6 | 7 | from typing import Optional, List 8 | from remote_api.jsonplaceholder.client import JSONPlaceholderClient 9 | from remote_api.jsonplaceholder.models import User 10 | 11 | 12 | class UserService: 13 | """ 14 | 用户服务类 15 | 16 | 提供用户相关的业务逻辑,用户数据来自JSONPlaceholder API 17 | """ 18 | 19 | def __init__(self): 20 | """初始化用户服务""" 21 | self.client = JSONPlaceholderClient() 22 | 23 | def get_user(self, user_id: int) -> Optional[User]: 24 | """ 25 | 获取用户信息 26 | 27 | Args: 28 | user_id: 用户ID 29 | 30 | Returns: 31 | 用户信息或None 32 | """ 33 | try: 34 | user_response = self.client.get_user(user_id) 35 | if user_response: 36 | return user_response.user 37 | return None 38 | except Exception as e: 39 | print(f"获取用户信息失败: {e}") 40 | return None 41 | 42 | def get_all_users(self) -> List[User]: 43 | """ 44 | 获取所有用户信息 45 | 46 | Returns: 47 | 用户列表 48 | """ 49 | try: 50 | users_response = self.client.get_users() 51 | if users_response: 52 | return users_response.users 53 | return [] 54 | except Exception as e: 55 | print(f"获取所有用户失败: {e}") 56 | return [] 57 | 58 | def get_user_posts(self, user_id: int): 59 | """ 60 | 获取用户的帖子 61 | 62 | Args: 63 | user_id: 用户ID 64 | 65 | Returns: 66 | 帖子列表 67 | """ 68 | try: 69 | posts_response = self.client.get_user_posts(user_id) 70 | if posts_response: 71 | return posts_response.posts 72 | return [] 73 | except Exception as e: 74 | print(f"获取用户帖子失败: {e}") 75 | return [] 76 | 77 | def get_user_todos(self, user_id: int): 78 | """ 79 | 获取用户的待办事项(JSONPlaceholder API) 80 | 81 | Args: 82 | user_id: 用户ID 83 | 84 | Returns: 85 | 待办事项列表 86 | """ 87 | try: 88 | todos_response = self.client.get_user_todos(user_id) 89 | if todos_response: 90 | return todos_response.todos 91 | return [] 92 | except Exception as e: 93 | print(f"获取用户待办事项失败: {e}") 94 | return [] 95 | 96 | def validate_user_exists(self, user_id: int) -> bool: 97 | """ 98 | 验证用户是否存在 99 | 100 | Args: 101 | user_id: 用户ID 102 | 103 | Returns: 104 | 是否存在 105 | """ 106 | user = self.get_user(user_id) 107 | return user is not None 108 | 109 | def get_user_display_name(self, user_id: int) -> str: 110 | """ 111 | 获取用户显示名称 112 | 113 | Args: 114 | user_id: 用户ID 115 | 116 | Returns: 117 | 用户显示名称 118 | """ 119 | user = self.get_user(user_id) 120 | if user: 121 | return user.name 122 | return f"用户 {user_id}" 123 | 124 | def get_user_email(self, user_id: int) -> Optional[str]: 125 | """ 126 | 获取用户邮箱 127 | 128 | Args: 129 | user_id: 用户ID 130 | 131 | Returns: 132 | 用户邮箱或None 133 | """ 134 | user = self.get_user(user_id) 135 | if user: 136 | return user.email 137 | return None 138 | 139 | def search_users_by_name(self, name: str) -> List[User]: 140 | """ 141 | 根据姓名搜索用户 142 | 143 | Args: 144 | name: 搜索的姓名 145 | 146 | Returns: 147 | 匹配的用户列表 148 | """ 149 | all_users = self.get_all_users() 150 | return [ 151 | user for user in all_users 152 | if name.lower() in user.name.lower() 153 | ] 154 | 155 | def search_users_by_username(self, username: str) -> List[User]: 156 | """ 157 | 根据姓名搜索用户 158 | 159 | Args: 160 | name: 搜索的姓名 161 | 162 | Returns: 163 | 匹配的用户列表 164 | """ 165 | all_users = self.get_all_users() 166 | return [ 167 | user for user in all_users 168 | if username.lower() in user.username.lower() 169 | ] 170 | 171 | def search_users_by_email(self, email: str) -> List[User]: 172 | """ 173 | 根据邮箱搜索用户 174 | 175 | Args: 176 | email: 搜索的邮箱 177 | 178 | Returns: 179 | 匹配的用户列表 180 | """ 181 | all_users = self.get_all_users() 182 | return [ 183 | user for user in all_users 184 | if email.lower() in user.email.lower() 185 | ] 186 | 187 | def get_user_summary(self, user_id: int) -> dict: 188 | """ 189 | 获取用户概要信息 190 | 191 | Args: 192 | user_id: 用户ID 193 | 194 | Returns: 195 | 用户概要信息字典 196 | """ 197 | user = self.get_user(user_id) 198 | if not user: 199 | return {} 200 | 201 | return { 202 | 'id': user.id, 203 | 'name': user.name, 204 | 'username': user.username, 205 | 'email': user.email, 206 | 'phone': user.phone, 207 | 'website': user.website, 208 | 'company': user.company.name if user.company else None, 209 | 'address': { 210 | 'city': user.address.city if user.address else None, 211 | 'zipcode': user.address.zipcode if user.address else None 212 | } 213 | } -------------------------------------------------------------------------------- /backend/agent/example_usage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 个人助手管理器使用示例 4 | 展示如何使用 PersonalAssistantManager 类 5 | """ 6 | 7 | import asyncio 8 | import sys 9 | from pathlib import Path 10 | 11 | # Add backend directory to Python path 12 | backend_dir = Path(__file__).parent.parent 13 | sys.path.insert(0, str(backend_dir)) 14 | 15 | from core.database_core import DatabaseClient 16 | from agent.personal_assistant_manager import PersonalAssistantManager, PersonalAssistantContext 17 | from agents import Runner 18 | 19 | 20 | async def main(): 21 | """主函数 - 演示如何使用PersonalAssistantManager""" 22 | 23 | print("=" * 50) 24 | print("🚀 个人助手管理器使用示例") 25 | print("=" * 50) 26 | 27 | # 1. 初始化数据库客户端 28 | print("\n📁 步骤1: 初始化数据库客户端") 29 | db_client = DatabaseClient() 30 | db_client.initialize() 31 | db_client.create_tables() 32 | print("✅ 数据库客户端初始化完成") 33 | 34 | # 2. 创建个人助手管理器 35 | print("\n🤖 步骤2: 创建个人助手管理器") 36 | manager = PersonalAssistantManager( 37 | db_client=db_client, 38 | mcp_server_url="http://127.0.0.1:8002/mcp" 39 | ) 40 | 41 | # 3. 初始化管理器 42 | print("\n⚙️ 步骤3: 初始化管理器") 43 | success = await manager.initialize() 44 | 45 | if not success: 46 | print("❌ 管理器初始化失败,程序退出") 47 | return 48 | 49 | # 4. 创建用户上下文 50 | print("\n👤 步骤4: 创建用户上下文") 51 | user_id = 1 52 | context = manager.create_user_context(user_id) 53 | print(f"✅ 用户上下文创建完成: {context.user_name}") 54 | 55 | # 5. 展示可用的智能体 56 | print("\n🤖 步骤5: 可用的智能体") 57 | print(f"可用智能体: {manager.available_agents}") 58 | print(f"管理器状态: {manager}") 59 | 60 | # 6. 获取任务调度中心智能体 61 | print("\n🎯 步骤6: 获取任务调度中心智能体") 62 | try: 63 | triage_agent = manager.get_triage_agent() 64 | print(f"✅ 获取任务调度中心智能体: {triage_agent.name}") 65 | 66 | # 7. 使用智能体处理用户请求 67 | print("\n💬 步骤7: 使用智能体处理用户请求") 68 | 69 | # 示例对话 70 | test_messages = [ 71 | "你好,我是新用户,请介绍一下你的功能", 72 | "今天天气怎么样?", 73 | "有什么新闻吗?", 74 | "推荐一个菜谱", 75 | "帮我记录一个待办事项:明天买菜" 76 | ] 77 | 78 | for i, message in enumerate(test_messages, 1): 79 | print(f"\n--- 对话 {i} ---") 80 | print(f"用户: {message}") 81 | 82 | try: 83 | # 运行智能体 84 | response = await Runner.run( 85 | triage_agent, 86 | message, 87 | context=context 88 | ) 89 | 90 | print(f"助手: 处理完成") 91 | 92 | # 显示响应摘要 93 | if hasattr(response, 'final_output') and response.final_output: 94 | content_preview = response.final_output[:100] + "..." if len(response.final_output) > 100 else response.final_output 95 | print(f" 响应: {content_preview}") 96 | 97 | except Exception as e: 98 | print(f"❌ 处理消息失败: {e}") 99 | 100 | except Exception as e: 101 | print(f"❌ 获取智能体失败: {e}") 102 | 103 | # 8. 演示单个智能体的使用 104 | print("\n🔧 步骤8: 演示单个智能体的使用") 105 | try: 106 | # 获取天气智能体 107 | weather_agent = manager.get_weather_agent() 108 | print(f"✅ 获取天气智能体: {weather_agent.name}") 109 | 110 | # 获取新闻智能体 111 | news_agent = manager.get_news_agent() 112 | print(f"✅ 获取新闻智能体: {news_agent.name}") 113 | 114 | # 获取菜谱智能体 115 | recipe_agent = manager.get_recipe_agent() 116 | print(f"✅ 获取菜谱智能体: {recipe_agent.name}") 117 | 118 | # 获取个人助手智能体 119 | personal_agent = manager.get_personal_agent() 120 | print(f"✅ 获取个人助手智能体: {personal_agent.name}") 121 | 122 | except Exception as e: 123 | print(f"❌ 获取智能体失败: {e}") 124 | 125 | # 9. 演示上下文刷新功能 126 | print("\n🔄 步骤9: 演示上下文刷新功能") 127 | try: 128 | print("刷新用户偏好...") 129 | # 注意:refresh_user_preferences 和 refresh_user_todos 方法 130 | # 通常在智能体运行过程中自动调用,这里仅为演示目的 131 | print(" (在实际使用中,这些方法会在智能体运行时自动调用)") 132 | 133 | print("刷新用户待办事项...") 134 | print(" (在实际使用中,这些方法会在智能体运行时自动调用)") 135 | 136 | print("✅ 上下文刷新功能说明完成") 137 | 138 | except Exception as e: 139 | print(f"❌ 上下文刷新失败: {e}") 140 | 141 | print("\n" + "=" * 50) 142 | print("🎉 个人助手管理器使用示例完成") 143 | print("=" * 50) 144 | 145 | 146 | def create_simple_usage_example(): 147 | """简单使用示例""" 148 | print("\n" + "=" * 30) 149 | print("💡 简单使用示例") 150 | print("=" * 30) 151 | 152 | example_code = ''' 153 | # 简单使用示例 154 | import asyncio 155 | from core.database_core import DatabaseClient 156 | from agent.personal_assistant_manager import PersonalAssistantManager 157 | 158 | async def simple_example(): 159 | # 1. 初始化数据库 160 | db_client = DatabaseClient() 161 | db_client.initialize() 162 | db_client.create_tables() 163 | 164 | # 2. 创建管理器 165 | manager = PersonalAssistantManager(db_client) 166 | 167 | # 3. 初始化管理器 168 | await manager.initialize() 169 | 170 | # 4. 创建用户上下文 171 | context = manager.create_user_context(user_id=1) 172 | 173 | # 5. 获取智能体 174 | agent = manager.get_triage_agent() 175 | 176 | # 6. 使用智能体 177 | from agents import Runner 178 | response = await Runner.run(agent, "你好", context=context) 179 | 180 | print(f"助手回复: {response.final_output}") 181 | 182 | # 运行示例 183 | asyncio.run(simple_example()) 184 | ''' 185 | 186 | print(example_code) 187 | 188 | 189 | if __name__ == "__main__": 190 | try: 191 | # 运行主示例 192 | asyncio.run(main()) 193 | 194 | # 显示简单使用示例 195 | create_simple_usage_example() 196 | 197 | except KeyboardInterrupt: 198 | print("\n\n⏹️ 程序被用户中断") 199 | except Exception as e: 200 | print(f"\n\n❌ 程序运行出错: {e}") 201 | import traceback 202 | traceback.print_exc() -------------------------------------------------------------------------------- /backend/core/web_socket_core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | WebSocket 核心模块 (WebSocket Core Module) 3 | 4 | AI 个人日常助手项目的 WebSocket 核心功能模块 5 | 提供完整的 WebSocket 连接管理、消息处理、房间管理等功能 6 | 7 | 主要组件 (Main Components): 8 | - models: 数据模型定义 (Data model definitions) 9 | - manager: 连接管理器 (Connection manager) 10 | - handler: 消息处理器 (Message handler) 11 | - utils: 工具函数 (Utility functions) 12 | 13 | 使用示例 (Usage Example): 14 | ```python 15 | from backend.core.web_socket_core import ( 16 | connection_manager, 17 | WebSocketMessageHandler, 18 | WebSocketMessage, 19 | MessageType, 20 | UserInfo 21 | ) 22 | 23 | # 创建消息处理器 (Create message handler) 24 | handler = WebSocketMessageHandler(connection_manager) 25 | 26 | # 创建用户信息 (Create user info) 27 | user = UserInfo(user_id="user123", username="张三") 28 | 29 | # 创建消息 (Create message) 30 | message = WebSocketMessage( 31 | type=MessageType.CHAT, 32 | content="你好!(Hello!)", 33 | sender_id="user123" 34 | ) 35 | ``` 36 | """ 37 | 38 | # 导入数据模型 (Import data models) 39 | from .models import ( 40 | MessageType, 41 | ConnectionStatus, 42 | UserInfo, 43 | WebSocketMessage, 44 | ConnectionInfo, 45 | RoomInfo, 46 | BroadcastMessage, 47 | WebSocketError 48 | ) 49 | 50 | # 导入连接管理器 (Import connection manager) 51 | from .manager import ( 52 | WebSocketConnectionManager, 53 | connection_manager # 全局单例实例 (Global singleton instance) 54 | ) 55 | 56 | # 导入消息处理器 (Import message handler) 57 | from .handler import ( 58 | WebSocketMessageHandler, 59 | create_message_handler 60 | ) 61 | 62 | # 导入工具函数 (Import utility functions) 63 | from .utils import ( 64 | generate_connection_id, 65 | generate_room_id, 66 | validate_message, 67 | parse_websocket_message, 68 | serialize_message, 69 | extract_query_params, 70 | validate_user_info, 71 | create_error_message, 72 | format_connection_info, 73 | calculate_connection_duration, 74 | is_connection_healthy, 75 | generate_message_hash, 76 | filter_connections_by_criteria, 77 | create_system_notification, 78 | sanitize_user_input, 79 | get_client_info_from_headers, 80 | create_message_from_template, 81 | MESSAGE_TEMPLATES 82 | ) 83 | 84 | # 模块版本信息 (Module version info) 85 | __version__ = "1.0.0" 86 | __author__ = "AI Personal Daily Assistant Team" 87 | 88 | # 导出的公共接口 (Public API exports) 89 | __all__ = [ 90 | # 数据模型 (Data Models) 91 | "MessageType", 92 | "ConnectionStatus", 93 | "UserInfo", 94 | "WebSocketMessage", 95 | "ConnectionInfo", 96 | "RoomInfo", 97 | "BroadcastMessage", 98 | "WebSocketError", 99 | 100 | # 核心组件 (Core Components) 101 | "WebSocketConnectionManager", 102 | "connection_manager", 103 | "WebSocketMessageHandler", 104 | "create_message_handler", 105 | 106 | # 工具函数 (Utility Functions) 107 | "generate_connection_id", 108 | "generate_room_id", 109 | "validate_message", 110 | "parse_websocket_message", 111 | "serialize_message", 112 | "extract_query_params", 113 | "validate_user_info", 114 | "create_error_message", 115 | "format_connection_info", 116 | "calculate_connection_duration", 117 | "is_connection_healthy", 118 | "generate_message_hash", 119 | "filter_connections_by_criteria", 120 | "create_system_notification", 121 | "sanitize_user_input", 122 | "get_client_info_from_headers", 123 | "create_message_from_template", 124 | "MESSAGE_TEMPLATES", 125 | 126 | # 版本信息 (Version Info) 127 | "__version__", 128 | "__author__" 129 | ] 130 | 131 | # 模块初始化日志 (Module initialization logging) 132 | import logging 133 | logger = logging.getLogger(__name__) 134 | logger.info(f"WebSocket 核心模块已加载 (WebSocket Core Module loaded) - Version {__version__}") 135 | 136 | # 快速启动函数 (Quick start functions) 137 | def create_websocket_server_components(): 138 | """ 139 | 创建 WebSocket 服务器所需的核心组件 (Create core components for WebSocket server) 140 | 141 | Returns: 142 | tuple: (连接管理器, 消息处理器) (Connection manager, Message handler) 143 | """ 144 | manager = connection_manager 145 | handler = create_message_handler(manager) 146 | 147 | logger.info("WebSocket 服务器组件已创建 (WebSocket server components created)") 148 | return manager, handler 149 | 150 | 151 | def get_default_message_handlers(): 152 | """ 153 | 获取默认的消息处理器映射 (Get default message handler mappings) 154 | 155 | Returns: 156 | dict: 消息类型到处理函数的映射 (Message type to handler function mapping) 157 | """ 158 | manager, handler = create_websocket_server_components() 159 | return { 160 | MessageType.PING: handler.handle_ping, 161 | MessageType.PONG: handler.handle_pong, 162 | MessageType.CONNECT: handler.handle_connect, 163 | MessageType.DISCONNECT: handler.handle_disconnect, 164 | MessageType.CHAT: handler.handle_chat, 165 | MessageType.NOTIFICATION: handler.handle_notification, 166 | MessageType.COMMAND: handler.handle_command, 167 | MessageType.DATA: handler.handle_data, 168 | MessageType.AI_RESPONSE: handler.handle_ai_response, 169 | MessageType.AI_THINKING: handler.handle_ai_thinking, 170 | MessageType.AI_ERROR: handler.handle_ai_error 171 | } 172 | 173 | 174 | # 常用配置常量 (Common configuration constants) 175 | class WebSocketConfig: 176 | """WebSocket 配置常量 (WebSocket configuration constants)""" 177 | 178 | # 默认端口 (Default port) 179 | DEFAULT_PORT = 8000 180 | 181 | # 心跳间隔(秒)(Heartbeat interval in seconds) 182 | HEARTBEAT_INTERVAL = 30 183 | 184 | # 连接超时时间(秒)(Connection timeout in seconds) 185 | CONNECTION_TIMEOUT = 90 186 | 187 | # 最大消息长度 (Maximum message length) 188 | MAX_MESSAGE_LENGTH = 10000 189 | 190 | # 最大房间成员数 (Maximum room members) 191 | MAX_ROOM_MEMBERS = 100 192 | 193 | # 支持的消息类型 (Supported message types) 194 | SUPPORTED_MESSAGE_TYPES = [e.value for e in MessageType] 195 | 196 | # 系统用户ID (System user ID) 197 | SYSTEM_USER_ID = "system" 198 | 199 | # 默认房间ID (Default room ID) 200 | DEFAULT_ROOM_ID = "general" 201 | 202 | 203 | # 添加配置到导出列表 (Add config to exports) 204 | __all__.append("WebSocketConfig") --------------------------------------------------------------------------------