From d096aa2b26fb6090e303318e9912e9bdaa72a390 Mon Sep 17 00:00:00 2001 From: 666ghj <670939375@qq.com> Date: Tue, 16 Dec 2025 20:58:48 +0800 Subject: [PATCH] Remove outdated README.md and update favicon in index.html - Deleted the backend README.md file as it was no longer needed. - Changed the favicon from a SVG to a PNG format for better compatibility. - Updated the page title in index.html to reflect a more concise branding message. --- backend/README.md | 2985 -------------------------------------- frontend/index.html | 4 +- frontend/public/icon.png | Bin 0 -> 30341 bytes frontend/public/vite.svg | 1 - 4 files changed, 2 insertions(+), 2988 deletions(-) delete mode 100644 backend/README.md create mode 100644 frontend/public/icon.png delete mode 100644 frontend/public/vite.svg diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 44d437e..0000000 --- a/backend/README.md +++ /dev/null @@ -1,2985 +0,0 @@ -# MiroFish Backend - 详细技术文档 - -## 目录 - -- [项目简介](#项目简介) -- [技术架构](#技术架构) -- [技术栈](#技术栈) -- [项目结构](#项目结构) -- [核心功能模块](#核心功能模块) -- [API接口文档](#api接口文档) -- [数据模型](#数据模型) -- [服务层详解](#服务层详解) -- [工具类](#工具类) -- [配置说明](#配置说明) -- [运行指南](#运行指南) -- [开发指南](#开发指南) -- [常见问题](#常见问题) - ---- - -## 项目简介 - -**MiroFish Backend** 是一个基于 Flask 的后端服务,用于社交媒体舆论模拟。系统核心功能包括: - -1. **知识图谱构建**: 从文档中提取实体和关系,使用 Zep Cloud 构建知识图谱 -2. **本体生成**: 使用 LLM 自动分析文档并生成适合舆论模拟的实体类型和关系类型 -3. **Agent人设生成**: 基于图谱实体,使用 LLM 生成详细的社交媒体用户人设 -4. **模拟配置智能生成**: 使用 LLM 根据需求自动生成模拟参数(时间、活跃度、事件等) -5. **双平台模拟**: 支持 Twitter 和 Reddit 双平台并行舆论模拟(基于 OASIS 框架) -6. **图谱记忆动态更新**: 可选功能,将模拟中Agent的活动实时更新到Zep图谱,让图谱"记住"模拟过程 -7. **智能报告生成**: 使用 LangChain + Zep 实现 ReACT 模式的模拟分析报告自动生成 -8. **Report Agent对话**: 报告生成后可与Report Agent对话,自主调用检索工具回答问题 - ---- - -## 技术架构 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ MiroFish Backend │ -├─────────────────────────────────────────────────────────────┤ -│ Flask Web Framework + CORS │ -│ ┌────────────────┐ ┌──────────────┐ ┌─────────────────┐ │ -│ │ API层 │ │ 服务层 │ │ 模型层 │ │ -│ │ - graph.py │→ │ - 本体生成 │→ │ - Project │ │ -│ │ - simulation │ │ - 图谱构建 │ │ - Task │ │ -│ │ - report.py │ │ - 实体读取 │ │ - Report │ │ -│ └────────────────┘ │ - 人设生成 │ └─────────────────┘ │ -│ │ - 配置生成 │ │ -│ │ - 模拟运行 │ │ -│ │ - 报告生成 │ │ -│ └──────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ 外部服务集成 │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ Zep Cloud│ │ LLM API │ │ OASIS │ │ 文件系统│ │ -│ │ 知识图谱 │ │ (OpenAI) │ │ 社交模拟│ │ 存储 │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 核心流程 - -1. **图谱构建流程**: - ``` - 上传文档 → 提取文本 → LLM生成本体 → 文本分块 → Zep构建图谱 - ``` - -2. **模拟准备流程**: - ``` - 创建模拟 → 读取图谱实体 → LLM生成人设 → LLM生成配置 → 准备完成 - ``` - -3. **模拟运行流程**: - ``` - 启动模拟 → 运行OASIS脚本 → 实时监控 → 记录动作 → (可选)更新Zep图谱记忆 → 状态查询 - ``` - -4. **Interview采访流程**: - ``` - 模拟完成 → 环境进入等待模式 → 发送Interview命令 → Agent回答 → 获取结果 → (可选)关闭环境 - ``` - -5. **报告生成流程**: - ``` - 模拟完成 → 调用Report API → ReACT规划大纲 → 逐章节生成(多次工具调用) → 生成Markdown报告 → 解锁Interview功能 - ``` - -6. **Report Agent对话流程**: - ``` - 用户提问 → Agent分析 → 调用Zep检索工具 → 整合信息 → 返回回答 - ``` - ---- - -## 技术栈 - -### 核心框架 -- **Flask 3.0+**: Web 框架 -- **Flask-CORS**: 跨域支持 - -### AI & 知识图谱 -- **Zep Cloud SDK 2.0+**: 知识图谱构建与管理 -- **OpenAI SDK 1.0+**: LLM 调用(支持 OpenAI 兼容接口) -- **LangChain 0.2+**: Report Agent框架(ReACT模式) -- **OASIS-AI**: 社交媒体模拟框架 -- **CAMEL-AI**: Agent 行为模拟 - -### 数据处理 -- **PyMuPDF (fitz)**: PDF 文本提取 -- **Pydantic 2.0+**: 数据验证 -- **Python-dotenv**: 环境变量管理 - -### 文件处理 -- **Werkzeug 3.0+**: 文件上传处理 - ---- - -## 项目结构 - -``` -backend/ -├── run.py # 启动入口 -├── requirements.txt # Python依赖 -├── .env # 环境配置(需创建) -├── logs/ # 日志文件 -│ └── YYYY-MM-DD.log -├── uploads/ # 数据存储 -│ ├── projects/ # 项目数据 -│ │ └── proj_xxx/ -│ │ ├── project.json # 项目元数据 -│ │ ├── files/ # 上传的文件 -│ │ └── extracted_text.txt # 提取的文本 -│ ├── reports/ # 报告数据 -│ │ └── report_xxx/ -│ │ ├── report_xxx.json # 报告元数据 -│ │ └── report_xxx.md # Markdown报告 -│ └── simulations/ # 模拟数据 -│ └── sim_xxx/ -│ ├── state.json # 模拟状态 -│ ├── simulation_config.json # 模拟配置 -│ ├── reddit_profiles.json # Reddit人设 -│ ├── twitter_profiles.csv # Twitter人设 -│ ├── run_state.json # 运行状态 -│ ├── simulation.log # 主日志 -│ ├── twitter/ # Twitter数据 -│ │ ├── actions.jsonl -│ │ └── twitter_simulation.db -│ └── reddit/ # Reddit数据 -│ ├── actions.jsonl -│ └── reddit_simulation.db -├── scripts/ # 模拟运行脚本 -│ ├── run_twitter_simulation.py -│ ├── run_reddit_simulation.py -│ ├── run_parallel_simulation.py -│ └── action_logger.py -└── app/ - ├── __init__.py # Flask应用工厂 - ├── config.py # 配置管理 - ├── api/ # API路由 - │ ├── __init__.py - │ ├── graph.py # 图谱相关接口 - │ ├── simulation.py # 模拟相关接口 - │ └── report.py # 报告相关接口 - ├── models/ # 数据模型 - │ ├── __init__.py - │ ├── project.py # 项目模型 - │ └── task.py # 任务模型 - ├── services/ # 业务服务 - │ ├── __init__.py - │ ├── ontology_generator.py # 本体生成 - │ ├── graph_builder.py # 图谱构建 - │ ├── text_processor.py # 文本处理 - │ ├── zep_entity_reader.py # 实体读取 - │ ├── zep_tools.py # Zep检索工具服务 - │ ├── oasis_profile_generator.py # 人设生成 - │ ├── simulation_config_generator.py # 配置生成 - │ ├── simulation_manager.py # 模拟管理 - │ ├── simulation_runner.py # 模拟运行 - │ ├── simulation_ipc.py # 模拟IPC通信(Interview功能) - │ ├── zep_graph_memory_updater.py # 图谱记忆动态更新 - │ └── report_agent.py # 报告生成Agent(ReACT模式) - └── utils/ # 工具类 - ├── __init__.py - ├── file_parser.py # 文件解析 - ├── llm_client.py # LLM客户端 - ├── logger.py # 日志配置 - └── retry.py # 重试机制 -``` - ---- - -## 核心功能模块 - -### 1. 图谱构建模块 - -**功能**: 从文档构建知识图谱 - -**流程**: -1. 上传文档(PDF/TXT/MD) -2. 提取文本内容 -3. LLM分析生成本体(实体类型+关系类型) -4. 文本分块(chunk_size=500, overlap=50) -5. 调用 Zep API 构建图谱 -6. 等待 Zep 处理完成 -7. 返回图谱ID和统计信息 - -**核心服务**: -- `OntologyGenerator`: 本体生成 -- `GraphBuilderService`: 图谱构建 -- `TextProcessor`: 文本处理 - -### 2. 模拟准备模块 - -**功能**: 准备舆论模拟所需的所有数据 - -**流程**: -1. 创建模拟(指定project_id和graph_id) -2. 从 Zep 图谱读取并过滤实体 -3. 为每个实体生成 OASIS Agent Profile(支持并行) -4. 使用 LLM 智能生成模拟配置(时间/活跃度/事件) -5. 保存配置文件和人设文件 - -**核心服务**: -- `ZepEntityReader`: 实体读取与过滤 -- `OasisProfileGenerator`: Agent人设生成 -- `SimulationConfigGenerator`: 模拟配置生成 -- `SimulationManager`: 模拟管理 - -### 3. 模拟运行模块 - -**功能**: 运行 Twitter/Reddit 双平台舆论模拟 - -**流程**: -1. 检查模拟准备状态 -2. 启动 OASIS 模拟进程(subprocess) -3. 监控进程运行状态 -4. 解析动作日志(actions.jsonl) -5. (可选)将Agent活动实时更新到Zep图谱 -6. 实时更新运行状态 -7. 模拟完成后进入等待命令模式 -8. 支持停止/暂停/恢复 - -**核心服务**: -- `SimulationRunner`: 模拟运行器 -- `ZepGraphMemoryUpdater`: 图谱记忆动态更新器 - -### 4. Agent采访(Interview)模块 - -**功能**: 在模拟完成后对Agent进行采访 - -**特点**: -- **模拟状态持久化**: 模拟完成后环境不立即关闭,进入等待命令模式 -- **IPC通信机制**: 通过文件系统在Flask后端和模拟脚本之间通信 -- **单个采访**: 对指定Agent提问并获取回答 -- **批量采访**: 同时对多个Agent提不同问题 -- **全局采访**: 使用相同问题采访所有Agent -- **采访历史**: 从数据库读取所有Interview记录 - -**核心服务**: -- `SimulationIPCClient`: IPC客户端(Flask端使用) -- `SimulationIPCServer`: IPC服务器(模拟脚本端使用) - -### 5. Report Agent模块(报告生成) - -**功能**: 模拟完成后自动生成分析报告,支持与用户对话 - -**特点**: -- **ReACT模式**: Reasoning + Acting,多轮思考与工具调用 -- **大纲规划**: LLM分析模拟需求,自动规划报告目录结构 -- **分段生成**: 逐章节生成,每章节可多次调用Zep检索工具 -- **Markdown输出**: 生成专业的Markdown格式报告 -- **对话功能**: 报告完成后可与Report Agent对话,自主调用工具回答问题 - -**工具(MCP封装)**: -- `search_graph`: 图谱语义搜索 -- `get_graph_statistics`: 获取图谱统计信息 -- `get_entity_summary`: 获取实体关系摘要 -- `get_simulation_context`: 获取模拟上下文 -- `get_entities_by_type`: 按类型获取实体 - -**核心服务**: -- `ZepToolsService`: Zep检索工具封装 -- `ReportAgent`: 报告生成Agent(ReACT模式) -- `ReportManager`: 报告持久化管理 - -**工作原理**: -``` -┌─────────────────────────────────────────────────────────────┐ -│ Report Agent (ReACT) │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ 1. 规划阶段 │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ LLM分析模拟需求 → 获取图谱上下文 → 生成报告大纲 │ │ -│ └─────────────────────────────────────────────────┘ │ -│ │ -│ 2. 生成阶段 (每章节) │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ Thought → Action → Observation → ... → Final │ │ -│ │ ↓ ↓ ↓ │ │ -│ │ 分析需求 调用工具 分析结果 生成内容 │ │ -│ └─────────────────────────────────────────────────┘ │ -│ │ -│ 3. 输出阶段 │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ 组装章节 → 生成Markdown → 保存JSON/MD文件 │ │ -│ └─────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -**工作原理**: -``` -Flask后端 模拟脚本 - │ │ - │ 写入命令文件 │ - │ ─────────────────────────→│ - │ │ 轮询命令目录 - │ │ 执行Interview - │ │ 写入响应文件 - │←───────────────────────── │ - │ 读取响应文件 │ - │ │ -``` - ---- - -## API接口文档 - -### 图谱管理接口 - -#### 1. 生成本体 - -**接口**: `POST /api/graph/ontology/generate` - -**请求类型**: `multipart/form-data` - -**请求参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| files | File[] | 是 | 上传的文档(PDF/MD/TXT) | -| simulation_requirement | String | 是 | 模拟需求描述 | -| project_name | String | 否 | 项目名称 | -| additional_context | String | 否 | 额外说明 | - -**返回示例**: -```json -{ - "success": true, - "data": { - "project_id": "proj_33469c670f56", - "project_name": "学术不端事件模拟", - "ontology": { - "entity_types": [ - { - "name": "Student", - "description": "Students involved in the event", - "attributes": [ - {"name": "full_name", "type": "text", "description": "Student full name"}, - {"name": "major", "type": "text", "description": "Major field"} - ], - "examples": ["张三", "李四"] - }, - { - "name": "Professor", - "description": "Faculty members", - "attributes": [...] - }, - ... - { - "name": "Person", - "description": "Any individual person not fitting other specific person types", - "attributes": [...] - }, - { - "name": "Organization", - "description": "Any organization not fitting other specific types", - "attributes": [...] - } - ], - "edge_types": [ - { - "name": "STUDIES_AT", - "description": "Student studies at university", - "source_targets": [ - {"source": "Student", "target": "University"} - ], - "attributes": [] - }, - ... - ] - }, - "analysis_summary": "文档涉及学术不端事件...", - "files": [ - {"filename": "document.pdf", "size": 102400} - ], - "total_text_length": 12345 - } -} -``` - -**说明**: -- 本体设计必须包含10个实体类型,最后2个为兜底类型(`Person`和`Organization`) -- 实体类型必须是现实中可以发声的主体 -- 属性名不能使用保留字(`name`, `uuid`, `group_id`, `created_at`, `summary`) - ---- - -#### 2. 构建图谱 - -**接口**: `POST /api/graph/build` - -**请求类型**: `application/json` - -**请求参数**: -```json -{ - "project_id": "proj_33469c670f56", - "graph_name": "学术不端事件图谱", - "chunk_size": 500, - "chunk_overlap": 50, - "force": false -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| project_id | String | 是 | - | 项目ID(来自接口1) | -| graph_name | String | 否 | 项目名称 | 图谱名称 | -| chunk_size | Integer | 否 | 500 | 文本块大小 | -| chunk_overlap | Integer | 否 | 50 | 块重叠大小 | -| force | Boolean | 否 | false | 强制重新构建 | - -**返回示例**: -```json -{ - "success": true, - "data": { - "project_id": "proj_33469c670f56", - "task_id": "a1b2c3d4-e5f6-...", - "message": "图谱构建任务已启动,请通过 /task/{task_id} 查询进度" - } -} -``` - -**异步任务**: 此接口立即返回task_id,实际构建在后台进行 - ---- - -#### 3. 查询任务状态 - -**接口**: `GET /api/graph/task/{task_id}` - -**返回示例**: -```json -{ - "success": true, - "data": { - "task_id": "a1b2c3d4-e5f6-...", - "task_type": "graph_build", - "status": "processing", - "created_at": "2025-12-02T10:00:00", - "updated_at": "2025-12-02T10:05:00", - "progress": 45, - "message": "Zep处理中... 10/30 完成", - "result": null, - "error": null, - "metadata": { - "project_id": "proj_33469c670f56" - } - } -} -``` - -**状态值**: -- `pending`: 等待中 -- `processing`: 处理中 -- `completed`: 已完成 -- `failed`: 失败 - ---- - -#### 4. 获取图谱数据 - -**接口**: `GET /api/graph/data/{graph_id}` - -**返回示例**: -```json -{ - "success": true, - "data": { - "graph_id": "mirofish_abc123", - "nodes": [ - { - "uuid": "node-uuid-1", - "name": "张三", - "labels": ["Entity", "Student"], - "summary": "某大学计算机专业学生", - "attributes": { - "full_name": "张三", - "major": "计算机科学" - } - }, - ... - ], - "edges": [ - { - "uuid": "edge-uuid-1", - "name": "STUDIES_AT", - "fact": "张三就读于某大学", - "source_node_uuid": "node-uuid-1", - "target_node_uuid": "node-uuid-2", - "attributes": {} - }, - ... - ], - "node_count": 50, - "edge_count": 120 - } -} -``` - ---- - -#### 5. 项目管理接口 - -**获取项目**: `GET /api/graph/project/{project_id}` - -**列出项目**: `GET /api/graph/project/list?limit=50` - -**删除项目**: `DELETE /api/graph/project/{project_id}` - -**重置项目**: `POST /api/graph/project/{project_id}/reset` - ---- - -### 模拟管理接口 - -#### 1. 创建模拟 - -**接口**: `POST /api/simulation/create` - -**请求参数**: -```json -{ - "project_id": "proj_33469c670f56", - "graph_id": "mirofish_abc123", - "enable_twitter": true, - "enable_reddit": true -} -``` - -**返回示例**: -```json -{ - "success": true, - "data": { - "simulation_id": "sim_10b494550540", - "project_id": "proj_33469c670f56", - "graph_id": "mirofish_abc123", - "status": "created", - "enable_twitter": true, - "enable_reddit": true, - "created_at": "2025-12-02T10:00:00" - } -} -``` - ---- - -#### 2. 准备模拟 - -**接口**: `POST /api/simulation/prepare` - -**请求参数**: -```json -{ - "simulation_id": "sim_10b494550540", - "entity_types": ["Student", "Professor"], - "use_llm_for_profiles": true, - "parallel_profile_count": 5, - "force_regenerate": false -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| simulation_id | String | 是 | - | 模拟ID | -| entity_types | String[] | 否 | null | 指定实体类型(为空则全部) | -| use_llm_for_profiles | Boolean | 否 | true | 是否用LLM生成详细人设 | -| parallel_profile_count | Integer | 否 | 5 | 并行生成人设数量 | -| force_regenerate | Boolean | 否 | false | 强制重新生成 | - -**返回示例**: -```json -{ - "success": true, - "data": { - "simulation_id": "sim_10b494550540", - "task_id": "task_xyz789", - "status": "preparing", - "message": "准备任务已启动", - "already_prepared": false - } -} -``` - -**特性**: -- 自动检测已完成的准备工作,避免重复生成 -- 支持并行生成人设(默认5个并发) -- 支持强制重新生成 - ---- - -#### 3. 查询准备进度 - -**接口**: `POST /api/simulation/prepare/status` - -**请求参数**: -```json -{ - "task_id": "task_xyz789", - "simulation_id": "sim_10b494550540" -} -``` - -**返回示例**: -```json -{ - "success": true, - "data": { - "task_id": "task_xyz789", - "status": "processing", - "progress": 45, - "message": "[2/4] 生成Agent配置: 5/15 - 已完成 Student: 张三", - "progress_detail": { - "current_stage": "generating_profiles", - "current_stage_name": "生成Agent人设", - "stage_index": 2, - "total_stages": 4, - "stage_progress": 33, - "current_item": 5, - "total_items": 15, - "item_description": "已完成 Student: 张三" - }, - "already_prepared": false - } -} -``` - -**进度阶段**: -1. `reading`: 读取图谱实体 (0-20%) -2. `generating_profiles`: 生成Agent人设 (20-70%) -3. `generating_config`: 生成模拟配置 (70-90%) -4. `copying_scripts`: 准备模拟脚本 (90-100%) - ---- - -#### 4. 启动模拟 - -**接口**: `POST /api/simulation/start` - -**请求参数**: -```json -{ - "simulation_id": "sim_10b494550540", - "platform": "parallel", - "max_rounds": 100, - "enable_graph_memory_update": false -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| simulation_id | String | 是 | - | 模拟ID | -| platform | String | 否 | parallel | 运行平台: twitter/reddit/parallel | -| max_rounds | Integer | 否 | - | 最大模拟轮数,用于截断过长的模拟 | -| enable_graph_memory_update | Boolean | 否 | false | 是否将Agent活动动态更新到Zep图谱 | - -**返回示例**: -```json -{ - "success": true, - "data": { - "simulation_id": "sim_10b494550540", - "runner_status": "running", - "process_pid": 12345, - "twitter_running": true, - "reddit_running": true, - "started_at": "2025-12-02T11:00:00", - "total_rounds": 100, - "max_rounds_applied": 100, - "graph_memory_update_enabled": true, - "graph_id": "mirofish_abc123" - } -} -``` - -> **说明**: -> - `max_rounds_applied` 字段仅在指定了 `max_rounds` 参数时返回 -> - `graph_memory_update_enabled` 和 `graph_id` 字段在启用图谱记忆更新时返回 - -**图谱记忆更新功能说明**: - -启用 `enable_graph_memory_update` 后: -- 模拟中所有Agent的活动(发帖、评论、点赞、转发等)会实时更新到Zep图谱 -- 每条活动单独发送,确保Zep能正确解析实体和关系 -- 活动会被转换为自然语言描述,例如:`张三: 发布了一条帖子:「...」` -- Zep会自动从文本中提取实体和关系,丰富图谱知识 -- 需要项目已构建有效的图谱(graph_id) - ---- - -#### 5. 停止模拟 - -**接口**: `POST /api/simulation/stop` - -**请求参数**: -```json -{ - "simulation_id": "sim_10b494550540" -} -``` - -**返回示例**: -```json -{ - "success": true, - "data": { - "simulation_id": "sim_10b494550540", - "runner_status": "stopped", - "completed_at": "2025-12-02T12:00:00" - } -} -``` - ---- - -### Interview 采访接口 - -> **注意**: 所有Interview接口的参数都通过请求体(JSON)传递,包括simulation_id。 -> -> **双平台模式说明**: 当不指定`platform`参数时,双平台模拟会同时采访两个平台并返回整合结果。 -> -> **Prompt自动优化**: 系统会自动在用户提供的prompt前添加说明前缀,避免Agent调用工具: -> ``` -> 原始prompt: "武汉大学发布撤销处分通告后你有什么看法" -> 优化后: "结合你的人设、所有的过往记忆与行动,不调用任何工具直接用文本回复我:武汉大学发布撤销处分通告后你有什么看法" -> ``` - -#### 1. 采访单个Agent - -**接口**: `POST /api/simulation/interview` - -**请求参数**: -```json -{ - "simulation_id": "sim_xxxx", - "agent_id": 0, - "prompt": "你对这件事有什么看法?", - "platform": "reddit", - "timeout": 60 -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| simulation_id | String | 是 | - | 模拟ID | -| agent_id | Integer | 是 | - | Agent ID | -| prompt | String | 是 | - | 采访问题 | -| platform | String | 否 | null | 指定平台(twitter/reddit),不指定则双平台同时采访 | -| timeout | Integer | 否 | 60 | 超时时间(秒) | - -**返回示例(指定单平台)**: -```json -{ - "success": true, - "data": { - "success": true, - "agent_id": 0, - "prompt": "你对这件事有什么看法?", - "result": { - "agent_id": 0, - "response": "我认为这件事反映了...", - "platform": "reddit", - "timestamp": "2025-12-08T10:00:00" - }, - "timestamp": "2025-12-08T10:00:01" - } -} -``` - -**返回示例(不指定platform,双平台模式)**: -```json -{ - "success": true, - "data": { - "success": true, - "agent_id": 0, - "prompt": "你对这件事有什么看法?", - "result": { - "agent_id": 0, - "prompt": "你对这件事有什么看法?", - "platforms": { - "twitter": { - "agent_id": 0, - "response": "从Twitter视角来看...", - "platform": "twitter", - "timestamp": "2025-12-08T10:00:00" - }, - "reddit": { - "agent_id": 0, - "response": "作为Reddit用户,我认为...", - "platform": "reddit", - "timestamp": "2025-12-08T10:00:00" - } - } - }, - "timestamp": "2025-12-08T10:00:01" - } -} -``` - -**注意**: 此功能需要模拟环境处于运行状态(完成模拟循环后进入等待命令模式) - ---- - -#### 2. 批量采访多个Agent - -**接口**: `POST /api/simulation/interview/batch` - -**请求参数**: -```json -{ - "simulation_id": "sim_xxxx", - "interviews": [ - {"agent_id": 0, "prompt": "你对A有什么看法?", "platform": "twitter"}, - {"agent_id": 1, "prompt": "你对B有什么看法?", "platform": "reddit"}, - {"agent_id": 2, "prompt": "你对C有什么看法?"} - ], - "platform": "reddit", - "timeout": 120 -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| simulation_id | String | 是 | - | 模拟ID | -| interviews | Array | 是 | - | 采访列表,每项包含agent_id、prompt和可选的platform | -| platform | String | 否 | null | 默认平台(被每项的platform覆盖),不指定则双平台同时采访 | -| timeout | Integer | 否 | 120 | 超时时间(秒) | - -**返回示例**: -```json -{ - "success": true, - "data": { - "success": true, - "interviews_count": 3, - "result": { - "interviews_count": 6, - "results": { - "twitter_0": {"agent_id": 0, "response": "...", "platform": "twitter"}, - "reddit_1": {"agent_id": 1, "response": "...", "platform": "reddit"}, - "twitter_2": {"agent_id": 2, "response": "...", "platform": "twitter"}, - "reddit_2": {"agent_id": 2, "response": "...", "platform": "reddit"} - } - }, - "timestamp": "2025-12-08T10:00:01" - } -} -``` - ---- - -#### 3. 全局采访(采访所有Agent) - -**接口**: `POST /api/simulation/interview/all` - -**请求参数**: -```json -{ - "simulation_id": "sim_xxxx", - "prompt": "你对这件事整体有什么看法?", - "platform": "reddit", - "timeout": 180 -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| simulation_id | String | 是 | - | 模拟ID | -| prompt | String | 是 | - | 采访问题(所有Agent使用相同问题) | -| platform | String | 否 | null | 指定平台(twitter/reddit),不指定则双平台同时采访 | -| timeout | Integer | 否 | 180 | 超时时间(秒) | - -**返回示例**: -```json -{ - "success": true, - "data": { - "success": true, - "interviews_count": 50, - "result": { - "interviews_count": 100, - "results": { - "twitter_0": {"agent_id": 0, "response": "...", "platform": "twitter"}, - "reddit_0": {"agent_id": 0, "response": "...", "platform": "reddit"}, - "twitter_1": {"agent_id": 1, "response": "...", "platform": "twitter"}, - "reddit_1": {"agent_id": 1, "response": "...", "platform": "reddit"}, - ... - } - }, - "timestamp": "2025-12-08T10:00:01" - } -} -``` - ---- - -#### 4. 获取Interview历史 - -**接口**: `POST /api/simulation/interview/history` - -**请求参数**: -```json -{ - "simulation_id": "sim_xxxx", - "platform": "reddit", - "agent_id": 0, - "limit": 100 -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| simulation_id | String | 是 | - | 模拟ID | -| platform | String | 否 | null | 平台类型(reddit/twitter),不指定则返回两个平台的所有历史 | -| agent_id | Integer | 否 | - | 只获取该Agent的采访历史 | -| limit | Integer | 否 | 100 | 返回数量限制 | - -**返回示例(不指定platform,返回双平台历史)**: -```json -{ - "success": true, - "data": { - "count": 10, - "history": [ - { - "agent_id": 0, - "response": "我认为...", - "prompt": "你对这件事有什么看法?", - "timestamp": "2025-12-08T10:00:02", - "platform": "twitter" - }, - { - "agent_id": 0, - "response": "从Reddit角度来看...", - "prompt": "你对这件事有什么看法?", - "timestamp": "2025-12-08T10:00:01", - "platform": "reddit" - }, - ... - ] - } -} -``` - ---- - -#### 5. 获取模拟环境状态 - -**接口**: `POST /api/simulation/env-status` - -**请求参数**: -```json -{ - "simulation_id": "sim_xxxx" -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| simulation_id | String | 是 | - | 模拟ID | - -**返回示例**: -```json -{ - "success": true, - "data": { - "simulation_id": "sim_xxxx", - "env_alive": true, - "twitter_available": true, - "reddit_available": true, - "message": "环境正在运行,可以接收Interview命令" - } -} -``` - ---- - -#### 6. 关闭模拟环境 - -**接口**: `POST /api/simulation/close-env` - -**请求参数**: -```json -{ - "simulation_id": "sim_10b494550540", - "timeout": 30 -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| simulation_id | String | 是 | - | 模拟ID | -| timeout | Integer | 否 | 30 | 超时时间(秒) | - -**返回示例**: -```json -{ - "success": true, - "data": { - "success": true, - "message": "环境关闭命令已发送", - "result": {"message": "环境即将关闭"}, - "timestamp": "2025-12-08T10:00:01" - } -} -``` - -**注意**: 此接口与 `/stop` 不同: -- `/stop`: 强制终止模拟进程 -- `/close-env`: 优雅地关闭环境,让模拟进程正常退出 - ---- - -### Report 报告接口 - -> **说明**: 报告生成完成后才能解锁Interview功能。Report Agent使用ReACT模式,可以在对话中自主调用Zep检索工具。 - -#### 1. 生成报告 - -**接口**: `POST /api/report/generate` - -**请求参数**: -```json -{ - "simulation_id": "sim_xxxx", - "force_regenerate": false -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| simulation_id | String | 是 | - | 模拟ID | -| force_regenerate | Boolean | 否 | false | 强制重新生成 | - -**返回示例**: -```json -{ - "success": true, - "data": { - "simulation_id": "sim_xxxx", - "task_id": "task_xxxx", - "status": "generating", - "message": "报告生成任务已启动", - "already_generated": false - } -} -``` - -**如果报告已存在**: -```json -{ - "success": true, - "data": { - "simulation_id": "sim_xxxx", - "report_id": "report_xxxx", - "status": "completed", - "message": "报告已存在", - "already_generated": true - } -} -``` - ---- - -#### 2. 查询生成进度 - -**接口**: `POST /api/report/generate/status` - -**请求参数**: -```json -{ - "task_id": "task_xxxx", - "simulation_id": "sim_xxxx" -} -``` - -**返回示例**: -```json -{ - "success": true, - "data": { - "task_id": "task_xxxx", - "status": "processing", - "progress": 45, - "message": "[generating] 正在生成章节: 关键发现 (3/5)" - } -} -``` - ---- - -#### 3. 获取报告 - -**接口**: `GET /api/report/{report_id}` - -**返回示例**: -```json -{ - "success": true, - "data": { - "report_id": "report_xxxx", - "simulation_id": "sim_xxxx", - "graph_id": "mirofish_xxxx", - "simulation_requirement": "模拟武汉大学撤销处分后的舆情走向", - "status": "completed", - "outline": { - "title": "武汉大学撤销处分事件舆情分析报告", - "summary": "基于模拟结果的全面舆情分析", - "sections": [ - {"title": "执行摘要", "content": "..."}, - {"title": "模拟背景", "content": "..."}, - {"title": "关键发现", "content": "..."}, - {"title": "舆情分析", "content": "..."}, - {"title": "建议与展望", "content": "..."} - ] - }, - "markdown_content": "# 武汉大学撤销处分事件舆情分析报告\n\n...", - "created_at": "2025-12-09T10:00:00", - "completed_at": "2025-12-09T10:05:00" - } -} -``` - ---- - -#### 4. 根据模拟ID获取报告 - -**接口**: `GET /api/report/by-simulation/{simulation_id}` - -**返回示例**: -```json -{ - "success": true, - "data": {...}, - "has_report": true -} -``` - ---- - -#### 5. 下载报告 - -**接口**: `GET /api/report/{report_id}/download` - -**返回**: Markdown文件下载 - ---- - -#### 6. 与Report Agent对话 - -**接口**: `POST /api/report/chat` - -**请求参数**: -```json -{ - "simulation_id": "sim_xxxx", - "message": "请详细解释一下舆情的主要趋势", - "chat_history": [ - {"role": "user", "content": "报告提到了哪些关键人物?"}, - {"role": "assistant", "content": "根据分析,关键人物包括..."} - ] -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| simulation_id | String | 是 | - | 模拟ID | -| message | String | 是 | - | 用户消息 | -| chat_history | Array | 否 | [] | 对话历史(用于上下文) | - -**返回示例**: -```json -{ - "success": true, - "data": { - "response": "根据模拟数据分析,舆情的主要趋势表现为...\n\n1. **初期阶段**:...\n2. **发酵阶段**:...\n3. **高峰阶段**:...", - "tool_calls": [ - {"name": "search_graph", "parameters": {"query": "舆情趋势"}}, - {"name": "get_graph_statistics", "parameters": {}} - ], - "sources": [] - } -} -``` - ---- - -#### 7. 检查报告状态 - -**接口**: `GET /api/report/check/{simulation_id}` - -**用途**: 判断是否解锁Interview功能 - -**返回示例**: -```json -{ - "success": true, - "data": { - "simulation_id": "sim_xxxx", - "has_report": true, - "report_status": "completed", - "report_id": "report_xxxx", - "interview_unlocked": true - } -} -``` - ---- - -#### 8. 列出所有报告 - -**接口**: `GET /api/report/list?simulation_id=sim_xxxx&limit=50` - -**返回示例**: -```json -{ - "success": true, - "data": [...], - "count": 5 -} -``` - ---- - -#### 9. 删除报告 - -**接口**: `DELETE /api/report/{report_id}` - ---- - -#### 10. 工具调试接口 - -**图谱搜索**: `POST /api/report/tools/search` -```json -{ - "graph_id": "mirofish_xxxx", - "query": "舆情走向", - "limit": 10 -} -``` - -**图谱统计**: `POST /api/report/tools/statistics` -```json -{ - "graph_id": "mirofish_xxxx" -} -``` - ---- - -#### 6. 获取运行状态 - -**接口**: `GET /api/simulation/{simulation_id}/run-status` - -**返回示例**: -```json -{ - "success": true, - "data": { - "simulation_id": "sim_10b494550540", - "runner_status": "running", - "current_round": 5, - "total_rounds": 144, - "progress_percent": 3.5, - "simulated_hours": 2, - "total_simulation_hours": 72, - "twitter_running": true, - "reddit_running": true, - "twitter_actions_count": 150, - "reddit_actions_count": 200, - "total_actions_count": 350, - "started_at": "2025-12-02T11:00:00", - "updated_at": "2025-12-02T11:30:00" - } -} -``` - ---- - -#### 7. 获取详细状态(含最近动作) - -**接口**: `GET /api/simulation/{simulation_id}/run-status/detail` - -**返回示例**: -```json -{ - "success": true, - "data": { - ... (基本状态同上) ..., - "recent_actions": [ - { - "round_num": 5, - "timestamp": "2025-12-02T11:30:15", - "platform": "twitter", - "agent_id": 3, - "agent_name": "张三_123", - "action_type": "CREATE_POST", - "action_args": { - "content": "对学术不端事件的看法..." - }, - "result": "post_id_123", - "success": true - }, - ... - ] - } -} -``` - ---- - -#### 8. 其他接口 - -**获取实体列表**: `GET /api/simulation/entities/{graph_id}` - -**获取模拟配置**: `GET /api/simulation/{simulation_id}/config` - -**获取Agent人设**: `GET /api/simulation/{simulation_id}/profiles?platform=reddit` - -**获取动作历史**: `GET /api/simulation/{simulation_id}/actions?limit=100&platform=twitter` - -**获取时间线**: `GET /api/simulation/{simulation_id}/timeline?start_round=0&end_round=10` - -**获取Agent统计**: `GET /api/simulation/{simulation_id}/agent-stats` - -**获取帖子**: `GET /api/simulation/{simulation_id}/posts?platform=reddit&limit=50` - -**获取评论**: `GET /api/simulation/{simulation_id}/comments?post_id=123` - ---- - -## 数据模型 - -### 1. Project (项目模型) - -**文件**: `app/models/project.py` - -**字段**: -```python -project_id: str # 项目ID (proj_xxx) -name: str # 项目名称 -status: ProjectStatus # 状态 -created_at: str # 创建时间 -updated_at: str # 更新时间 - -# 文件信息 -files: List[Dict] # 上传的文件列表 -total_text_length: int # 文本总长度 - -# 本体信息 -ontology: Dict # 实体类型和关系类型 -analysis_summary: str # 分析摘要 - -# 图谱信息 -graph_id: str # Zep图谱ID -graph_build_task_id: str # 构建任务ID - -# 配置 -simulation_requirement: str # 模拟需求 -chunk_size: int # 文本块大小 -chunk_overlap: int # 块重叠大小 - -# 错误信息 -error: str # 错误描述 -``` - -**状态枚举**: -```python -CREATED = "created" # 已创建 -ONTOLOGY_GENERATED = "ontology_generated" # 本体已生成 -GRAPH_BUILDING = "graph_building" # 图谱构建中 -GRAPH_COMPLETED = "graph_completed" # 图谱已完成 -FAILED = "failed" # 失败 -``` - ---- - -### 2. Task (任务模型) - -**文件**: `app/models/task.py` - -**字段**: -```python -task_id: str # 任务ID (UUID) -task_type: str # 任务类型 -status: TaskStatus # 状态 -created_at: datetime # 创建时间 -updated_at: datetime # 更新时间 -progress: int # 进度 (0-100) -message: str # 状态消息 -result: Dict # 任务结果 -error: str # 错误信息 -metadata: Dict # 元数据 -progress_detail: Dict # 详细进度 -``` - -**状态枚举**: -```python -PENDING = "pending" # 等待中 -PROCESSING = "processing" # 处理中 -COMPLETED = "completed" # 已完成 -FAILED = "failed" # 失败 -``` - ---- - -### 3. SimulationState (模拟状态) - -**文件**: `app/services/simulation_manager.py` - -**字段**: -```python -simulation_id: str # 模拟ID (sim_xxx) -project_id: str # 项目ID -graph_id: str # 图谱ID -enable_twitter: bool # 启用Twitter -enable_reddit: bool # 启用Reddit -status: SimulationStatus # 状态 -entities_count: int # 实体数量 -profiles_count: int # 人设数量 -entity_types: List[str] # 实体类型列表 -config_generated: bool # 配置已生成 -config_reasoning: str # 配置推理说明 -current_round: int # 当前轮次 -twitter_status: str # Twitter状态 -reddit_status: str # Reddit状态 -created_at: str # 创建时间 -updated_at: str # 更新时间 -error: str # 错误信息 -``` - ---- - -### 4. EntityNode (实体节点) - -**文件**: `app/services/zep_entity_reader.py` - -**字段**: -```python -uuid: str # 实体UUID -name: str # 实体名称 -labels: List[str] # 标签列表 -summary: str # 摘要 -attributes: Dict # 属性字典 -related_edges: List[Dict] # 相关边信息 -related_nodes: List[Dict] # 关联节点信息 -``` - ---- - -### 5. OasisAgentProfile (Agent人设) - -**文件**: `app/services/oasis_profile_generator.py` - -**字段**: -```python -user_id: int # 用户ID -user_name: str # 用户名 -name: str # 真实姓名 -bio: str # 简介 (200字) -persona: str # 详细人设 (2000字) -karma: int # Reddit积分 -friend_count: int # Twitter好友数 -follower_count: int # 粉丝数 -statuses_count: int # 发帖数 -age: int # 年龄 -gender: str # 性别 (male/female/other) -mbti: str # MBTI类型 -country: str # 国家 -profession: str # 职业 -interested_topics: List[str] # 兴趣话题 -source_entity_uuid: str # 来源实体UUID -source_entity_type: str # 来源实体类型 -created_at: str # 创建时间 -``` - ---- - -### 6. Report (报告模型) - -**文件**: `app/services/report_agent.py` - -**字段**: -```python -report_id: str # 报告ID (report_xxx) -simulation_id: str # 模拟ID -graph_id: str # 图谱ID -simulation_requirement: str # 模拟需求 -status: ReportStatus # 状态 -outline: ReportOutline # 报告大纲 -markdown_content: str # Markdown内容 -created_at: str # 创建时间 -completed_at: str # 完成时间 -error: str # 错误信息 -``` - -**状态枚举**: -```python -PENDING = "pending" # 等待中 -PLANNING = "planning" # 规划大纲中 -GENERATING = "generating" # 生成内容中 -COMPLETED = "completed" # 已完成 -FAILED = "failed" # 失败 -``` - -**ReportOutline字段**: -```python -title: str # 报告标题 -summary: str # 报告摘要 -sections: List[ReportSection] # 章节列表 -``` - -**ReportSection字段**: -```python -title: str # 章节标题 -content: str # 章节内容 -subsections: List[ReportSection] # 子章节 -``` - ---- - -### 7. SimulationParameters (模拟参数) - -**文件**: `app/services/simulation_config_generator.py` - -**字段**: -```python -simulation_id: str # 模拟ID -project_id: str # 项目ID -graph_id: str # 图谱ID -simulation_requirement: str # 模拟需求 - -# 时间配置 -time_config: TimeSimulationConfig - ├── total_simulation_hours: int # 总时长(小时) - ├── minutes_per_round: int # 每轮分钟数 - ├── agents_per_hour_min: int # 每小时最少激活Agent数 - ├── agents_per_hour_max: int # 每小时最多激活Agent数 - ├── peak_hours: List[int] # 高峰时段 [19,20,21,22] - ├── off_peak_hours: List[int] # 低谷时段 [0,1,2,3,4,5] - ├── morning_hours: List[int] # 早间时段 [6,7,8] - ├── work_hours: List[int] # 工作时段 [9-18] - ├── peak_activity_multiplier: float # 高峰活跃度系数 1.5 - ├── off_peak_activity_multiplier: float # 低谷活跃度系数 0.05 - ├── morning_activity_multiplier: float # 早间活跃度系数 0.4 - └── work_activity_multiplier: float # 工作时段活跃度系数 0.7 - -# Agent配置列表 -agent_configs: List[AgentActivityConfig] - ├── agent_id: int # Agent ID - ├── entity_uuid: str # 实体UUID - ├── entity_name: str # 实体名称 - ├── entity_type: str # 实体类型 - ├── activity_level: float # 活跃度 (0.0-1.0) - ├── posts_per_hour: float # 每小时发帖数 - ├── comments_per_hour: float # 每小时评论数 - ├── active_hours: List[int] # 活跃时间段 - ├── response_delay_min: int # 最小响应延迟(分钟) - ├── response_delay_max: int # 最大响应延迟(分钟) - ├── sentiment_bias: float # 情感倾向 (-1.0到1.0) - ├── stance: str # 立场 (supportive/opposing/neutral/observer) - └── influence_weight: float # 影响力权重 - -# 事件配置 -event_config: EventConfig - ├── initial_posts: List[Dict] # 初始帖子 - ├── scheduled_events: List[Dict] # 定时事件 - ├── hot_topics: List[str] # 热点话题 - └── narrative_direction: str # 舆论方向 - -# 平台配置 -twitter_config: PlatformConfig -reddit_config: PlatformConfig - ├── platform: str # 平台名称 - ├── recency_weight: float # 时间新鲜度权重 - ├── popularity_weight: float # 热度权重 - ├── relevance_weight: float # 相关性权重 - ├── viral_threshold: int # 病毒传播阈值 - └── echo_chamber_strength: float # 回声室效应强度 - -# LLM配置 -llm_model: str # LLM模型名称 -llm_base_url: str # LLM API地址 -generated_at: str # 生成时间 -generation_reasoning: str # LLM推理说明 -``` - ---- - -## 服务层详解 - -### 1. OntologyGenerator (本体生成器) - -**文件**: `app/services/ontology_generator.py` - -**功能**: 使用LLM分析文档内容,生成适合舆论模拟的实体类型和关系类型 - -**核心方法**: -```python -def generate( - document_texts: List[str], - simulation_requirement: str, - additional_context: Optional[str] = None -) -> Dict[str, Any]: - """ - 生成本体定义 - - Returns: - { - "entity_types": [...], # 10个实体类型(最后2个为Person和Organization) - "edge_types": [...], # 6-10个关系类型 - "analysis_summary": "..." # 分析摘要 - } - """ -``` - -**设计原则**: -- 必须返回**10个实体类型**,最后2个为兜底类型 -- 实体必须是现实中可以发声的主体(人/组织) -- 属性名不能使用Zep保留字 -- 关系类型要反映社交媒体互动 - -**LLM提示词要点**: -- 系统角色: 知识图谱本体设计专家 -- 任务背景: 社交媒体舆论模拟 -- 输出格式: 严格的JSON结构 -- 实体类型层次: 具体类型(8个) + 兜底类型(2个) - ---- - -### 2. GraphBuilderService (图谱构建服务) - -**文件**: `app/services/graph_builder.py` - -**功能**: 调用Zep API构建知识图谱 - -**核心方法**: -```python -def create_graph(name: str) -> str: - """创建Zep图谱""" - -def set_ontology(graph_id: str, ontology: Dict): - """设置图谱本体(动态创建Pydantic类)""" - -def add_text_batches( - graph_id: str, - chunks: List[str], - batch_size: int = 3, - progress_callback: Optional[Callable] = None -) -> List[str]: - """分批添加文本,返回episode UUIDs""" - -def _wait_for_episodes( - episode_uuids: List[str], - progress_callback: Optional[Callable] = None, - timeout: int = 600 -): - """等待所有episode处理完成""" - -def get_graph_data(graph_id: str) -> Dict: - """获取完整图谱数据(节点和边)""" -``` - -**关键技术点**: -1. **动态类创建**: 根据本体定义动态创建Pydantic类 -2. **批量上传**: 避免一次性提交大量数据 -3. **异步等待**: 轮询episode的`processed`状态 -4. **容错重试**: 所有API调用带重试机制 - ---- - -### 3. ZepEntityReader (实体读取器) - -**文件**: `app/services/zep_entity_reader.py` - -**功能**: 从Zep图谱读取并过滤实体 - -**核心方法**: -```python -def get_all_nodes(graph_id: str) -> List[Dict]: - """获取所有节点(带重试)""" - -def get_all_edges(graph_id: str) -> List[Dict]: - """获取所有边(带重试)""" - -def filter_defined_entities( - graph_id: str, - defined_entity_types: Optional[List[str]] = None, - enrich_with_edges: bool = True -) -> FilteredEntities: - """ - 筛选符合预定义类型的实体 - - 筛选逻辑: - - 只保留Labels中包含除"Entity"和"Node"外的自定义标签的节点 - - 如果指定了entity_types,只保留匹配的类型 - - 可选:获取每个实体的相关边和关联节点 - """ - -def get_entity_with_context( - graph_id: str, - entity_uuid: str -) -> Optional[EntityNode]: - """获取单个实体及其完整上下文""" -``` - -**容错机制**: -- 所有Zep API调用带**3次重试** -- 使用指数退避策略 -- 详细的日志记录 - ---- - -### 4. OasisProfileGenerator (人设生成器) - -**文件**: `app/services/oasis_profile_generator.py` - -**功能**: 将图谱实体转换为OASIS Agent Profile - -**核心方法**: -```python -def generate_profile_from_entity( - entity: EntityNode, - user_id: int, - use_llm: bool = True -) -> OasisAgentProfile: - """ - 从实体生成Agent人设 - - 步骤: - 1. 构建实体上下文(属性+边+关联节点+Zep检索) - 2. 使用LLM生成详细人设(2000字persona) - 3. 返回OasisAgentProfile对象 - """ - -def generate_profiles_from_entities( - entities: List[EntityNode], - use_llm: bool = True, - progress_callback: Optional[callable] = None, - graph_id: Optional[str] = None, - parallel_count: int = 5 -) -> List[OasisAgentProfile]: - """ - 批量生成人设(支持并行) - - 特性: - - 并行生成(默认5个并发) - - Zep混合检索增强上下文 - - 区分个人实体和机构实体 - - 容错处理(失败则使用规则生成) - """ -``` - -**LLM提示词设计**: -- **个人实体**: 生成2000字详细人设(基本信息+背景+性格+社交行为+立场观点+个人记忆) -- **机构实体**: 生成官方账号设定(机构信息+账号定位+发言风格+发布内容+立场态度+机构记忆) -- **输出格式**: JSON (bio, persona, age, gender, mbti, country, profession, interested_topics) - -**容错措施**: -1. LLM调用失败:最多重试3次 -2. JSON解析失败:尝试修复JSON -3. 完全失败:使用规则生成基础人设 - ---- - -### 5. SimulationConfigGenerator (配置生成器) - -**文件**: `app/services/simulation_config_generator.py` - -**功能**: 使用LLM智能生成模拟配置参数 - -**核心方法**: -```python -def generate_config( - simulation_id: str, - project_id: str, - graph_id: str, - simulation_requirement: str, - document_text: str, - entities: List[EntityNode], - enable_twitter: bool = True, - enable_reddit: bool = True, - progress_callback: Optional[Callable] = None, -) -> SimulationParameters: - """ - 智能生成完整模拟配置 - - 分步生成策略(避免一次性生成过长): - 1. 生成时间配置(符合中国人作息) - 2. 生成事件配置(热点话题+初始帖子) - 3. 分批生成Agent配置(每批15个) - 4. 生成平台配置 - """ -``` - -**时间配置特点**: -- **高峰时段**: 19-22点(活跃度系数1.5) -- **低谷时段**: 0-5点(活跃度系数0.05) -- **早间时段**: 6-8点(活跃度系数0.4) -- **工作时段**: 9-18点(活跃度系数0.7) - -**Agent配置规则**: -- **官方机构**: 活跃度低(0.1-0.3),工作时间活动,响应慢,影响力高(2.5-3.0) -- **媒体**: 活跃度中(0.4-0.6),全天活动,响应快,影响力高(2.0-2.5) -- **个人/学生**: 活跃度高(0.6-0.9),晚间活动,响应快,影响力低(0.8-1.2) -- **专家/教授**: 活跃度中(0.4-0.6),工作+晚间,影响力中高(1.5-2.0) - ---- - -### 6. SimulationManager (模拟管理器) - -**文件**: `app/services/simulation_manager.py` - -**功能**: 管理模拟的完整生命周期 - -**核心方法**: -```python -def create_simulation( - project_id: str, - graph_id: str, - enable_twitter: bool = True, - enable_reddit: bool = True, -) -> SimulationState: - """创建新模拟""" - -def prepare_simulation( - simulation_id: str, - simulation_requirement: str, - document_text: str, - defined_entity_types: Optional[List[str]] = None, - use_llm_for_profiles: bool = True, - progress_callback: Optional[callable] = None, - parallel_profile_count: int = 3 -) -> SimulationState: - """ - 准备模拟环境(全程自动化) - - 步骤: - 1. 读取并过滤图谱实体 - 2. 并行生成Agent人设(带Zep检索增强) - 3. LLM智能生成模拟配置 - 4. 保存配置和人设文件 - """ - -def get_simulation(simulation_id: str) -> Optional[SimulationState]: - """获取模拟状态""" - -def list_simulations(project_id: Optional[str] = None) -> List[SimulationState]: - """列出所有模拟""" -``` - -**数据存储**: -``` -uploads/simulations/sim_xxx/ -├── state.json # 模拟状态 -├── simulation_config.json # 模拟配置(LLM生成) -├── reddit_profiles.json # Reddit人设(JSON格式) -├── twitter_profiles.csv # Twitter人设(CSV格式) -├── run_state.json # 运行状态 -├── simulation.log # 主日志 -├── twitter/ -│ ├── actions.jsonl # Twitter动作日志 -│ └── twitter_simulation.db # Twitter数据库 -└── reddit/ - ├── actions.jsonl # Reddit动作日志 - └── reddit_simulation.db # Reddit数据库 -``` - ---- - -### 7. SimulationRunner (模拟运行器) - -**文件**: `app/services/simulation_runner.py` - -**功能**: 在后台运行OASIS模拟并实时监控 - -**核心方法**: -```python -@classmethod -def start_simulation( - cls, - simulation_id: str, - platform: str = "parallel" -) -> SimulationRunState: - """ - 启动模拟 - - 步骤: - 1. 启动模拟进程(subprocess) - 2. 创建监控线程 - 3. 解析动作日志 - 4. 实时更新状态 - """ - -@classmethod -def stop_simulation(cls, simulation_id: str) -> SimulationRunState: - """ - 停止模拟 - - 使用进程组终止(确保子进程也被终止) - """ - -@classmethod -def get_run_state(cls, simulation_id: str) -> Optional[SimulationRunState]: - """获取运行状态""" - -@classmethod -def get_actions( - cls, - simulation_id: str, - limit: int = 100, - offset: int = 0, - platform: Optional[str] = None, - agent_id: Optional[int] = None, - round_num: Optional[int] = None -) -> List[AgentAction]: - """获取动作历史(支持过滤)""" - -@classmethod -def cleanup_all_simulations(cls): - """清理所有运行中的模拟进程(服务器关闭时调用)""" -``` - -**进程管理**: -- 使用`subprocess.Popen`启动模拟脚本 -- 使用`start_new_session=True`创建新进程组 -- 使用`os.killpg`终止整个进程组 -- 支持优雅关闭(SIGTERM)和强制终止(SIGKILL) - -**日志解析**: -- 实时读取`twitter/actions.jsonl`和`reddit/actions.jsonl` -- 解析每个Agent的动作记录 -- 更新运行状态和进度 -- 保存最近50个动作用于前端展示 - ---- - -### 8. ZepGraphMemoryUpdater (图谱记忆更新器) - -**文件**: `app/services/zep_graph_memory_updater.py` - -**功能**: 将模拟中的Agent活动动态更新到Zep图谱 - -**核心类**: - -```python -class AgentActivity: - """Agent活动记录""" - platform: str # twitter / reddit - agent_id: int - agent_name: str - action_type: str # CREATE_POST, LIKE_POST, etc. - action_args: Dict - round_num: int - timestamp: str - - def to_episode_text(self) -> str: - """ - 将活动转换为自然语言描述(不添加模拟前缀) - - 示例输出: - - "张三: 发布了一条帖子:「官方声明:...」" - - "李四: 在帖子#5下评论道:「我认为...」" - - "王五: 引用帖子#3并评论:「同意!」" - """ -``` - -```python -class ZepGraphMemoryUpdater: - """ - 图谱记忆更新器 - - 特性: - - 逐条发送活动到Zep,确保图谱正确解析 - - 后台线程异步处理,不阻塞主模拟流程 - - 带重试的API调用(MAX_RETRIES=3) - - 自动跳过DO_NOTHING类型的活动 - - 发送间隔控制(SEND_INTERVAL=0.5秒) - """ - - def start(self): - """启动后台工作线程""" - - def stop(self): - """停止并发送剩余活动""" - - def add_activity(self, activity: AgentActivity): - """添加活动到队列""" - - def add_activity_from_dict(self, data: Dict, platform: str): - """从动作日志字典添加活动""" - - def get_stats(self) -> Dict: - """获取统计信息(total_activities, total_sent, failed_count等)""" -``` - -```python -class ZepGraphMemoryManager: - """ - 管理多个模拟的更新器实例 - """ - - @classmethod - def create_updater(cls, simulation_id: str, graph_id: str) -> ZepGraphMemoryUpdater: - """为模拟创建并启动更新器""" - - @classmethod - def get_updater(cls, simulation_id: str) -> Optional[ZepGraphMemoryUpdater]: - """获取模拟的更新器""" - - @classmethod - def stop_updater(cls, simulation_id: str): - """停止并移除模拟的更新器""" - - @classmethod - def stop_all(cls): - """停止所有更新器(服务器关闭时调用)""" -``` - -**活动类型转换**: - -| action_type | 转换后的描述 | -|-------------|-------------| -| CREATE_POST | 发布了一条帖子:「{content}」 | -| LIKE_POST | 点赞了帖子#{post_id} | -| DISLIKE_POST | 踩了帖子#{post_id} | -| REPOST | 转发了帖子#{post_id} | -| QUOTE_POST | 引用帖子#{quoted_id}并评论:「{content}」 | -| FOLLOW | 关注了用户#{user_id} | -| CREATE_COMMENT | 在帖子#{post_id}下评论道:「{content}」 | -| LIKE_COMMENT | 点赞了评论#{comment_id} | -| SEARCH_POSTS | 搜索了「{query}」 | -| MUTE | 屏蔽了用户#{user_id} | - -**使用示例**: - -```python -# 在启动模拟时启用图谱记忆更新 -POST /api/simulation/start -{ - "simulation_id": "sim_xxx", - "enable_graph_memory_update": true -} -``` - -启用后,模拟中的活动会被逐条转换为自然语言描述并发送到Zep: - -``` -上级: 发布了一条帖子:「官方声明:经复核并结合司法判决,校方决定撤销对肖某某的处分。学校向当事人致以正式歉意...」 -全国顶尖新闻传播学院的大学: 发布了一条帖子:「武汉大学官方发布:学校已决定撤销此前对当事人的处分...」 -全国考生: 引用帖子#5并评论 -教师代表: 在帖子#2下评论道:「此事暴露出高校在程序正义上的问题...」 -``` - -每条活动单独发送,确保Zep能正确从文本中提取实体(如人名、机构名)和关系,丰富图谱知识。 - ---- - -### 9. SimulationIPCClient/Server (IPC通信模块) - -**文件**: `app/services/simulation_ipc.py` - -**功能**: 实现Flask后端与模拟脚本之间的进程间通信 - -**核心类**: - -```python -class SimulationIPCClient: - """IPC客户端(Flask端使用)""" - - def send_interview(agent_id: int, prompt: str, timeout: float) -> IPCResponse: - """发送单个Agent采访命令""" - - def send_batch_interview(interviews: List[Dict], timeout: float) -> IPCResponse: - """发送批量采访命令""" - - def send_close_env(timeout: float) -> IPCResponse: - """发送关闭环境命令""" - - def check_env_alive() -> bool: - """检查模拟环境是否存活""" -``` - -```python -class SimulationIPCServer: - """IPC服务器(模拟脚本端使用)""" - - def poll_commands() -> Optional[IPCCommand]: - """轮询获取待处理命令""" - - def send_response(response: IPCResponse): - """发送响应""" -``` - -**命令类型**: - -| 命令类型 | 说明 | -|----------|------| -| interview | 单个Agent采访 | -| batch_interview | 批量采访 | -| close_env | 关闭环境 | - -**文件结构**: - -``` -uploads/simulations/sim_xxx/ -├── ipc_commands/ # 命令文件目录 -│ └── {command_id}.json # 待处理命令 -├── ipc_responses/ # 响应文件目录 -│ └── {command_id}.json # 命令响应 -└── env_status.json # 环境状态文件 -``` - -**使用示例**: - -```python -# Flask端发送Interview命令 -from app.services import SimulationRunner - -# 单个采访 -result = SimulationRunner.interview_agent( - simulation_id="sim_xxx", - agent_id=0, - prompt="你对这件事有什么看法?" -) - -# 批量采访 -result = SimulationRunner.interview_agents_batch( - simulation_id="sim_xxx", - interviews=[ - {"agent_id": 0, "prompt": "问题A"}, - {"agent_id": 1, "prompt": "问题B"} - ] -) - -# 全局采访 -result = SimulationRunner.interview_all_agents( - simulation_id="sim_xxx", - prompt="你认为事件会如何发展?" -) -``` - ---- - -### 10. ZepToolsService (Zep检索工具服务) - -**文件**: `app/services/zep_tools.py` - -**功能**: 封装多种Zep图谱检索工具,供Report Agent调用 - -**核心方法**: - -```python -def search_graph( - graph_id: str, - query: str, - limit: int = 10 -) -> SearchResult: - """ - 图谱语义搜索 - - 使用混合搜索(语义+BM25)查找相关信息 - 返回: facts列表、edges列表、nodes列表 - """ - -def get_all_nodes(graph_id: str) -> List[NodeInfo]: - """获取图谱所有节点""" - -def get_all_edges(graph_id: str) -> List[EdgeInfo]: - """获取图谱所有边""" - -def get_node_detail(node_uuid: str) -> Optional[NodeInfo]: - """获取单个节点详情""" - -def get_node_edges(node_uuid: str) -> List[EdgeInfo]: - """获取节点相关的边""" - -def get_entities_by_type( - graph_id: str, - entity_type: str -) -> List[NodeInfo]: - """按类型获取实体""" - -def get_entity_summary( - graph_id: str, - entity_name: str -) -> Dict[str, Any]: - """获取实体关系摘要""" - -def get_graph_statistics(graph_id: str) -> Dict[str, Any]: - """ - 获取图谱统计信息 - - 返回: - - total_nodes: 节点总数 - - total_edges: 边总数 - - entity_types: 实体类型分布 - - relation_types: 关系类型分布 - """ - -def get_simulation_context( - graph_id: str, - simulation_requirement: str, - limit: int = 30 -) -> Dict[str, Any]: - """ - 获取模拟相关上下文 - - 综合搜索与模拟需求相关的所有信息 - """ -``` - -**容错机制**: -- 所有API调用带3次重试 -- 指数退避策略 -- 搜索失败返回空结果而非抛出异常 - ---- - -### 11. ReportAgent (报告生成Agent) - -**文件**: `app/services/report_agent.py` - -**功能**: 使用ReACT模式生成模拟分析报告 - -**核心类**: - -```python -class ReportAgent: - """ - Report Agent - 模拟报告生成Agent - - 采用ReACT(Reasoning + Acting)模式: - 1. 规划阶段:分析模拟需求,规划报告目录结构 - 2. 生成阶段:逐章节生成内容,每章节可多次调用工具获取信息 - 3. 对话阶段:支持与用户对话,自主调用检索工具 - """ - - # 配置 - MAX_TOOL_CALLS_PER_SECTION = 5 # 每章节最大工具调用次数 - MAX_REFLECTION_ROUNDS = 2 # 最大反思轮数 -``` - -**核心方法**: - -```python -def plan_outline( - progress_callback: Optional[Callable] = None -) -> ReportOutline: - """ - 规划报告大纲 - - 步骤: - 1. 获取模拟上下文(图谱统计、相关事实) - 2. 使用LLM分析并生成大纲结构 - 3. 返回包含章节列表的大纲对象 - """ - -def _generate_section_react( - section: ReportSection, - outline: ReportOutline, - previous_sections: List[str], - progress_callback: Optional[Callable] = None -) -> str: - """ - 使用ReACT模式生成单个章节 - - ReACT循环: - 1. Thought(思考)- 分析需要什么信息 - 2. Action(行动)- 调用工具获取信息 - 3. Observation(观察)- 分析工具返回结果 - 4. 重复直到信息足够或达到最大次数 - 5. Final Answer(最终回答)- 生成章节内容 - """ - -def generate_report( - progress_callback: Optional[Callable] = None -) -> Report: - """ - 生成完整报告 - - 步骤: - 1. 规划大纲 - 2. 逐章节生成(ReACT模式) - 3. 组装Markdown报告 - 4. 保存报告文件 - """ - -def chat( - message: str, - chat_history: List[Dict[str, str]] = None -) -> Dict[str, Any]: - """ - 与Report Agent对话 - - 在对话中Agent可以自主调用检索工具来回答问题 - - Returns: - { - "response": "Agent回复", - "tool_calls": [调用的工具列表], - "sources": [信息来源] - } - """ -``` - -**工具调用格式**: - -Agent使用以下格式调用工具: - -``` - -{"name": "search_graph", "parameters": {"query": "舆情走向", "limit": 10}} - -``` - -或者: - -``` -[TOOL_CALL] search_graph(query="舆情走向", limit="10") -``` - -**报告大纲结构示例**: - -```json -{ - "title": "武汉大学撤销处分事件舆情分析报告", - "summary": "基于模拟结果的全面舆情分析", - "sections": [ - { - "title": "执行摘要", - "description": "简要总结模拟结果和关键发现" - }, - { - "title": "模拟背景", - "description": "描述模拟的初始条件和场景设定" - }, - { - "title": "关键发现", - "description": "分析模拟中的重要发现和趋势" - }, - { - "title": "舆情分析", - "description": "分析舆论走向、情绪变化、关键意见领袖" - }, - { - "title": "影响评估", - "description": "评估事件的影响范围和程度" - }, - { - "title": "建议与展望", - "description": "基于分析结果提出建议" - } - ] -} -``` - ---- - -### 12. ReportManager (报告管理器) - -**文件**: `app/services/report_agent.py` - -**功能**: 报告的持久化存储和检索 - -**核心方法**: - -```python -@classmethod -def save_report(cls, report: Report) -> None: - """ - 保存报告 - - 同时保存: - - JSON文件(报告元数据) - - Markdown文件(报告内容) - """ - -@classmethod -def get_report(cls, report_id: str) -> Optional[Report]: - """获取报告""" - -@classmethod -def get_report_by_simulation(cls, simulation_id: str) -> Optional[Report]: - """根据模拟ID获取报告""" - -@classmethod -def list_reports( - cls, - simulation_id: Optional[str] = None, - limit: int = 50 -) -> List[Report]: - """列出报告""" - -@classmethod -def delete_report(cls, report_id: str) -> bool: - """删除报告""" -``` - -**存储结构**: -``` -uploads/reports/ -├── report_abc123.json # 报告元数据 -└── report_abc123.md # Markdown报告 -``` - ---- - -## 工具类 - -### 1. FileParser (文件解析器) - -**文件**: `app/utils/file_parser.py` - -**功能**: 从PDF/MD/TXT文件提取文本 - -**支持格式**: -- PDF: 使用PyMuPDF -- Markdown: 直接读取 -- TXT: 直接读取 - -**核心方法**: -```python -@classmethod -def extract_text(cls, file_path: str) -> str: - """从文件提取文本""" - -@classmethod -def extract_from_multiple(cls, file_paths: List[str]) -> str: - """从多个文件提取并合并文本""" - -def split_text_into_chunks( - text: str, - chunk_size: int = 500, - overlap: int = 50 -) -> List[str]: - """ - 文本分块 - - 特点: - - 尝试在句子边界分割 - - 支持中英文句子结束符 - - 块之间有重叠(overlap) - """ -``` - ---- - -### 2. LLMClient (LLM客户端) - -**文件**: `app/utils/llm_client.py` - -**功能**: 统一的LLM调用封装(OpenAI格式) - -**核心方法**: -```python -def chat( - self, - messages: List[Dict[str, str]], - temperature: float = 0.7, - max_tokens: int = 4096, - response_format: Optional[Dict] = None -) -> str: - """发送聊天请求""" - -def chat_json( - self, - messages: List[Dict[str, str]], - temperature: float = 0.3, - max_tokens: int = 4096 -) -> Dict[str, Any]: - """发送聊天请求并返回JSON""" -``` - -**配置**: -- 从`Config.LLM_API_KEY`读取API密钥 -- 从`Config.LLM_BASE_URL`读取API地址 -- 从`Config.LLM_MODEL_NAME`读取模型名称 - ---- - -### 3. Logger (日志管理) - -**文件**: `app/utils/logger.py` - -**功能**: 统一的日志配置 - -**特点**: -- 双输出:控制台(INFO+) + 文件(DEBUG+) -- 按日期命名日志文件 -- 日志轮转(10MB,保留5个备份) -- 详细格式(文件) + 简洁格式(控制台) - -**使用方法**: -```python -from app.utils.logger import get_logger - -logger = get_logger('mirofish.mymodule') -logger.debug("调试信息") -logger.info("普通信息") -logger.warning("警告") -logger.error("错误") -``` - ---- - -### 4. Retry (重试机制) - -**文件**: `app/utils/retry.py` - -**功能**: API调用重试装饰器 - -**核心方法**: -```python -@retry_with_backoff( - max_retries=3, - initial_delay=1.0, - backoff_factor=2.0, - exceptions=(ConnectionError, TimeoutError) -) -def call_api(): - ... -``` - -**特点**: -- 指数退避 -- 随机抖动(避免雷击) -- 自定义异常类型 -- 重试回调 - ---- - -## 配置说明 - -### 环境变量配置 - -在项目根目录创建`.env`文件: - -```bash -# Flask配置 -FLASK_DEBUG=True -FLASK_HOST=0.0.0.0 -FLASK_PORT=5001 -SECRET_KEY=your-secret-key - -# LLM配置(OpenAI兼容接口) -LLM_API_KEY=sk-xxx -LLM_BASE_URL=https://api.openai.com/v1 -LLM_MODEL_NAME=gpt-4o-mini - -# Zep配置 -ZEP_API_KEY=z_xxx - -# OASIS模拟配置 -OASIS_DEFAULT_MAX_ROUNDS=10 - -# Report Agent配置(可选) -REPORT_AGENT_MAX_TOOL_CALLS=5 -REPORT_AGENT_MAX_REFLECTION_ROUNDS=2 -REPORT_AGENT_TEMPERATURE=0.5 -``` - -### 配置项说明 - -| 配置项 | 类型 | 默认值 | 说明 | -|--------|------|--------|------| -| FLASK_DEBUG | Boolean | True | 调试模式 | -| FLASK_HOST | String | 0.0.0.0 | 监听地址 | -| FLASK_PORT | Integer | 5001 | 监听端口 | -| SECRET_KEY | String | - | Flask密钥 | -| LLM_API_KEY | String | - | LLM API密钥(必填) | -| LLM_BASE_URL | String | https://api.openai.com/v1 | LLM API地址 | -| LLM_MODEL_NAME | String | gpt-4o-mini | LLM模型名称 | -| ZEP_API_KEY | String | - | Zep API密钥(必填) | -| OASIS_DEFAULT_MAX_ROUNDS | Integer | 10 | 默认模拟轮数 | -| REPORT_AGENT_MAX_TOOL_CALLS | Integer | 5 | 每章节最大工具调用次数 | -| REPORT_AGENT_MAX_REFLECTION_ROUNDS | Integer | 2 | 最大反思轮数 | -| REPORT_AGENT_TEMPERATURE | Float | 0.5 | 报告生成温度参数 | - ---- - -## 运行指南 - -### 1. 环境准备 - -```bash -# 1. 激活conda环境 -conda activate MiroFish - -# 2. 安装依赖 -cd backend -pip install -r requirements.txt - -# 3. 配置环境变量 -cp .env.example .env -# 编辑.env文件,填入API密钥 -``` - -### 2. 启动服务 - -```bash -# 启动Flask服务 -python run.py -``` - -服务启动后访问: -- 主页: http://localhost:5001 -- 健康检查: http://localhost:5001/health -- API文档: (见上文API接口文档) - -### 3. 使用流程 - -**完整流程示例**: - -```bash -# Step 1: 上传文档并生成本体 -curl -X POST http://localhost:5001/api/graph/ontology/generate \ - -F "files=@document.pdf" \ - -F "simulation_requirement=模拟学术不端事件的舆论发展" \ - -F "project_name=学术不端事件" - -# 返回: project_id, ontology - -# Step 2: 构建图谱 -curl -X POST http://localhost:5001/api/graph/build \ - -H "Content-Type: application/json" \ - -d '{ - "project_id": "proj_xxx", - "graph_name": "学术不端事件图谱" - }' - -# 返回: task_id - -# Step 3: 查询构建进度 -curl http://localhost:5001/api/graph/task/{task_id} - -# 等待status=completed, 获取graph_id - -# Step 4: 创建模拟 -curl -X POST http://localhost:5001/api/simulation/create \ - -H "Content-Type: application/json" \ - -d '{ - "project_id": "proj_xxx", - "graph_id": "mirofish_xxx" - }' - -# 返回: simulation_id - -# Step 5: 准备模拟 -curl -X POST http://localhost:5001/api/simulation/prepare \ - -H "Content-Type: application/json" \ - -d '{ - "simulation_id": "sim_xxx", - "use_llm_for_profiles": true, - "parallel_profile_count": 5 - }' - -# 返回: task_id - -# Step 6: 查询准备进度 -curl -X POST http://localhost:5001/api/simulation/prepare/status \ - -H "Content-Type: application/json" \ - -d '{ - "task_id": "task_xxx", - "simulation_id": "sim_xxx" - }' - -# 等待status=completed - -# Step 7: 启动模拟(可选参数:max_rounds限制轮数,enable_graph_memory_update启用图谱记忆更新) -curl -X POST http://localhost:5001/api/simulation/start \ - -H "Content-Type: application/json" \ - -d '{ - "simulation_id": "sim_xxx", - "platform": "parallel", - "max_rounds": 50, - "enable_graph_memory_update": true - }' - -# Step 8: 实时查询运行状态 -curl http://localhost:5001/api/simulation/{sim_xxx}/run-status - -# Step 9: 检查环境状态(模拟完成后环境会进入等待命令模式) -curl http://localhost:5001/api/simulation/{sim_xxx}/env-status - -# Step 10: 采访单个Agent -curl -X POST http://localhost:5001/api/simulation/{sim_xxx}/interview \ - -H "Content-Type: application/json" \ - -d '{ - "agent_id": 0, - "prompt": "你对这件事有什么看法?" - }' - -# Step 11: 全局采访(采访所有Agent) -curl -X POST http://localhost:5001/api/simulation/{sim_xxx}/interview/all \ - -H "Content-Type: application/json" \ - -d '{ - "prompt": "你认为事件的后续发展会如何?" - }' - -# Step 12: 获取Interview历史 -curl http://localhost:5001/api/simulation/{sim_xxx}/interview/history - -# Step 13: 生成模拟分析报告 -curl -X POST http://localhost:5001/api/report/generate \ - -H "Content-Type: application/json" \ - -d '{ - "simulation_id": "sim_xxx" - }' - -# 返回: task_id - -# Step 14: 查询报告生成进度 -curl -X POST http://localhost:5001/api/report/generate/status \ - -H "Content-Type: application/json" \ - -d '{ - "task_id": "task_xxx", - "simulation_id": "sim_xxx" - }' - -# 等待status=completed - -# Step 15: 获取报告 -curl http://localhost:5001/api/report/by-simulation/sim_xxx - -# Step 16: 与Report Agent对话 -curl -X POST http://localhost:5001/api/report/chat \ - -H "Content-Type: application/json" \ - -d '{ - "simulation_id": "sim_xxx", - "message": "请解释一下舆情的主要趋势" - }' - -# Step 17: 下载Markdown报告 -curl -O http://localhost:5001/api/report/{report_id}/download - -# Step 18: 关闭模拟环境(优雅退出) -curl -X POST http://localhost:5001/api/simulation/close-env \ - -H "Content-Type: application/json" \ - -d '{ - "simulation_id": "sim_xxx" - }' - -# 或者强制停止模拟 -curl -X POST http://localhost:5001/api/simulation/stop \ - -H "Content-Type: application/json" \ - -d '{ - "simulation_id": "sim_xxx" - }' -``` - ---- - -## 开发指南 - -### 添加新的实体类型 - -1. 修改本体生成提示词(`app/services/ontology_generator.py`) -2. 更新实体类型参考列表 -3. 测试本体生成 - -### 添加新的平台支持 - -1. 在`app/services/oasis_profile_generator.py`添加平台格式转换方法 -2. 在`app/services/simulation_manager.py`更新文件保存逻辑 -3. 在`scripts/`目录添加平台模拟脚本 -4. 更新`SimulationRunner`的平台检测逻辑 - -### 自定义LLM提示词 - -主要提示词文件: -- 本体生成: `app/services/ontology_generator.py` → `ONTOLOGY_SYSTEM_PROMPT` -- 人设生成: `app/services/oasis_profile_generator.py` → `_build_individual_persona_prompt` -- 配置生成: `app/services/simulation_config_generator.py` → `_generate_time_config` - -### 调试技巧 - -1. **查看日志**: - ```bash - tail -f logs/$(date +%Y-%m-%d).log - ``` - -2. **测试API**: - ```bash - # 使用httpie - http POST localhost:5001/api/graph/ontology/generate \ - files@document.pdf \ - simulation_requirement="测试需求" - ``` - -3. **调试模式**: - ```python - # 在代码中添加断点 - import pdb; pdb.set_trace() - ``` - ---- - -## 常见问题 - -### Q1: Zep API调用失败 - -**原因**: API密钥错误或网络问题 - -**解决**: -1. 检查`.env`中的`ZEP_API_KEY` -2. 测试Zep连接: - ```python - from zep_cloud.client import Zep - client = Zep(api_key="your-key") - client.graph.list() - ``` -3. 查看日志中的详细错误信息 - -### Q2: LLM生成的JSON解析失败 - -**原因**: LLM输出被截断或格式不正确 - -**解决**: -- 系统已实现JSON修复逻辑 -- 如仍失败,会自动回退到规则生成 -- 可调整`temperature`参数降低随机性 - -### Q3: 模拟进程启动失败 - -**原因**: conda环境未激活或依赖缺失 - -**解决**: -```bash -# 确保在MiroFish环境中 -conda activate MiroFish - -# 检查OASIS依赖 -pip install oasis-ai camel-ai -``` - -### Q4: 内存不足 - -**原因**: 大型文档或大量实体 - -**解决**: -1. 减小chunk_size -2. 限制entity_types数量 -3. 使用更小的LLM模型 -4. 增加系统内存 - -### Q5: 文件上传失败 - -**原因**: 文件大小超过限制或格式不支持 - -**解决**: -- 检查`Config.MAX_CONTENT_LENGTH`(默认50MB) -- 支持格式:PDF/MD/TXT -- 确保文件编码为UTF-8 - ---- - -## 性能优化建议 - -1. **并行处理**: - - 人设生成并行数:`parallel_profile_count=5` - - Zep批量上传:`batch_size=3` - -2. **缓存策略**: - - 项目状态已持久化到文件 - - 任务状态使用内存缓存 - -3. **容错重试**: - - Zep API调用:3次重试 - - LLM API调用:3次重试 - -4. **日志管理**: - - 日志文件自动轮转 - - 控制台只显示INFO+ - ---- - -## 贡献指南 - -### 代码规范 - -1. 遵循PEP 8 -2. 使用类型注解 -3. 添加docstring -4. 编写单元测试 - -### 提交规范 - -``` -feat: 添加新功能 -fix: 修复bug -docs: 更新文档 -refactor: 重构代码 -test: 添加测试 -``` - ---- - -## 许可证 - -MIT License - ---- - -## 联系方式 - -- 项目地址: [GitHub链接] -- 问题反馈: [Issues链接] -- 技术文档: 见本README - ---- - -**最后更新**: 2025-12-09 -**版本**: v1.3.0 - -### 更新日志 - -**v1.3.0 (2025-12-09)**: -- 新增 Report Agent 模拟报告生成功能 - - 使用 LangChain + Zep 实现 ReACT 模式 - - 自动规划报告大纲,分段生成内容 - - 每章节可多次调用Zep检索工具获取信息 - - 生成专业的Markdown格式报告 -- 新增 Report Agent 对话功能 - - 报告完成后可与Agent对话 - - Agent自主调用检索工具回答问题 -- 新增 Zep 检索工具服务 - - 封装图谱搜索、节点读取、边查询等工具 - - 支持语义搜索、统计分析、上下文获取 -- 新增报告管理接口 - - 报告生成、查询、下载、删除 - - 报告状态检查(解锁Interview功能) -- 依赖更新 - - 新增 langchain>=0.2.0 - - 新增 langchain-core>=0.2.0 - - 新增 langchain-openai>=0.1.0 - -**v1.2.0 (2025-12-08)**: -- 新增 Interview 采访功能 - - 支持单个Agent采访 - - 支持批量采访多个Agent - - 支持全局采访(所有Agent使用相同问题) - - 支持获取Interview历史记录 -- 新增模拟状态持久化 - - 模拟完成后环境不立即关闭,进入等待命令模式 - - 支持优雅关闭环境命令 -- 新增 IPC 通信机制 - - Flask后端与模拟脚本之间的进程间通信 - - 基于文件系统的命令/响应模式 - -**v1.1.0 (2025-12-05)**: -- 新增图谱记忆动态更新功能 -- 支持 max_rounds 参数限制模拟轮数 - -**v1.0.0**: -- 初始版本发布 -- 支持知识图谱构建 -- 支持Agent人设生成 -- 支持双平台模拟 - diff --git a/frontend/index.html b/frontend/index.html index 621bf64..de8cabd 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,10 +5,10 @@ - + - MiroFish - 上传任意报告,模拟世界即刻开始 + MiroFish - 预测万物
diff --git a/frontend/public/icon.png b/frontend/public/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..20da2f06432e54d80c5ee5a7ba840d496f17e504 GIT binary patch literal 30341 zcmbq)`9IZ9^#8o>=Hl9AUoMs9o$UL1?Ud{ak!zQIOLi|ywg`!kbfZv&L}ba8iYy^( zWCUd)<3hk+C`LjJyyDit1VYf&TH;R{al_jd037 z`UJy)x!sp~a+{A+{PTflO7v%^th4v;8<>XcXJ_iYWNP2k403uCxMcp?^6(ccCbpF* z&W8KHi^b@cE6V0Advu3&f@`nBqBdET7>-Wbmss{GqTZN6%$7S^hiFg0BndM^heg3(H#C@pmKN#69azhu;#Ux9<3by)AgV}P7XB>(2FKLY zk6`uys2mc=q5un|&jP~m73I?3Q(qAdBG*4+#Q89Uu%rHxQ*tEsk#4jrvgDqNEd3xK z&5bE<)nu!Ic26)M0UOe1v$8gVgJW8PIao+V>y5PQOxCq!_8g(cuK$ohUB z9StpP9*`o(X%iJ!6Uw3aPr&=qR1y*(Z}I-?eFS~Rht36Z`K#IihB-7uS`N~+exGm7 zRgggv^q`ZX_hXvO`_VFD3Jozd1aKcbb%iCQ9dZ9=K7|@`fhRR8Y$%}`GLWY;rjz+N z01CS4Imsf0@EVt~&=y4jb&fMLto}IdmR?Q{!UdBfi6muiw$9vVu3ChugJBw?DGuH& zzb7@revbSH3=9B>)rSRTyAMpi^U1<5enI#$W-bL>z8I49h~BqZ<U z&mG})aT}ean>tYBx5_8+4Nmm_;RsIhGd7U&YihDz+Yl`l2leSwDteIlr071Hkar5< z;Gev^`84y&r-W2dzET5i%Mcc0ZVmbg=1UK4=>ccpH}-^_VSU~RLjY2azXbf z;JLbfCaQc-u61p#V!h%=&nSqrxVLH3B=*2R&D$9q-(J|xXD6gZSruG(!1&23;&^MX zJYHi`Wx4%VTI7s~&jW3fVKrA3?^nXP0?#J}yQDDdQrSAxH>d4yMcZ$fd z^{XnC>!ip>#Sb41e4$*fm}+PfTRfd&gR#7ScQbfMsIARUhU@pOZ^nF04yX;df(-rB zI^^nrNqp5%HsY0+H5Q7q;1je?9rw7~chQa{JdN;lxpnum14#y6=`XS80GwwjcAh;9 zl=+sgJzaAB+;yEmsHbBdS2_B3PGePQ?Dps<|E~Aw1aS}6jpdC8WB4(0c#xHxqd>~p zrp;vuwdtyuswXF=4>n!eMh_C-q#?Y42*!+>xfp}bM3-`Ve9JRZQMcU`<5KNYH1&E% zEhf=g+zG6GNMB9JIA0lj01N9yR=j_>qP7C*u9){QvipqNV-;-|Sgwjz+!j2?w7k1$ zBULJ_Y00^6y505oPC{Zg7-@**8~={;eRqYQ4=v0g9!}}{T>0{qGb-Xp8?~L$Cv?^f zqqIEibsf@lKc8Z%We8nRV>Nu^iZj(PC<;5pb*YDm!i8TRYsr#R5r=EyQslBF$V~?J zO^{G&;->Wd2_NbtLgk`0pG`HLF!fd_;z(d!j|%w~q`=Xazxx!nNR&qsn0CKkNXuZK zp67s)FQX;{QEqIoDD4E#%`Hl;sXpp1r=Xh%|1;E5v!d@zeXE)Y=gIb*7dNm78u)=%d-s+Xrt#i9Owa@@>gJ>-bF% zxp4Q(sNu&4w#i&m{9=mFYN)Gr8Aj1fS&Rf-WMWQrSL`MJ_4_yV16g=`Lq(^biq416 zzNUDC41D!rn4Ky50}WcMo!z+QCGqcyxuVEXwhh530Dt_0(tRw;wW2Z zQ#7(0_^8t?-$01PA_KDyL}y!KdobmYvXGAemXmUg`OX+^iytN)#~hMWWNYY$xPbyC zF?RhbIUdr|NbHkhrWdicY*}Y{Xh29<^uM)|Yt6Rmh;SniR{xd zod`Z_CV2yDt{dMPe0N^c(}TV*+DullZ4yQA}`Oay!tPu+6ML=I6bjH z!=q0B7#GtN;eA;%2b%nd8V-89ekrO-?qPxya-SVcW>5g1E5u?m`V+GF1%|l{(SHUo zqZ5Dnt9f#~;{e(1V569#Ne(xds;Xu9m!fi+^#%=`!Lrly-Mv-*7$-$$L*iP4=~u1u zQAlhQjqw1j9#&2yMEsQy6$p4t=4YEOPKeqsSNj?JIupz9G5=(YUu|m< zc@udqpG~7C1SCcgEz_}0lt|#>8uiVdq_+(0W$E>X(uFXKBLeOUA%N?c#H)!4)rPUO zvXt#m`Zzy^+HUi94%X!>aQ<4KB`3<=_JDe$g5FnCrSTO$sR~mX2BYU?ADg%O3&h4O z*p^=>WdG5z{78Mm*FvEg18H?54B~yc-uoUQtpXr7^GKG4*^MvsL*KWgew^7=I!nw$$4rMW&}h-gJ`) zF3~kkYaSkeZbuzga|J5uW+$`_%-?*I5k+fcl$Mq@qo3~CVoh;Fa$^{+cxy#_=Nziv z@=KTx1H6A2-ntud{igzA_DklGP(n{p%+cTV9iH$&zm{{zVCkw?qU|LHeb}?Sp4hFX z2<5A4Dz7J&=r%2z1IsFSd5wN^_ZO7=`(GF`2^=~t_q{HERy+&K~-@>P3LxZJa zphWEW-APBE(T!QZ3u^7#0nQrjUiY9JlS}8>?Nr4Qq2Zy2=IW7ty82*zAt>=VyY$tn zjJmmr)KJSqoO{5n!rwDGinWzYw>~t9g?`z#wF(QZ8RFqsXgg0szZJc@Yn*Md&SlC*#{`=W8EPeNL{V^rMT?+o} zgn}FP3K;M&QSmlsI=bKnkJi}TTJ)}b3vNYR(q&N#G~2u!x6yl^4aka;+d%{l{$Me# zITAdGtBT-i#TeR72rF|Ce#tJaJ2Bw;x^shYL?vY^`jh*S8JD($siG@(!7IG-`IRe! zIHF=STAZL1vF-?KW<#jFdIe0zjsE558M8tx_$$*73@^UJRMFD_307b1_15AxFvqLE z#T#m+(Bk)b$^m3}fv!H_#X}5uj@ft{TGT{4{X*Oqr6iA5)Mrz&*MWW=v4d1}wFR?p z{Uua#&D*nSN!aVc$swKzOXtI8Gc4o`ovEfpT!sN7~$h$h$L z(#4~bA#!OltgQv5GS-iw=gJr84Ot17@(4l2Vn)c1sb8JpcfFXx@Bgi~$?&J={pRsW zSU5V*GqJ6m(g{o;yeJ3B#+V0ih#B*Ic7oj&+Eu$+d%iR;7s5Wjn^)!_7E?MBE~*}b zmzpo>v$E4VHS{TxB%DVGpI7m-$|d|YH#$|UkF4cI8WVQRy1$scg>ZRv)vDj>;yzgz zu6zK$V%R`(nJxJKI{a(K6+UPD3dd_tjJ$y7ba3)lWJUtyyJkzJxP9ArKY^vc9QgYnWLYsNmA3C&em182GheXxoh z&ZWI!%HI(LwL>_3=(Vl8RgUyUz3A|}vS$V9kmPO|xF}f4hAdDF#dvXd)cp{f*l2MC zMi^TI@>Wu<26z91aacMg2?l`=@nIAEB)lXq?A zsg)F9vB$^<(cqOYYVpez7u_(*Y)5nX$goadM*R#iSw<-0@H-m<5%BqyZ5i_Oi7r*f zi(EAJpDwY$RzL zvZ-V=H%1;KL;mam90qxO<~`aC(CfneNlTt`>12NsT;JyF!fOKQTD_T(2^0k>pEMjt zkcq~1`blqY8Bd%37b8loc@Nn}P@*{Z?P(OZym2>QUn-S`Rz3BXcUp~E(-R+X%8ex0fJN=cg-`5c*>1o`6HMYeDVP~nFLFghUA_BKU@DgRxoD=u3| z7sZM0`>zDCO?02fScNktND%Jk^&IhrRSS#Tw@t37$hB0@Xfl3E&@%NIH*jdLUimoF zz8=1it}Gb)$6bS!hA4>0xaQ9*vVzyOehzcIjlbjdRE-np`S!%E336(UjJUcX?(2X> zC;aDmZoKUT;laLE6whrIK={vGDnxRzn~jj?rG6hVD&r0w7S!orbto3rrfTy*cdFX` zl8Vbv=2|8Rr6W z2DDdq-$c0Cz78Hs$AJV`clg)f|!*jng4bz0rXt?lJJpKP`gKzWd-Po$D&4-j9Ugk=J7Ytc%t(~XM)aTX$;yNy!E%mey8=@E z{+;s|3_Cc)Lbv#S@LW*|m%bg@q{7qkT9H*fYcKO^9Vs6riuEiBW?;hp>AOxtib1g-iV{j-Dg?Z>SVm z&X_sT0wd3SDdB_3O(4nyiu3FV^@iWA19XNvfoE-$15DToc5{FoA5p^Z1#q;E5P4}e z36M_;RhhqrjK;xL>d0V@Xx3vPmhVwV|BZDyz@&G2HPh10E_V-_$K2B8*VpvR4aogC zARfspXGTuS0d79Ry!kP8OHW6~sogkJd=Wsz{2v`a*#F?J-JK^v7&ng=1HLWm z@j|xg3c8M{7dnxWmgmHWr9gg9hn)ZtHk6p=0OetJ3a_Y<-T2+GU7WELq`Iv1O8d!R z;Y07{$RyM}4k#vk!5_`Aqk^2^dTOMNG?~mn>grs3dWG!P4>xA!QCli_z#l#eIzrQ* zvOoX41Ld=ke}_T{_*u@1zVb6N0Y`R8CYXtr84<~u-vBoodE(3>Hz4GHW~qyzTRgg$ z4G~5*!8JR@;)7)e#B@zyrIl;F2&UeQB7R%?KWA9<6K}*{wZl*SZOcgr!m4#^z#oJp z1JHkHOb#mjPWCH_W>sVb4NTLl!pk2AaUd`trpky07=ET^Bh%#DaS`31syV&s$FyJj zXpT_eX9@)3AI5J;vf`~^->W!)k=U(8k`&*IVBq6i_~SK9s0VD7ME)cRRy0v89OC(t zC^)IDm3ja~=pZt4gCV9Q_291@_}``rv(hub`R~C$mailb>G}z3MlYTS z2iCuXp;*Mm4>qEP*tD}dP4Ff{Up|;1^)LovSWEA*%rN z9jp(1XZ%5p zrWQXWta2$u!_WGXCrV}GVvge@DP;KlF#+mMKPyArol!~|IS%pvAkGPwsJcG(tzx2jG&?4$~a7T38)e7qF zBM(<1?S710dXe#ga*m<-tQzOL3QS1Z@swIf4PDXBX;#eD>xlgfJC4u@{Yzx-cQa0l z)zlTml*gfglE0yzZz9E|80jIoE4$s&K`*v*&n=uD_Y822zoGL=F?%~hRi!HSIfCdl z9x2~{cunxi!%*D~7b;-PCybW__}}RU&o6}fOJaml^1IO!3W~7&*_2ypc_o`68o_8XiDGN zLKs=^T!k)E>*WMx-*W+D+F6{j81)PD7j5P4!Y&K;*3VL=%pWI?(kXp>GA%~XTb!F= z7*;RbcL2Wmx%n_OyloG^+7lV1DKotu$S~u)n7qdH-{jerr<3L^S$lG(FMp~a7m|K| ze*`{&M!ses&MF4Z{1G^&kEO=cyMct^R+^6ki7Mt|QLmB-Bg&UA6Je`7Sk;2ppJO3! ztMoY~yf$+1k!^bMGdQ_Ho-C{D&$K-b&^B=}d+y|-SVaZ_G7vEy2Xq`jIR(mHIcCmu z>2TTI07!>lxY!p!Bn!v58$jy57b2~~w+b;K{O6+)rT0GrBBAHs2aa6?kW6zh$L z=oso7f}!RxXz`0%<-{5MH){3GgG;D4vgQJlD@7}$f+KY}=R5#%Eo@}4S`zFQ2@Z>@)yZ(==* zaaE+FjKvGp@6M?Kry?X+`qAIdhG)z^X`kdEhlqkNkJG#?Zb;O+(Nf@jFqThjxbS}B zP`jwWbY%WJxMB2RajN+bWn`Pn%aB~Kh#I(Y1r|R_bJRgxuLao}N*x=BXUAEUVwN$i z(@k>Kb~JeEXxbZq!@Jxg6)78^^SYjgC=(?HXx$JZfIdlw=!99+JEIqdso)W9r3Uxt z7q26q9S1{u4ZrtaTuqQPB3LsxeW|@nAN_O76?u##Sngt8ve94t8n%0&d{&I=&OPU$ z#$31~XH{^GgHo$v!XuCVv+QKc&0MnCh)nbVD#rS>%YZq07e_w~(bIeZ!SteXVu0Lp z`O`rj;r8R&f)6xd8*NRbG*=a)G)_47*l1-`kxq$_J^aia+ zsT^#`{~w-79Ngzghua!Mvbm?4^cL(C%fw@ZhK_b6>#ZK9B-{{Fe_IOkoU=Lraz z5!zdl@HbJ|`g`;4q*&^3v#kGsvvy9`b^EqUW`Y3DfapQmnw>fWuy5quuwefy^8L}^ zUoOPWN1Sfouz&RMTec?&%3u4Ap#mE+4Ek+VCk2^ptF^Ht|U(e z1V^V3GWVPT!7Y*Q0>FP!JL|pa$Tuz)i)M-@-MJ{U{ZQ^~C7P_EMgRMDK|?PlMnZ-_ z?{@zgvLKe>!Ae#3bC?A@HK7VW&mJ-RszzV=B0togLk+R;8Qj#k?WR{|?&9qFee_di z$}+U_SaU_6V^QCVI`IOgG@jk)TIJN}N3-F~lJ-F$V+5+%xVU4g^sxC>kKn#(^hSl+ zeS!1{^GjsQWF&lPFcjm1AdY; z6q2wotNL{C33Hp8y}X_i6MYvYRP=#To~oE{>jzJP9}`Sr%cNsw_|u^}VS$RU6+~{@ z95kQnvOuOU7c}#Bq0)o6QFSA8*S$tOWKX1=wfibM8EcD@Vs0h1gmlhF4f-oGvB-1L zO2-CTYofZi9yhoSKYte&`;SyBjk*>~dfUQ$(_y>c?w6Nco!`mUh~=X{O6TPyA|#7jxG3GXrYysrS9YC7U-QpbPCod4`o^+;{0!5x z)&9tqcImV7v@dp^TD8mV+*8Y#vL8MP6;ZP?5aqZz_a=ROb+fL?%klNGpy5DUlv;Sh zVPMu&_&#|}^;r7KU$-0@fA%LEueRRaE;nbg@ROF1&nT+7M~x-xNy*>vGIP1RaJ#dl z`Nh_!foI!FVmu-5o0}t8f4pz}x?Zsz6!tfW0~|?oab@bc&!u7jjg&VcaSg)D3?IV1}p)ESC<*TQLscfc zd%P`Dunrn*^nq_j3|DXer`ol>Q}PRKGm+~YM?`dfA3l9^Tc$a6)}{XOG1@zczvJS- z30$c1M~}Rye%t6{^A(2R#Ab^4Tb#t6(6Ec?rKC#WSH&og2;Cd!H&Vw(Sbr$F?4W>{db7kZA zWY5tXB_rVRDRSPw!kMuq!$)YQ5LP%C&&hl5^JMV|15TxTfIDx-B@9Ghg(FnSX~1)? z<>3acH#4zlG^$qyggS@7Z~n33+aPy>N{}-G{(M2FC+M$!e!(Z(zti}RcUZ#lMq7;m zXcVReXTjF3u%e%T2H`1Rt-0NTK@p$uKRB+rU`>j=w4O=RvbzbzlN@jKkR7Z-U`8lQp$|1aW6v9omSqdMj`mEFNUvmF z3~Z6)oAVGq14vx}20n#;FC)vE*E%I(o4Qjp?!Th!Kd)QJ_!!JbfgV?mUe3C`8 zaR*(kl$>B|55?bulNZi)X5D^&rP+J*#NsOi3r(HDli{5rIstWFsK*Q(LMp)xt$+%%2_ z*B>4%zZhh6M`MG=C)$Ky1ep>prjbwg4gg#OXS!1g778eKd=B&I@&3m*Q>cT}dw4%k z1{TSDZ}|EQ3U=Q4cNNdIB@Novc1`s z_87&)*-h&sv+ttkZ|TWi`cuw)>8f-8OFoOn#{(ZvqQ5!rI6N7-pvUih>vnfqM4eS6 z5c$QsD|6(ifq@_33XP$eF~M2?reY4~?S+ROnH_u2EJWw7U%!8~y;?fUt84A`U~{G& zJzc%k#<7|{Uze@Y%P*gJ6(T?tFUM=|ns?xPSymC5spF{t4dr`kv&cfH) z{X}EIsga|jp`|^2cG$|W{$cn%vDS9(hdzpvPae!yMPU1f=YJT1<=NWsK?G9K;F3eQIJO9EA@WjOnV03(G9*W zRbMJ$yWRjca`9rV*M;||@owI(VCJNU!WMqKpMHzJ9r*v`O4|}(5Z%(ZQQ-M+=m+MS z{6%ExNYCG1ErQtvlw0IQ0q@8qhES3sf{o2s|EWpHl9t79hs++eFuucME@1q2yf;e( z4F;PJuXuoWffJg`MH3eV#ge^1PuZ=uO#@xnTBoyT$mUh1B=8)R**LBjz2e~WWiLb5 zc+K{rV_`5V>b+zLbWY|e*bGs+R@@?bS}h`qmKiJFzd{@Ad*x}schN5Dmv9o5g<$=@K*Z=&G zzTN@zSN0^vL5ChnXf~}5(PE@Fyy(f=w?xbt)1yUj{Sk)zWJHkl5Q_A3V6Wy`(_K(v zf+aMC*S}1}`T|4(33^pYSqs%^=KEPA9e@=p%T^9o3r#j%YbIk$+stTt-8e&caB zq`0HYfUUB4>|ivXGW-fpn0o$u8!Xhr&5_x;A%jmv_R`skV0zN(%`6Q4bvg&N& zJSbns8S$lt3w8!se15%coBAf8+Maw3_iZih(KHy<030Lfjy{t=C0?>Mjo(Um^t(=> zzvv%ATa<|DOaQ^5%&;db;gthC5$C{02y(P1TAsv4i;{9osl|KcFUD^4N#40aTMw^I z+8!^+$DpcubF+{dH&Basf=tFF8wGguUU)EbT?a}y@^g?7Qbm@!CK6DD%q{R_#D(W3 z_Y9cN3YJ{f`@&A3c?Umb>w8E6j0$mVK%!Sxlp?5L-;tKH8BIYnyQ^-iPEJP9#vqLy zWzTz|IzC_tZ9UZ(N@Z!@5i>j8fkir;NUSUj+hlzFBSVWsjuyKhT z+@z9m;>W6s*zG%c%MC@u1PkE?tqq4=1Y9*PlB1)Hc;GyV`x>yK1+p|jo}gDOCrMR` zz#1Di9N448zslvNfS-{7M&etc>KX=cz^Fd>kmlKmR(cZj*D4gj7hAg901>l#blyR67n}as0)Mxq_a`E61SJ_=TOB)PXhfV}k-B5St;J7Phu=s>S6oRK zKT|_bz$y2pfp%kNVgP(NEDt8!zo{BugmXUqwM6D5A>AOC@tg=*ijEX1#s+iof%e44 z*X%TqWGkXNZX1Nxqs!6z=UcTWi$!{Up|hB1ScpW8G`cg#RooAsicBl=X}CGjBeXebgfv81_!kmR&N(A`a&Hl)*f2g#=_pW&yzY%N7+_$+lcl4R*k4 zg|8I3Me13dU9|mMz#tfPp(cn19*-yQ+l7&=Q!MS-35lF9Y`t;M1IpYSLA)UeyKq&0eaxbs4hSO=m{6D z&9^HI3+k#$Tttay{woDODEQkl$Uu7eU)5JTQNPBA46%ITh{Yy8S1ES*1%Eml!ZO#p z51peO$qj|8sNOj;79m5O`dC;AW&HMn4LJpqE`-2Kr+|+wXG${by2@@rzvg2u5DEWqOQvER|Q=o2g!>H^O2`faetv$3|AhZuHhL z5LF0>=hSSa?;js+PxU_XNJNOkARz>YH_Atn0vOft6r-GX_!uzvFZYw*VkqlD6Eh;b zfdNI1)-oOihUws=DhX=(@gD(2zDR+m_Jb9Vl>x0EeA9#+Sx1l&&*8@x8#}rDMv2CP zgux;>Gza;>$%cnmd4}SNTrhA{S6a-O(_$R3IH--?C(A+J_fNg%#sUq#p z&YCWdz!yD&dKfU5SgL$%$v*)725@o`*x*DQJ!$s%;>D$3D6`7c(ZNx3dp1uIF7rtr zV(xpaM5iMj67Ai)LUZOFcZW=l?M23#rVDr)_FJ9yCmlypW`(NHK2^MR{*t{<`97F| zee;72O*h7hrhMqID&Z5~EaALVNU1VG^~3;g5@7y@ZZ0;)Oc#D-ONglX zgda!4Dp_}oASkg5#BJGzTh))Fsun*grf`;OoKd&iZG%pF7QBi)yCa~V^L_VP&kS>4 z+ZDk~$7)A&P_D?s-M&sz10XrN2uOFD`@W~rMb@57> z)vtX>Yj}mOd88?^Zp=vrS05mA?pGISfCTE`2ixm~HiSg9?9~ltIKC26j7?c1dK(zU zvc^?Gkgquxd4OYAUWj7R+pZ~-n|aug$5~YCOR}O=scd_sI~ROK_6w6w(B2!no&foZ zP{0j!^hVW4eNe%YS`VrPac(rE=A%zf8Gs^=G%8D%{^&nH1zphR2c#)SMU13SgCa}_~fAi{$**sx@ezjE0`VY$R9VI9PXGU+*g zV&DkSrk8_tRS(sdw5H((-AYx$yCeTT^C}qy^RGBWA3oZ1aHErTJd7JgN}oKt6y{qK z0rj;)N1`NgW3m!O)hlY1MQb=2WU*}_K-OXWW4#BZ+&{7DdmYh}dvuSEGlkoF2};j# zDeMqeE;;;lXQm$UosHD<#ak2MQP$;oYPXwPJGX!AhH8|ZzKoYIy7r`V61hv$U9dH8 ze6_Q6?$zLkip8<4`1Iy$?$poo(aMtceSiPzzP&|Ve9Dn|bt9D>ZOPmG;c?gx^74gzZ; zs!VtHrUQiL-p(WXdb*A>UNE{0BBgozj=nkc3^gy8WMz(-nQYEy-3d-#NG&53Yx3q^ zcdR}=AbOX0fF~6%6!#P89{QWDM`eF*D0HlI=lK|L%eQV#Q*P7b`l;COXOyzSNXgZ? z`S*@fP*`4Mkt6dQYllu|)d<<(M;WY=mz2c0;xoG5-!64Ree{;)$w!xfjfF=ZHkfKg zwoU~$&wb5p*KL`BYS*g*xJy(ml_OIw8RLn?>P?N=ahCZGVFJSzt=?s|^;d*DA7(Sw zw;UC9-)qczI`(W`;9U3j=X*7IQEw)jEX;?0%%@4PlS{m_pN@9N9zC8FeFF>+b~uct zaC-d=4R;`d!VgxbS4)z5kxV7f;tun+g zJ}X12o4EtGPr`T}5wD#^ev8**A01-U7z>1q`5CzQ@2~=g_Fp$Nh2B?r z*%=P_I>YNkLzkoUX5BzLN_h@LQ71wUtGF$ z^=`!9g5PhnekEW(^F}?qy0%isdD5z%9gb!yp&LHaNpDtsnv>T=pz_=IQjk_uh zIe6p2e=UzbJ=*3^zj-vgXZ0OxOD>sgdnXxC{mGsNc((yAY?Ys7>+OM<`CWXzcVp@g z25EOBphXEipd#caf-b0{MWRBX3pYn=b64b}Mc1&$ZBcUA>Fj5K&*4JY?0s0bxc%!L zH?yI2?dAsA^sJi-f%hh@FicEu=@bWTpXcvLQO$W7)Gsn9$8 zXGod)Ho#AC$HfeNOBRTv9(u7;4|^-`SI$ZT##qok3zWQDADijn8M%li!OLd#)Nr%8 zZ=5LnES=ou4^PN4OT*ttKnqV0qg;`Qwi9GV=L;dU{z7az;7_E$E9#AMktr81iPpJ7 zz^nlR4x-FOiA;+oj98%96ij)htW$_ITa8j!^-ne-nEstQu>qrB!GB^1Ct60!UkP#2rH;E8L^a%JK)SbB`7}HB`fFupXZw2T`kyEM>_Wk(t|0X_ z{$tCrcR}VkWa*_~PsaMBPNXH4SX=pP5FB;&bsff>JMuoENIEL1OxbA9KkBO z1ODK1N);pe!3lf6U^dm{P6MEZxCSnAdq7S*m<|0hg!Vo1eDirSmk}^RK(Z2CWN{$U zZCe|o{Pvuu4fbE+gunVeI-6ozrH0dpE^&b2f#KJbtF9hgsk@e%?u_P_XA?)`(#z)`IGed{VbRCOVk~z;2rGN<#ThRj{H8M^qN_Fh)b_6cU3rL?fee8NXY&7vmaPsr^R}1?Hi%VF30-At8F|S0{c>l68yZ^Q$+}` zecet63ptx=%Di*-Kgx{f`S4aWuA08z`spulqq!;HHdM~Tj^UaNY+$V+uHCa^3gnJ`TCl%~v<% z47{72mQ;7zt<9|YY6Pke5{!c#ijtyq=iF?6+`E>7=ShrXwl`Vm{?(Z0Ig_Ad(l z6~3txoxR^NtM7iOzqONF6`krkOKS0(sj8&Ew#?ilsh!_6;CAS~-}|yqQbf`isS_n9 ztv~k*Ryyhb@x8>t@K^EGK(+6xl^Xxq*D~^iUTPF7lxCA;LZD%F+#D{-z}iOzODqv> zkRqZrZo3Tb;Y<^c7gP=}gw0(Z>nqjLkYGm|$C1xjC*X(!!p#9Vz_4oK6*;$kgMnnH zZg}}pp&rM&a`taFvRzHLNdKaY^i5+wSJR_~(Qh{Ow^94| z+1an%2hZ|#s{~Q_eo>O)xF1_-TD}W2F^}9NN2S-8v8#_@QXsf{QArv&`eWfyz9m&( z7D@QmJ`S(e2uISw)$%C%u_w2}4I|U+@%jtka_U^>v%_=Z=UF#|eOrlihzIRjutz3R z+4C+~OabqU@;PZt_P<*HRj_7r`vvjb*PGrpyp==@h#t0vZLxi<=s{W}_J^^Pm?*&*)}pPteisqW ziAn#H5ZzVu4rZn$hZs~mk7@O$7nfm7JTd{ac!JfQ;FaBE@c5wT8$C&}5kC1u5<bWd3$QtE5xI`!#+{iO_YSa5h=rDx%z-&f8T{3AI5E_ zLXO?rx=h`%EJdNG36Iz_OtYOcjzwG4Pv=uUz`~4->YofTRX!+Rx)6`{_oZCKe*hLy zV*<9cp??>2T@UL1Y(n(iR+iXFJsx}*J1YWJ2*T&V^>WO;bR;q0gX3#sWla?}iX2mB z!nZJp>dg%2AZJW2(>8CXv`jPf!p9|YlnU8DyQZ@gQqQ&l6r)jF3_0tVNrOB z3sgN}j8MH|0L+5|#n4D^2_ zo4em0sWqeR6kPM0k6<8RES>P*cI&-|6!l*Nn+mxAwam2@99o>kQByc(2p`ZnfM{dV zU^>&fBM;^%s6?9O7G>f4cVcd-Aqe2i?Z%FIYmjZd0fF6P$KW*n zOS74?)$W;^&K={#=54unf<;{kk}mI5Fb>$5%3+|Y2T5fITK{m-N?f(fu)Q(TtwaZw z#W@vzwHe{$G=x=N=s@zho_o|ETZ5dpEnEcu?uKEvIU8eR4{*5OAadusDnlKywi}_3FhON_!OQzNj4Vks? zJGUy|T6_2KVM2u(8T|8aW^Rp70SmC?o-7N0y1aKLar(0vC9D zj6JDeZ4C@7zjwt#6Z<|39#`rf4D$wTzvEmVA0k+r{9)XES^r-H-8GvpYBR5QBTc3< zPdnR}{m#E1bFF-@25dx{W__j(e+HqKZ;a2tk8gcZIMo^KT2@jXGl_6Ktx_p}^O$Ze zCpzkFdrQmr*r?gniBpkX&XK7nW81v#+hyIqmp$H%c|T@vs_l&omhM2br-hK23bK5E zhHpgS?$K#S)tg6IW`tvluBqjZ@4x$dro&C}S$Utl*lVQi+DhW-gUmlysK6k#-_xEh z6gc9xV$dIHaK*x=FL}c52hDUpu_Y;=I8>gf$&@^MZgC>u32Q;HKP0Ie07uJ~eo%cyPyjyNB^ToezBM(865^|ZdlID-7l+5e(HyYwl8ro0Tu zc-I=l!S$F8F4ANoVR)Os59;S~e-98IH+?Qg!x%Ob3X&pqdH)M1=pBv>fsdTj+bps7 zHNlf6TDT^}pc4^;!%rd~dtmVl7~f@3Ckpyrvk|5`9=>5QKK!(vD@ia%dI~y$klY>? zL}xP_kS5GqlNUv%wGJUJ#(D@-#Rk8Kp-YnjT_}PAC+OUv@7t}QpXCkzXKM_O+{8hw zyJkcTaljFF!T}q}iZk^0edQ%_5Tr%?>_GqIA+#j}C}`-{DAL24ZGZ<)_y2zjfC+xB zNgmkLfGZFZ>{i&BbTn!{C!RIdP#i@#?;G>~I=b?BsJ{Pw?wu7gjGajtOIc&=JJ%MK zAfas4P>I2u%ywx5^SRic%q!B8(b)=HKu`Q(e%UThpxY&8f5>I> zZ5McviOa~{@!^$~S2&)&uS94anrgdO&&%rB#7VHvUjJ)>)P%U!T7+u@(z@KDj@uzN%$(;pui zPv0}%j2par(k?Xqc+^8Y>j%ECM)k}I;_(k2*1@~%RyD>fW72B4#TkSb;~9_c6$gq* zDZEF&?Cb=HV3`I-8w{`?D#W43N|d@+FUkg_Wd7oig&>rK3nNjU^;RA?FXR>6Wnghp zfsW{6JTHSD2;d|+UmKhkBtXJ+QkzrK3pQAY1EV$5+Qd*v^n(4zw(R^nf-=CGNL!4q z{0HOPN-$mDs7))fzW_Zbf8Yq79o-r)fkmNLalllTFA-u@ec?a7EC9X1XtTRX4*VKy zFL7?7;rb6{ZYnQW=c*>Ei

bM)rq0WA&UR^p<;R6KZaVC-Q~rVQ%+{yBe*JL98* zI&KRI*@tIa*&F{!SV%}NYOL1@rZOnNSdVYc7f9{KtazfF)>7$-U5uR^pQB(} z5~$$mysavJV%++i&ACg?A^?y8y5E%`aT3K`K^R=t7IWfP?s?QZVkdgo(T|Sc`&K)& zWoZ9xL-Ds8g+kvPkpizi6&)ic^Fs6ep&L&;h>##)y-|sNuW{K&T&zusgmA}56BITr z#xq6m1tT(O=1rQ>4}DJ}AX6apq*&S;@R^BcH0QGa^Mj1X=M^^cPB2graz+1R1-cxT za;9yZ(FM%G+;1vd91c3+6tn{)5`h5{3_Lyvprp&Tk8b})d+O0#n88lT2~=33ZYdJ;Ax?$7O%Ntt&YDm z*GH+NYcKkE}9tJ?Vy>j{8Foyk%x ziM=%VTp8OYk1n50%CgSyop8v056=8DJMchJep^t8jRkK;$ET6asr>aYoEiYSAQIze zALO#lpM7hVZ^_uzc{j4;s%V4PI9gtFP6hAfzqgfw7nDuErZ8Ri?l|hSq*&G`v*}^H zIB`>>MQ*BcIdf#HBg`nS!KLAl$L|Z3p??D_8ShhU%M0p4M*$l7_*H<{XGnSd>y2wJ zFUF;VAG|8sD0*GiSmQ7nu<&DOXBkF%b8Rg=I5_xut($6A{m9{sQt7l*S5>u^0KGAf zsb%Nn$9VK?>E_1Jw!5vhsa{Z1V7>Xc`@{)blbvd7PaPybe#~o}sAT;jEc~t6;(w{A z3D$Y9U8bFtQFABnbVJjl>pA8j6Lt5Ne?V^1M+ctF^k_(GOkYl~5o}oZ|3W{gg#i!n zK>FgRCfA|YcK2tM|IYOGh8{TMKwRC$I=U{S(-Ox_yU!2kdcdwS^_sd2elkn{B4C*k z=~LF$5hXq=``>}~yFNYTF9BK?|5#yk^rJX-V<)=pa1`bd!+&quUJif{5hmoBHz*zyNz* z@axy#Gl)D^BF8jI_YN}XVO{C_{X1Z(%nAYaaUG!4gtbJxrv*O&e>zMgJ1qC>UKaot zc1mlrqpQ&HND`n;FV0OKk$YKQ+4xHneJTQxf@&!hJP9bLE;^+8xBGj7^E^uf^z1XL z%!eL!xgNT3hX6PZBxG)f8CJrys@t9P@FL@goXcpK1nUZ9RR{pv|3QUbmw&@~APDM5CD`}o zZJcA}M^4%&t2Tf3qKLWt3l!pi4InamjsJZpkrj*k_OxK*!cLnb-hlTe`@t#D3QUO& z-s{Z_HE>}Ebne65Q>j&D=`-6@rCj)EEXY0{I>2kbjmPH~-rdL~A@-U%8$6PQ(;$xL z<|}9LG1+w=nVn5xPmhU!Abr7A#=iB~SAeURKLnHIib~go_(?&kWX?39H^~!a*f7UG zicAsM6=FfF2P*Wi#&+*OHCeRMq?IKzcALs+R)BQv_&yj*0@W_zNl%Cbf(&JYMunmG zhb}nL$y(?iel~dB13a0~tq;jE^~Pk*j0*JxXoL9+p=}MasP*A#IVx)XEM`25c|`Pr z1Z$+_+*u50*S+y9<}z`>+KO{6VfH8mA5L`GQAvUA>-afz(5fF`pwA-oq_~Ve|2>i; z?5PhJZh+Z*5!&068CR<8PPHxel6Qsv=1RfETkCNIdb0f7Nb+riKF9trk7rTwbYvX!kX zc_)&r_0d0{kbyvfp##oMmR}AI_<_n(aR4Yd@QNK}?@0*{U*-Iv{AW^ofwb$kzqAja zSpeN5SC~S8u7a79Ay*tV<&~B$F*b3#t+~Lu8MIn>x^X$<3Ek(#s_rcT2QQLiYUw0= z(lO-5Ej3H!R|89pGIuUDW}a@q;U$K)HU4|x`;8*(?-XSOR#UO8*!BC{++VDv`6%8` zeFY1hs=nzVDwn?ERM^O@!lv#2)^i14_^8jxV`+Uv$2!^_!pO&=W`&`ZGmNzwpu8CB=u^b^Q$&eRoR4k{o2GbmwTpg~!|5;b(5|=0vp@_#;fo^4`#(qg&yIfNy>~ z4g1A|E?yjo){4Ea$r#UZIj*Pt2{ZARhZ|X3L4F;p!dUM9(10#ckw)5 zNBl#gzY%VmHwMtr^9A?sLbv=%b@%_=`{&IC??x#_QUxdTOkZl9WBqQcA9!)StEwu53MGuK1T5>3ztzL37R!Vb;(ri`+=x)kI}0iJjo7W z-cDXniD-1MG4_lFz7_)9ETrp}ooJsGZ}M{OU2nVhz%}E0jkD|@(TvdeuLL|3&`Jdp zJkbt-q0O{&JXHBu(M0xILdodp-%~dgvPUVjw4;6VYyGXk>g%d&WSE!2 zJVehFAl^_fK$7ih`6&33uOTC|Inb6fVS0Q#vw6VjZPqb>x1YcHGQsxqXwNlOLle*c zsy_wx&&Otb&pkUEo9ex&M~Wj(a{t7C=W+AS8H5ZYqE?4&4~u ziuBv^0cO8W8H(O^L?ffSm5}#v>&xRWr|{0gR)V95~jDa6PB0V$yOK)B8^ zye$!&!)iPbaHyH`&pAAo6)|Llz9c$akgsqtzD?o0$wqd}T-JDqzAnpu9U)2?*uG~c z(ZO^l{17k=0Oly$#FcN(9E{(1^hoG^9a(CM;IooC|F|nV33KZT27MgeK8a!3n1C05 zTPzKbBE0D#jW#=2D@+^&$RQnp@cKu$s z1!HB|&DDatei4Y7B4^fsY)1xVdzER^)@2fYJL6+ z22lp81aAuLV2i`H$M;8~aiHnG#Ml_n?jmsbg?x5_s2<5Hn18U4ZTqq25z)Y1K>61- zO{vZE!KhMkw6A{md!9H!E}07sWrwaBfYo#Vd+pni;3j-yVrl3G>@UTxlVab8L+dSk zl=)F%IPNSOk3TRqLm>W+ZkJ+NgTCQF-8}S>10?UHfhx7K5Nyy##DiDA7}Yf7Y57Tw zi32;ZjFbFxmeIfjXsR8+zJ8^6a3^P{8KHB0H7oU-@W*o%!#=&uZhRGKNT4@7po}Rz zuh|w9SowEGZ?A1Ix8kM4Ox#?|U}S!lLjQKd{}dV9F&)Y%{}I7hV33|{!VpL_n&T=7WksxVWckZND@Mku`q$K|4c~K4`Wzo1Qq-I z!PrCeh;>=qmmfxq-kpIxpTFGs-k(=>u0~~kuq>?VepF$upOe{2nU7m>O*kltNg3CV ze)XY(aA!A=Lv!%8>iue9&-!26>*Kq?eH3?d8g8@j9n9${o&ovU z;bRtbI&SfcN_hWZR$0W_={mdp7Z0#;yg>ruY^ZzRJ*#58rgyyC0(yXbq5hLg zmz^^0lRhGceP-uji5^?9*hNDEl-svLy7B9vTExNCu=?_+U@4ZM%np7eum9jP10V%5 z)p@KIY(R*??ZN}=94&EnjxyQ{g9fSL^V3BbPnH?XvX(<-aW8B+&idcaW%K$_9eK6j zHp)T(0v4s%Hhil4aN55pPP{=fs3qQ^hSX5=2f!qf4f-(+r}~K}y*T=dj3%?bko{nE z=8F=?^`lZiid=FgjL0$aiD~)bw3lC$AM2@{nna?6 z1}}^ANEO1Uaq@2?(PNVm{7jf_3kMJ#z^dJ(9N?6$Dg%;0sK?D{yLpH?uT&2| z3w}r!pNNaTN2HYs`8PJFMk}+)P^c1-J)q2SF08yF1#)22W(l%893%nXHUNF0zi7u! z?dO;~41b9s(6p?ux^Y%>kZEW>I8~~gv;tLql|y&qT+L17>T|gn+Yx6+DDsp7T?}4L zO6!z_D(dAXgO%15~x3j&lFjiI?q(l1HVyxI87_ zoJfCzP`wufj*K$OwM6(ZarYVmCbG#h7?!>qdYGU8M6~5a^nfKQUH6VimoQf>1S|>c zLo{6fRT2>5d6;$v)x0)=DG|AU`j{d850a~&i>ph&P(Wl;_Rx@26y%X|Mha;+437P-r4e2umCWzGKT zApc3;S5>^M{U`|E+l%m{sAp4GE+S0OC%)4H*xrA#Sk@jaX_C;px$OlcrXh(nXCLCY z0_ib~#3Rgb#2IvVIQ-!S1sHC&J}*@WI)kmyjhj1peSpDF$%n;IU;__o@3BT@?+e>5 zNHre3d3JBF%V(ZNpPdp#uvjc8X$;N|-Vkm+)~w+OG`^!FoEaXec1Rn`riuI4zv>dA zaC{b_pn9+V2Vxau8}Lz&g_l7T33KM?vxisu-K%}Y3~YW*FFkr*ns ze29#o)q-yvuEq$0(>*oOkV7J|V)NX8CZ5s=YrS^!bmd{~&97`Z8SEsFe_+Lb?ve$+ zh_Zv{+RO*XpPT*++$|BO^Rzs!Z?rTG(|xSu#M&=G59GDH6L1ZtOSX^&0R!+wuX!UulMnOr4E zcL_LLO2FstpXO40X1~6lsx93%_8v0D88Q`pAif0>6;7tB+CX5ssW#!>^z-_Eubjd~xxXuk62b41^i;-4Lkhmw3; zIm1halZN4n<(36e% z{Vbk(p0QworjDpbeAx8*rKV>3b4&NYr8Q+&O2MQ$!v%GU=*%v1TIIh z_U#qyd&Reybf-FDFi@J@>99UFB5?SL__J4&W-AWz|B`C|80?qwI)Bkqh_8posdN%nID}Gyp)!h5B7`8Ap)|}A=n4)sITVRqcG|c@ju(Bu<6oi0C$M>Cg zQzV=GV^4S6e@M`P9JXE?1BzlW`iglPFjFyNrh`8F(VUR`GJp-`QlTQ2vlOAXSPH!609iOFv?i7S|2zI4 zTr%Oue$#!KDlT0n(SA z^C}9%1w;hlXa2dteum5x!ZtGC8VBRQv3U@Q(c!wzhyGCx9-rU@&uGqG8i=*$e`Rwqb+2a-Jt zcE}=ygJ~<7a=5NqCECG>D`k?`3SqHiAi)a`$boHK7(I)&UjxB6p4HV0S6IG{|RUzarpjiq6TrukP2WOQ?#A*cRxNU(~f0$cLRTUM|9fBq()abZIQY zJ~zj}BKcYj9sr4D9lCt|0Af%mnsh^;OAZ)4#}!3S@oG#tLjJH&uU*L$#1?|%3eo2T zI4L9AO@@P@Bxlt$TUH77mj4)UUuHb`4^?pWX#I2_j6tPa|{w|BF*K5Iz#jJ)CPe=v@8 zVaCO{ZYE&I<4b6)u5RHS@#VmGE7%ts=!{_v2yE_+NOiKoc)gx>uj`mSe68bj)$Uf` z>lI$Dm0r6e*QX9uuKEzzgba!U@5YA&DcG$ce-t06M@C*hn~JI-b>Beq`9a^U3YRb) z2mRdY+!Dwqq{<-q9T;xHQ=qwoCL4#}ZA-D5TRJ=bP_ACtgX#5eG0|rC;Xvt|i<{Xv zke(Jo_{5E$t-kj+U2=V=9{zlr2Ad%~kQ!3Du33Mic=qbLW|ZCAxvr+up23mzPZI-6 z!^?YL|D2dQ#fk3k?@4J0!3FM%J{B;t;al@}Bjj^9IU{H9xrNjvb3b&rd_x(wQj%L{Br4{SB?AE zrf0u2uZ2yR4$TVpPm(ZGrq~jFG9!dF4&t!LbqUSPG;W`wasZP zs4@||s^Xn?E3Gf~cG!E-{6PF<%MSXqgg&F z;$S0{ZcfAdY0wKHO~wj_{GXNRRP~&oBx;-(uo)i@ILtU93@bxqQY8yqrC+0w$hn+42A# zOo^)`DyAmOR8+WIq#Qc>>g>z-hr3qFkI=};Ld`HoEZBLUj8gGi zigwE&hVk3ZiqQpuo-BsBlE2mDi-rV4G8Y$`7o7<`yVjBcsa~f23VxQP0KX=opFXln zAV(6T7=y@?DKKzRyKRXqs7=u2m$7JnpZlRwA~t)qxUL*ar)d>;dGJo>>un@c=6 zNT+K6PcSLLI3R{bokt}Aa!g*oR~0<)K{am?8-_6$tQRD&g(~xf=G~3r0$M3FTWUj) ze+!q$2BjQDzLi4-3B-owtcY@pF3m*ff-(i3;=T~OPn}HSg|6!WvH0C9&U|e$EH9ur zJN@)YT(sMUuf(5VX(|}B0B?L*H=!XLFQ9mC=keoYnVyl<{aa~H@SWeS#WG_bts|q~ z#@tWIgB*tHNntxt0(dw>{P_H9?VSh_T9p@SxJ!&ZDxfN)?FZb+3<5h0A?okDjYE|- z+lr%qe}T)^A0w~8yF^(^PP5Hf8wX@NGfRTi=(zvMe>ZbxQ{)omVVg^oCzs6B@p%kD zFN0aNLSv0$94gEXvo;LDwtfK_Egs&T6=DuXUv2)KK?3XJSMJ-6$r@ySmTKn*=z z2T8>HJVx`HW_Wf0AUeI9dq%(me9#`k?2J!$goUgQ@4FIbj~GU{fRz15uiq;A?M`=RB$k4{{}R zPSy2N3E)v-&V8f^t#;nUnk4Z2tI6@){8H(sp$!+C?L3xFejr@O z(Zq?EDe$N)0wc(4<>*fgsJdkLL?@~0kB-}tf`WqC-G+d`;f=_u!1c*5dL7@xb0Xla zkHJLMKxX5?Dn`moheE*4+B-&Ol{NDxTkHO1h(6Bn{&K$VY3Aw1xl)gFA-k&xfoj7O zTdXt%ij5ZL_AOMp2Hv_8{KDp z;P-xci54>{0de+yxbx<5hHb$lT{{AgxZ@GO>p2AEC6>I!2V$EEf86$qbBN~e=mRi` zz^7v@#LQAakq@|YEtbwdslWF!&tf|`w{m#uC9d-)?|(e#R$PT;`wZ77KkL3MCaEmK zaL>UrVyqI3#Ov!K@F{V85E-)YWpBrd+@df7rQQSPUx;)%BLiOnadJxOV`tP#xL9_b z_#`Hx5g@anj08hqxbMgGy#Q0p|dr*NIs0Nek(OUr5r9i&dytD|mryEezwu9)>Eu?I<#( zN;|icT`rRCfN_=o0v>n5G#o~%P6IJGhQVrr4l2!edr&(V*Txq#d5NLi;+8u>LNw;B znxx%L999#t$9y^oZKJXe9~roYSL8p4;n?C{d7@5w_Q1gh3Y>kbVIQ-e66vXqBhXHd0r$xuG9w$qNbGYNegCgZ@!1a+^=04$`kqyg{yR@Rbu;0e~l_v=T_;3*TgGUemY72(vT%vtl_ID98AnP0ox1)Hp)w3g^xlpMhu-gvApW8O z7Hw?QX)@&QKDQqEggEZg5#u!q+KQ3rdvT9XM)8}k*-@a;EqC%Q&>U3;4#9vT)ob3k zSabYxkv2MSTaf}WT6Qp5BH1DMH%HO*0n0sPl)Ic6AK`|o4i|)v?CX^ryM;SP>asU0 zQlMSNBYAivH%_0_>5e()TiXI|M(qT4e}IhDAmzZ`-}PQ#@(<>}yk@RwMNtRb^IRMb zwXKg$a8g~yq)$;8KS%R%_w3ifyK8Iyk09+#+QosFvy2l%Ta7L8v}^aDjtn|0KmO7b z>cG7Qo$&s;Kkesx@qg>hCm)|BUvME6sdwT%90Im;DpzF7)r$0~$SiNvbH>K2ClX2O zyPJ>6J_-A#g<n=~_b&aHxc87KLprb!qdnT9e-aOL-Lyv&+@GaxxLi7Z zip2a{Z)VagTz~xTAj7PCGgQm%TvOx4^x6E)_Y?h1|JthlJ_@WLgvH#{tSfLXzuU7b zJM&70c%Fz)mO|X1M@7P^*^TpO%+OkDZxemFr9R^e`9jm*S%CzNsACfG!*}NHYu!+2 zH*NI!RjU2RYp+)Q`={NkeRqOBJDi*T-f=rT*md7K82JdoNFPz_u)J;eqrmEa#)ivB zaNu%JZQbLdcC$m3Mlth~Tt*zjDjINY@A0%3mHS>7Z^`&?JB%9ewLKt! zKP4egT*9M4lQBGC#wvQ*A$nK%P+?U?sp;v{T9}y{q&KF46Bmk zdPz(jR1~oKrZlcY4ZP+qlgE3&a3aK*I?=@^Yivuypd)X_43QH+v8F9f6D)NDGGNQX zyFh^^=AjDqcWZ6oQTFdWl|M=xXDsU;uLkDiH(t2(jr?ie*O%K~(eD#}$ES@=11?9HN9nj|I@T7C#E^8-^X+J|cTp-%wt z8o@;aU<1zLDSo9Cwfgj+RbpVdSyENAE{`92iWiOmP|h20oe?vm-F!E&*viP%L9Bq!U=uwZqEsG?F0Sr8IOKSf6n!HtI^+xv z?SCJAaVyKMD)ri|ZaJ>S1EeSSQDQ8?3$;h?PycS-`EZ}J`7>v4VE-Pm_}FFM$+GC{ zAD({Vqu&>u(evP%sahUoi*^_!79# zlX~eOwlpnYsSzOoS{Km{9tx8Cz901s34 z?Jc%t`V<>F@G^Z8ITM=?o!!ar5CN;fi_}}nr95!US*TLck6=&RI5Jv zxk|chB9d=wgJ$xrQtd!zb_l^^NeA@rJ1Qb0bi1}_-^Xz`M9dz*&<;Y3;t4g>(uxzc zON}uAmb<_~GTJ$cpR&CBab-J~!ptJdFKk$)cmU6c%$HQuf{4t)ol?ZBB7`8*^L66P zedjSe*xM{f_EubB;36S{%s6bq_Jk=!dLVF(z}kH$;lALz)I~N0v{8Vm=+hjvg1^o# zH%J!o^2>4iKZmI}6Cc!MB6OdGX00?`2)2EKP%cYDQbz=}a{v}5`Jt`iyX<(e zOBUwem5D!uQ3U-h$BMtSzskClXDksX(*=H~f+;sSX((TqIV2$*JHPT9mcRy4k+0|P z=KJ0dRWCf?oBy_aI`CgBp6N4q5)YlZ0|u=8PxzqgmxLb-lDVO8(;&Iz2o+tw2!+w0 z?)$C)HqSV^OA2+y=#%*evbY0%78(uYbk-V(xUv71o2#=lVgeCtvLDEO2%UQHUn0h} z2NdZ2OXkX4I#Tbb!KrD_!AU77gO0YG7iinJF!8>hM}x3F?^8~EcsW^y#;*4R368kpAHirJI6y&?i2g+>fzNnG!hTKlQq&`jdpgy?zt8#lrDz@r6Uz@aa&5y~%k69p~@I*%yCG$*T3HPOLuK; zd8^U<5O!`qZ-6UmHdQm>V;Qpp{<<41{LKBgbqg&Tc-qlE^I1fB`a)BxY_im6E}3q8 z|HnS(jVolP3S7J;j=z6BRh2xohg?9=r>35eKyAqkLE7F@NYq2N%Lc?iw%uTYb3H+g z-;u)poH>?^S?nX_V7n%AEir8};E!@lRJQC>kus=vA6osdVaHV%dKdvETm`RLMt&IC z=zKiF*^h^Mm5Gz$m-}Ry>sXdKnO$PCr zREu}@U;}f4(d*%0TNhaKjnQWl60v>vI8!6x>X1(&kR;B1(`Gk!1F3Vqj$E2Feol~w zBD1Ae2-u)RGV-YbEDvro)ey4fr6Rgeb(yEbZE^N-EL(6+W&~s!ym_>$pCSdd;Ucqa zIi7l5!;)AylEO74F7|jyW>BHGkFit)4@K(5kL8Fnr!<)-$GNJdN-X0J_@l(nr~_O* zodX%bwoCAQwSj((HKRAj+?)#VG+PYQ662qPleu|E&jKC24N+vH&kzwcvcsXv;$~a% zNH7tVLn_28O!6=}Se7CcTu0tqX9o!Laoy+CP!?qRpFI8aC@4HtUvp>c$@y;VNe`W9 z-zEY%kTqw^;OZf{3Yh75aX&8fjZx4+*@`DGal8D=Tys|bj z_3W80O`KKojsD8<3!pj_@DLyjRs7ow$GHIuEPO0hTg_Gfp2h&hqCp!LQR3 z{J0K}4~5Yo`)hhm6Keco-tFw zkD1suV;erbFYYm*N{3B?4Fxp6ZxEoI?;*!L{Q8&=P)@~+ldIQg(AdXUqde0>PbgK=J;h$?B0mhm{Mmy*g z=DWX}HqmiX zPxX3v)Zz=Aj|f`HoNrFV5Vr$s6&{ar9*HGDLq*_AYkb>$6p+;Q5D48d+ zmfs4-LnZAXeJc5~qBi%xB|+BSe^Od7ba)ykQch!sKS$qU5%jzeYVU>R{?q{@z^?}! z`>RJ{+ze#&^VoKQX*qQ09}X^AA}V5{Z77V3Hpsz0U&YpAfQneO*D(na<8mPF{6&Pu z-ot|gI6>T^kv99wiG>_Q-x1L8Xn*?-I`p)JgRS$66Zfkq1 JG7Cn`{{dMGO;i8? literal 0 HcmV?d00001 diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file