Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ LLM_API_KEY=your_api_key_here
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
LLM_MODEL_NAME=qwen-plus

# ===== ZEP记忆图谱配置 =====
# 每月免费额度即可支撑简单使用:https://app.getzep.com/
# ===== Graph memory backend =====
# Default cloud mode:
GRAPH_MEMORY_BACKEND=zep_cloud

# Zep Cloud configuration. Required only when GRAPH_MEMORY_BACKEND=zep_cloud.
# Free monthly quota is sufficient for simple usage: https://app.getzep.com/
ZEP_API_KEY=your_zep_api_key_here

# Local on-premise mode. Enable with:
# GRAPH_MEMORY_BACKEND=graphiti_bridge
# GRAPHITI_BRIDGE_URL=http://graphiti-bridge:8008
# GRAPHITI_MODEL_NAME=gpt-5.4-mini
# GRAPHITI_EMBEDDING_MODEL_NAME=text-embedding-3-small

# ===== 加速 LLM 配置(可选)=====
# 注意如果不使用加速配置,env文件中就不要出现下面的配置项
LLM_BOOST_API_KEY=your_api_key_here
LLM_BOOST_BASE_URL=your_base_url_here
LLM_BOOST_MODEL_NAME=your_model_name_here
LLM_BOOST_MODEL_NAME=your_model_name_here
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ Reads `.env` from root directory by default, maps ports `3000 (frontend) / 5001

> Mirror address for faster pulling is provided as comments in `docker-compose.yml`, replace if needed.

### Optional: On-Premise Graph Memory

The default graph-memory backend remains Zep Cloud. For local graph memory, MiroFish can run a Graphiti bridge and FalkorDB through Docker Compose. See [On-Premise Graph Memory](./docs/on-prem-graph-memory.md) for architecture, configuration, health checks, and installation-agent instructions.

## 📬 Join the Conversation

<div align="center">
Expand All @@ -200,4 +204,4 @@ MiroFish's simulation engine is powered by **[OASIS (Open Agent Social Interacti
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=666ghj/MiroFish&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=666ghj/MiroFish&type=date&legend=top-left" />
</picture>
</a>
</a>
121 changes: 87 additions & 34 deletions backend/app/api/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,46 @@
logger = get_logger('mirofish.api.report')


def _get_status_request_data():
"""Read status parameters from JSON bodies or query strings."""
if request.method == 'GET':
return request.args
return request.get_json(silent=True) or {}


def _find_report_task(task_manager: TaskManager, task_id: str = None, report_id: str = None, simulation_id: str = None):
if task_id:
task = task_manager.get_task(task_id)
return task.to_dict() if task else None

for task in task_manager.list_tasks('report_generate'):
metadata = task.get('metadata') or {}
if report_id and metadata.get('report_id') == report_id:
return task
if simulation_id and metadata.get('simulation_id') == simulation_id:
return task
return None


def _report_status_payload(report, progress=None):
status = report.status.value if hasattr(report.status, 'value') else str(report.status)
payload = {
"simulation_id": report.simulation_id,
"report_id": report.report_id,
"status": status,
"progress": 100 if report.status == ReportStatus.COMPLETED else 0,
"message": t('api.reportGenerated') if report.status == ReportStatus.COMPLETED else status,
"already_completed": report.status == ReportStatus.COMPLETED
}
if progress:
payload.update(progress)
payload["simulation_id"] = report.simulation_id
payload["report_id"] = report.report_id
payload["status"] = progress.get("status", status)
payload["already_completed"] = report.status == ReportStatus.COMPLETED
return payload


# ============== 报告生成接口 ==============

@report_bp.route('/generate', methods=['POST'])
Expand Down Expand Up @@ -200,58 +240,71 @@ def progress_callback(stage, progress, message):
}), 500


@report_bp.route('/generate/status', methods=['POST'])
@report_bp.route('/generate/status', methods=['GET', 'POST'])
def get_generate_status():
"""
查询报告生成任务进度

请求(JSON):
{
"task_id": "task_xxxx", // 可选,generate返回的task_id
"simulation_id": "sim_xxxx" // 可选,模拟ID
}

返回:
{
"success": true,
"data": {
"task_id": "task_xxxx",
"status": "processing|completed|failed",
"progress": 45,
"message": "..."
}
}
支持通过 JSON body 或 query string 传入 task_id、report_id 或 simulation_id。
"""
try:
data = request.get_json() or {}
data = _get_status_request_data()

task_id = data.get('task_id')
report_id = data.get('report_id')
simulation_id = data.get('simulation_id')

# 如果提供了simulation_id,先检查是否已有完成的报告

task_manager = TaskManager()

if report_id:
report = ReportManager.get_report(report_id)
if report:
return jsonify({
"success": True,
"data": _report_status_payload(report, ReportManager.get_progress(report_id))
})

task = _find_report_task(task_manager, report_id=report_id)
if task:
return jsonify({"success": True, "data": task})

return jsonify({
"success": False,
"error": t('api.reportNotFound', id=report_id)
}), 404

if simulation_id:
existing_report = ReportManager.get_report_by_simulation(simulation_id)
if existing_report and existing_report.status == ReportStatus.COMPLETED:
if existing_report:
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"report_id": existing_report.report_id,
"status": "completed",
"progress": 100,
"message": t('api.reportGenerated'),
"already_completed": True
}
"data": _report_status_payload(
existing_report,
ReportManager.get_progress(existing_report.report_id)
)
})


task = _find_report_task(task_manager, simulation_id=simulation_id)
if task:
return jsonify({"success": True, "data": task})

return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"status": "not_started",
"progress": 0,
"message": t('api.requireTaskOrSimId')
}
})

if not task_id:
return jsonify({
"success": False,
"error": t('api.requireTaskOrSimId')
}), 400

task_manager = TaskManager()
task = task_manager.get_task(task_id)

task = _find_report_task(task_manager, task_id=task_id)

if not task:
return jsonify({
Expand All @@ -261,7 +314,7 @@ def get_generate_status():

return jsonify({
"success": True,
"data": task.to_dict()
"data": task
})

except Exception as e:
Expand Down
12 changes: 11 additions & 1 deletion backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ class Config:
LLM_API_KEY = os.environ.get('LLM_API_KEY')
LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'https://api.openai.com/v1')
LLM_MODEL_NAME = os.environ.get('LLM_MODEL_NAME', 'gpt-4o-mini')

# Graph memory backend configuration
GRAPH_MEMORY_BACKEND = os.environ.get('GRAPH_MEMORY_BACKEND', 'zep_cloud')
GRAPHITI_MODEL_NAME = os.environ.get('GRAPHITI_MODEL_NAME', LLM_MODEL_NAME)
GRAPHITI_EMBEDDING_MODEL_NAME = os.environ.get('GRAPHITI_EMBEDDING_MODEL_NAME', 'text-embedding-3-small')
GRAPHITI_BRIDGE_URL = os.environ.get('GRAPHITI_BRIDGE_URL', 'http://graphiti-bridge:8008')
FALKORDB_HOST = os.environ.get('FALKORDB_HOST', 'localhost')
FALKORDB_PORT = int(os.environ.get('FALKORDB_PORT', '6379'))
FALKORDB_DATABASE = os.environ.get('FALKORDB_DATABASE', 'mirofish')

# Zep配置
ZEP_API_KEY = os.environ.get('ZEP_API_KEY')
Expand Down Expand Up @@ -69,7 +78,8 @@ def validate(cls) -> list[str]:
errors: list[str] = []
if not cls.LLM_API_KEY:
errors.append("LLM_API_KEY 未配置")
if not cls.ZEP_API_KEY:
graph_backend = (cls.GRAPH_MEMORY_BACKEND or 'zep_cloud').lower()
if graph_backend in {'zep', 'zep_cloud', 'zep-cloud'} and not cls.ZEP_API_KEY:
errors.append("ZEP_API_KEY 未配置")
return errors

13 changes: 13 additions & 0 deletions backend/app/graph_memory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Graph memory backend adapters."""

from .base import GraphMemoryAdapter
from .factory import create_graph_memory_adapter
from .graphiti_bridge_adapter import GraphitiBridgeGraphMemoryAdapter
from .zep_cloud_adapter import ZepCloudGraphMemoryAdapter

__all__ = [
"GraphMemoryAdapter",
"GraphitiBridgeGraphMemoryAdapter",
"ZepCloudGraphMemoryAdapter",
"create_graph_memory_adapter",
]
66 changes: 66 additions & 0 deletions backend/app/graph_memory/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Graph memory adapter contracts.

This module defines the narrow graph-memory surface Mirofish needs. Concrete
backends can implement it without leaking vendor SDK details into services.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any, Protocol


class GraphMemoryAdapter(ABC):
"""Backend-neutral graph memory interface used by Mirofish services."""

@abstractmethod
def create_graph(self, graph_id: str, name: str, description: str) -> Any:
"""Create a graph and return the backend response."""

@abstractmethod
def set_ontology(self, graph_id: str, ontology: dict[str, Any]) -> Any:
"""Apply ontology definitions for a graph."""

@abstractmethod
def add_text_batch(self, graph_id: str, chunks: list[str]) -> Any:
"""Add a batch of text episodes to a graph."""

@abstractmethod
def add_text(self, graph_id: str, text: str) -> Any:
"""Add a single text episode to a graph."""

@abstractmethod
def get_episode(self, episode_uuid: str) -> Any:
"""Return one episode by UUID."""

@abstractmethod
def get_all_nodes(self, graph_id: str) -> list[Any]:
"""Return all nodes for a graph."""

@abstractmethod
def get_all_edges(self, graph_id: str) -> list[Any]:
"""Return all edges for a graph."""

@abstractmethod
def search(self, graph_id: str, query: str, limit: int = 10, scope: str = "edges", **kwargs: Any) -> Any:
"""Search graph memory."""

@abstractmethod
def get_node(self, node_uuid: str) -> Any:
"""Return one node by UUID."""

@abstractmethod
def get_node_edges(self, node_uuid: str) -> list[Any]:
"""Return edges related to one node."""

@abstractmethod
def delete_graph(self, graph_id: str) -> Any:
"""Delete a graph."""


class SupportsRawClient(Protocol):
"""Compatibility escape hatch for legacy code not yet adapter-native."""

@property
def raw_client(self) -> Any:
"""Return the underlying SDK client."""
23 changes: 23 additions & 0 deletions backend/app/graph_memory/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Graph memory adapter factory."""

from __future__ import annotations

from typing import Optional

from ..config import Config
from .base import GraphMemoryAdapter
from .zep_cloud_adapter import ZepCloudGraphMemoryAdapter


def create_graph_memory_adapter(api_key: Optional[str] = None, backend: Optional[str] = None) -> GraphMemoryAdapter:
selected_backend = (backend or Config.GRAPH_MEMORY_BACKEND).strip().lower()

if selected_backend in {"zep", "zep_cloud", "zep-cloud"}:
return ZepCloudGraphMemoryAdapter(api_key=api_key or Config.ZEP_API_KEY)

if selected_backend in {"graphiti", "graphiti_core", "graphiti-core", "graphiti_bridge", "graphiti-bridge"}:
from .graphiti_bridge_adapter import GraphitiBridgeGraphMemoryAdapter

return GraphitiBridgeGraphMemoryAdapter(api_key=api_key or Config.LLM_API_KEY)

raise ValueError(f"Unsupported GRAPH_MEMORY_BACKEND: {selected_backend}")
Loading