[feat]: Replace Zep Cloud to Graphiti
This commit is contained in:
parent
a93873ac34
commit
6bafb8b270
|
|
@ -0,0 +1,140 @@
|
|||
# Performance Tuning — Tăng tốc pipeline khi có resource lớn
|
||||
|
||||
Tài liệu này liệt kê các **param có thể nâng lên để chạy nhanh hơn** khi bạn có máy/GPU/LLM-server mạnh hơn.
|
||||
Mỗi mục ghi rõ: **file + dòng**, giá trị hiện tại, giá trị đề xuất, và **giúp nhanh ở bước nào**.
|
||||
|
||||
> ⚠️ **Nút thắt thật không nằm ở code mà ở 2 server LLM/embedding.**
|
||||
> Toàn bộ semaphore/concurrency bên dưới bị giới hạn bởi số request đồng thời mà
|
||||
> - LLM server (`LLM_BASE_URL`, vd `localhost:8027`) và
|
||||
> - embedding server (`EMBEDDING_BASE_URL`, vd `localhost:8026`)
|
||||
>
|
||||
> chịu được. Tăng các con số vượt quá khả năng 2 server này sẽ gây **timeout / 429 / 400**, KHÔNG nhanh hơn.
|
||||
> Quy tắc: nâng dần, theo dõi log, dừng khi bắt đầu thấy lỗi.
|
||||
|
||||
---
|
||||
|
||||
## ✅ ĐÃ ÁP DỤNG — cấu hình cho 2 model hiện tại (cập nhật mới nhất)
|
||||
|
||||
Server hiện tại:
|
||||
- **LLM:** `Qwen/Qwen3.6-27B` @ `localhost:8027` — `max_model_len = 262144` (256K)
|
||||
- **Embedding:** `Qwen/Qwen3-Embedding-4B` @ `localhost:8026` — `max_model_len = 40960` (40K)
|
||||
|
||||
> ⚠️ **Đã fix tên model:** `.env` trước ghi `Qwen/Qwen3.6-27B-FP8` (sai → gây 404). Server chỉ phục vụ `Qwen/Qwen3.6-27B`.
|
||||
|
||||
Các giá trị đã set (tận dụng cửa sổ context lớn 256K):
|
||||
|
||||
| Param | File:dòng | Cũ → **Mới** | Lý do |
|
||||
|---|---|---|---|
|
||||
| `LLM_MODEL_NAME` | [.env:6](.env#L6) | `…-27B-FP8` → **`…-27B`** | Fix 404 (bắt buộc) |
|
||||
| `SEMAPHORE_LIMIT` | [.env:26](.env#L26) | `10` → **`30`** | Build graph nhanh hơn |
|
||||
| `CHUNK_TOKEN_SIZE` | [.env:27](.env#L27) | `3000` → **`8000`** | Ít chunk hơn (< embed 40960) |
|
||||
| `max_tokens` ×2 | [backend/app/utils/llm_client.py:44](backend/app/utils/llm_client.py#L44), [:91](backend/app/utils/llm_client.py#L91) | `16000` → **`32000`** | Output dài hơn |
|
||||
| `MAX_TEXT_LENGTH_FOR_LLM` | [backend/app/services/ontology_generator.py:291](backend/app/services/ontology_generator.py#L291) | `40000` → **`400000`** | Ontology đọc nhiều text hơn (~100K token) |
|
||||
| `token_limit` / `message_window_size` | [backend/scripts/run_parallel_simulation.py:1128](backend/scripts/run_parallel_simulation.py#L1128) | `24000`/`25` → **`150000`/`50`** | Agent nhớ nhiều hơn, ít bị cắt |
|
||||
| `parallel_profile_count` | [backend/app/services/simulation_manager.py:352](backend/app/services/simulation_manager.py#L352) | `3` → **`15`** | Sinh profile song song |
|
||||
| `parallel_count` (default) | [backend/app/services/oasis_profile_generator.py:1198](backend/app/services/oasis_profile_generator.py#L1198) | `5` → **`15`** | (đồng bộ với trên) |
|
||||
| `semaphore` ×2 (oasis.make) | [backend/scripts/run_parallel_simulation.py:1340](backend/scripts/run_parallel_simulation.py#L1340), [:1535](backend/scripts/run_parallel_simulation.py#L1535) | `8` → **`30`** | Nhiều agent gọi LLM/round |
|
||||
| `batch_size` (build) | [backend/app/api/graph.py:459](backend/app/api/graph.py#L459) | `3` → **`8`** | Ít round-trip khi nạp chunk |
|
||||
|
||||
**Cần làm sau khi sửa:** restart backend để `.env` có hiệu lực (`cd backend && FLASK_PORT=5002 uv run python run.py`).
|
||||
|
||||
---
|
||||
|
||||
## 🎯 TL;DR — Sửa 3 thứ này trước (ăn nhất)
|
||||
|
||||
| Ưu tiên | Param | File:dòng | Hiện tại | Đề xuất (máy mạnh) | Tăng tốc bước |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | `SEMAPHORE_LIMIT` | [.env:26](.env#L26) | `10` | `30` | **Build graph** (chậm nhất) |
|
||||
| 2 | `parallel_profile_count` | [backend/app/services/simulation_manager.py:352](backend/app/services/simulation_manager.py#L352) | `3` | `15` | **Sinh agent profile** |
|
||||
| 3 | `semaphore` (oasis.make) | [backend/scripts/run_parallel_simulation.py:1340](backend/scripts/run_parallel_simulation.py#L1340), [:1535](backend/scripts/run_parallel_simulation.py#L1535) | `8` | `30` | **Simulation** (nhiều agent gọi LLM) |
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Nhóm 1 — Build knowledge graph (bước chậm nhất)
|
||||
|
||||
### 1.1 `SEMAPHORE_LIMIT` — ⭐ quan trọng nhất
|
||||
- **File:** [.env:26](.env#L26) → `SEMAPHORE_LIMIT=10`
|
||||
- **Là gì:** số thao tác LLM/embedding **đồng thời** mà `graphiti_core` chạy khi build graph.
|
||||
(Đọc bởi thư viện tại `graphiti_core/helpers.py`, default lib = 20.)
|
||||
- **Đề xuất:** `20`–`50` (giới hạn bởi sức chịu của LLM + embedding server).
|
||||
- **Giúp gì:** Build graph là bước tốn thời gian nhất (extract entity + embedding cho từng chunk). Đây là đòn bẩy lớn nhất.
|
||||
|
||||
### 1.2 `USE_PARALLEL_RUNTIME`
|
||||
- **File:** [.env:28](.env#L28) → `USE_PARALLEL_RUNTIME=false`
|
||||
- **Là gì:** bật Neo4j parallel runtime cho các Cypher query.
|
||||
- **Đề xuất:** `true` — **CHỈ khi dùng Neo4j Enterprise** (Community Edition không hỗ trợ, bật vô tác dụng).
|
||||
- **Giúp gì:** truy vấn graph nhanh hơn khi Neo4j có nhiều core.
|
||||
|
||||
### 1.3 `CHUNK_TOKEN_SIZE`
|
||||
- **File:** [.env:27](.env#L27) → `CHUNK_TOKEN_SIZE=3000`
|
||||
- **Là gì:** kích thước mỗi chunk (token) khi graphiti tự chia text.
|
||||
- **Đề xuất:** `5000`–`8000` — **CHỈ khi LLM/embedding có cửa sổ context lớn** (model hiện tại 32K nên cẩn thận).
|
||||
- **Giúp gì:** chunk to hơn → ít chunk hơn → ít vòng LLM call hơn khi build. (Đánh đổi: mỗi call nặng hơn.)
|
||||
|
||||
### 1.4 `batch_size` khi build (hardcode — phải sửa code)
|
||||
- **File:** [backend/app/api/graph.py:459](backend/app/api/graph.py#L459) → `batch_size=3`
|
||||
(default cũng ở [backend/app/services/graph_builder.py:67](backend/app/services/graph_builder.py#L67) và [:294](backend/app/services/graph_builder.py#L294))
|
||||
- **Là gì:** số chunk gửi đi mỗi đợt. Endpoint `build` KHÔNG nhận param này từ request → phải sửa trực tiếp.
|
||||
- **Đề xuất:** `5`–`10`.
|
||||
- **Giúp gì:** giảm số lần round-trip khi nạp chunk vào graph.
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Nhóm 2 — Simulation (bước chậm thứ 2)
|
||||
|
||||
### 2.1 `parallel_profile_count` — sinh agent profile song song
|
||||
- **File:** [backend/app/services/simulation_manager.py:352](backend/app/services/simulation_manager.py#L352) → `parallel_profile_count: int = 3`
|
||||
(worker thực thi ở [backend/app/services/oasis_profile_generator.py:1198](backend/app/services/oasis_profile_generator.py#L1198) → `parallel_count: int = 5`)
|
||||
- **Là gì:** số thread sinh profile đồng thời, mỗi profile = 1 LLM call.
|
||||
- **Đề xuất:** `10`–`20`.
|
||||
- **Giúp gì:** với 50 profile: 3 thread ≈ 100–250s; 15 thread nhanh ~5×.
|
||||
|
||||
### 2.2 `semaphore` trong `oasis.make()` — LLM call đồng thời mỗi round
|
||||
- **File:**
|
||||
- [backend/scripts/run_parallel_simulation.py:1340](backend/scripts/run_parallel_simulation.py#L1340) (Twitter) → `semaphore=8`
|
||||
- [backend/scripts/run_parallel_simulation.py:1535](backend/scripts/run_parallel_simulation.py#L1535) (Reddit) → `semaphore=8`
|
||||
- [backend/scripts/run_twitter_simulation.py:597](backend/scripts/run_twitter_simulation.py#L597) → `semaphore=30`
|
||||
- [backend/scripts/run_reddit_simulation.py:582](backend/scripts/run_reddit_simulation.py#L582) → `semaphore=30`
|
||||
- **Là gì:** giới hạn số agent gọi LLM cùng lúc trong 1 round.
|
||||
- **Đề xuất:** `20`–`40` (đã hạ xuống 8 cho server nhỏ; server mạnh thì nới ra).
|
||||
- **Giúp gì:** mỗi round simulation chạy nhanh hơn khi nhiều agent xử lý song song.
|
||||
|
||||
---
|
||||
|
||||
## 🔵 Nhóm 3 — Token / context (chỉ nâng SAU KHI đổi sang model cửa sổ lớn)
|
||||
|
||||
> ⚠️ Mấy param này đã được **hạ xuống** để vừa model 32K hiện tại (`max_total_tokens=32768`).
|
||||
> Chúng **không tăng tốc trực tiếp**, mà tăng *throughput mỗi call* → ít vòng lặp/ít bị cắt context.
|
||||
> **Nâng lên sẽ gây lỗi HTTP 400 nếu model vẫn là 32K.** Chỉ nâng khi đã trỏ sang model/endpoint cửa sổ lớn (vd 128K).
|
||||
|
||||
| Param | File:dòng | Hiện tại | Khi model lớn | Vai trò |
|
||||
|---|---|---|---|---|
|
||||
| `max_tokens` (chat) | [backend/app/utils/llm_client.py:44](backend/app/utils/llm_client.py#L44) | `16000` | `32000`–`50000` | output tối đa mỗi call |
|
||||
| `max_tokens` (chat_json) | [backend/app/utils/llm_client.py:91](backend/app/utils/llm_client.py#L91) | `16000` | `32000`–`50000` | output JSON (ontology) |
|
||||
| `MAX_TEXT_LENGTH_FOR_LLM` | [backend/app/services/ontology_generator.py:291](backend/app/services/ontology_generator.py#L291) | `40000` | `100000`+ | số ký tự input cho ontology (hiện cắt bớt) |
|
||||
| `token_limit` (agent memory) | [backend/scripts/run_parallel_simulation.py:1128](backend/scripts/run_parallel_simulation.py#L1128) | `24000` | `100000`+ | context history mỗi agent giữ lại |
|
||||
| `message_window_size` | [backend/scripts/run_parallel_simulation.py:1128](backend/scripts/run_parallel_simulation.py#L1128) | `25` | `50`+ | số message agent nhớ |
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Nhóm 4 — Throughput-vs-độ-sâu (đánh đổi chất lượng/tốc độ, không phải resource)
|
||||
|
||||
Mấy cái này không cần máy mạnh — chúng đổi **độ sâu xử lý** lấy **tốc độ**. Giảm xuống = nhanh hơn nhưng kết quả nông hơn.
|
||||
|
||||
| Param | File:dòng | Hiện tại | Để chạy nhanh |
|
||||
|---|---|---|---|
|
||||
| `OASIS_DEFAULT_MAX_ROUNDS` | [backend/app/config.py:55](backend/app/config.py#L55) (env `OASIS_DEFAULT_MAX_ROUNDS`) | `10` | giảm còn `3`–`5` khi test nhanh |
|
||||
| `REPORT_AGENT_MAX_TOOL_CALLS` | [backend/app/config.py:69](backend/app/config.py#L69) | `5` | giảm → report nhanh hơn, ít truy vấn graph hơn |
|
||||
| `REPORT_AGENT_MAX_REFLECTION_ROUNDS` | [backend/app/config.py:70](backend/app/config.py#L70) | `2` | giảm còn `1` → report nhanh hơn |
|
||||
|
||||
---
|
||||
|
||||
## 📌 Lưu ý vận hành
|
||||
|
||||
1. **Sửa `.env` → phải restart backend** (biến môi trường chỉ đọc lúc khởi động; Flask reloader KHÔNG reload `.env`).
|
||||
```bash
|
||||
cd backend && FLASK_PORT=5002 uv run python run.py
|
||||
```
|
||||
2. **Sửa code `.py` → Flask reloader tự nạp lại** (nếu debug mode bật), không cần restart.
|
||||
3. **Nâng dần, đừng nhảy vọt.** Tăng semaphore → xem log có `429`/`timeout`/`400` không → nếu có thì hạ lại.
|
||||
4. **Trần trên = sức chịu của LLM + embedding server**, không phải code. Đó mới là nút thắt thật.
|
||||
|
|
@ -298,8 +298,8 @@ def build_graph():
|
|||
|
||||
# Kiểm tra cấu hình
|
||||
errors = []
|
||||
if not Config.ZEP_API_KEY:
|
||||
errors.append("ZEP_API_KEY not configured")
|
||||
if not Config.NEO4J_URI or not Config.NEO4J_PASSWORD:
|
||||
errors.append("NEO4J_URI / NEO4J_PASSWORD not configured")
|
||||
if errors:
|
||||
logger.error(f"Configuration error: {errors}")
|
||||
return jsonify({
|
||||
|
|
@ -396,9 +396,9 @@ def build_graph():
|
|||
message="Initializing graph builder service..."
|
||||
)
|
||||
|
||||
# INITIALIZE GRAPH BUILDER
|
||||
# Creates service instance with Zep Cloud API client
|
||||
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
|
||||
# INITIALIZE GRAPH BUILDER
|
||||
# Graphiti (Neo4j) backend — cấu hình lấy lazy qua get_graphiti()
|
||||
builder = GraphBuilderService()
|
||||
|
||||
# Chunk text
|
||||
task_manager.update_task(
|
||||
|
|
@ -454,9 +454,9 @@ def build_graph():
|
|||
|
||||
# [4] - Sends text chunks as episodes to Zep for entity extraction
|
||||
episode_uuids = builder.add_text_batches(
|
||||
graph_id,
|
||||
graph_id,
|
||||
chunks,
|
||||
batch_size=3,
|
||||
batch_size=8,
|
||||
progress_callback=add_progress_callback
|
||||
)
|
||||
|
||||
|
|
@ -591,13 +591,13 @@ def get_graph_data(graph_id: str):
|
|||
Lấy dữ liệu graph (nodes và edges)
|
||||
"""
|
||||
try:
|
||||
if not Config.ZEP_API_KEY:
|
||||
if not Config.NEO4J_URI or not Config.NEO4J_PASSWORD:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "ZEP_API_KEY not configured"
|
||||
"error": "NEO4J_URI / NEO4J_PASSWORD not configured"
|
||||
}), 500
|
||||
|
||||
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
|
||||
|
||||
builder = GraphBuilderService()
|
||||
graph_data = builder.get_graph_data(graph_id)
|
||||
|
||||
return jsonify({
|
||||
|
|
@ -616,16 +616,16 @@ def get_graph_data(graph_id: str):
|
|||
@graph_bp.route('/delete/<graph_id>', methods=['DELETE'])
|
||||
def delete_graph(graph_id: str):
|
||||
"""
|
||||
Xóa Zep graph
|
||||
Xóa graph (Graphiti + Neo4j)
|
||||
"""
|
||||
try:
|
||||
if not Config.ZEP_API_KEY:
|
||||
if not Config.NEO4J_URI or not Config.NEO4J_PASSWORD:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "ZEP_API_KEY not configured"
|
||||
"error": "NEO4J_URI / NEO4J_PASSWORD not configured"
|
||||
}), 500
|
||||
|
||||
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
|
||||
|
||||
builder = GraphBuilderService()
|
||||
builder.delete_graph(graph_id)
|
||||
|
||||
return jsonify({
|
||||
|
|
|
|||
|
|
@ -58,10 +58,10 @@ def get_graph_entities(graph_id: str):
|
|||
enrich: Có lấy thông tin cạnh liên quan hay không (mặc định true)
|
||||
"""
|
||||
try:
|
||||
if not Config.ZEP_API_KEY:
|
||||
if not Config.NEO4J_URI or not Config.NEO4J_PASSWORD:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "ZEP_API_KEY is not configured"
|
||||
"error": "NEO4J_URI / NEO4J_PASSWORD is not configured"
|
||||
}), 500
|
||||
|
||||
entity_types_str = request.args.get('entity_types', '')
|
||||
|
|
@ -95,10 +95,10 @@ def get_graph_entities(graph_id: str):
|
|||
def get_entity_detail(graph_id: str, entity_uuid: str):
|
||||
"""Lấy thông tin chi tiết của một thực thể."""
|
||||
try:
|
||||
if not Config.ZEP_API_KEY:
|
||||
if not Config.NEO4J_URI or not Config.NEO4J_PASSWORD:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "ZEP_API_KEY is not configured"
|
||||
"error": "NEO4J_URI / NEO4J_PASSWORD is not configured"
|
||||
}), 500
|
||||
|
||||
reader = ZepEntityReader()
|
||||
|
|
@ -128,10 +128,10 @@ def get_entity_detail(graph_id: str, entity_uuid: str):
|
|||
def get_entities_by_type(graph_id: str, entity_type: str):
|
||||
"""Lấy toàn bộ thực thể theo loại chỉ định."""
|
||||
try:
|
||||
if not Config.ZEP_API_KEY:
|
||||
if not Config.NEO4J_URI or not Config.NEO4J_PASSWORD:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "ZEP_API_KEY is not configured"
|
||||
"error": "NEO4J_URI / NEO4J_PASSWORD is not configured"
|
||||
}), 500
|
||||
|
||||
enrich = request.args.get('enrich', 'true').lower() == 'true'
|
||||
|
|
@ -309,7 +309,7 @@ def _check_simulation_prepared(simulation_id: str) -> tuple:
|
|||
# - completed: đã chạy xong, nghĩa là đã chuẩn bị xong từ trước
|
||||
# - stopped: đã dừng, nghĩa là đã chuẩn bị xong từ trước
|
||||
# - failed: chạy thất bại (nhưng phần chuẩn bị đã hoàn tất)
|
||||
prepared_statuses = ["ready", "preparing", "running", "completed", "stopped", "failed", "paused"]
|
||||
prepared_statuses = ["ready", "preparing", "running", "completed", "stopped", "failed"]
|
||||
|
||||
if status in prepared_statuses and config_generated:
|
||||
# Lấy thông tin thống kê file
|
||||
|
|
@ -383,7 +383,7 @@ def prepare_simulation():
|
|||
"simulation_id": "sim_xxxx", // bắt buộc, simulation ID
|
||||
"entity_types": ["Student", "PublicFigure"], // tùy chọn, chỉ định loại thực thể
|
||||
"use_llm_for_profiles": true, // tùy chọn, có dùng LLM để sinh persona hay không
|
||||
"parallel_profile_count": 5, // tùy chọn, số lượng sinh persona song song, mặc định 5
|
||||
"parallel_profile_count": 15, // tùy chọn, số lượng sinh persona song song, mặc định 15
|
||||
"force_regenerate": false // tùy chọn, buộc sinh lại, mặc định false
|
||||
}
|
||||
|
||||
|
|
@ -468,7 +468,7 @@ def prepare_simulation():
|
|||
|
||||
entity_types_list = data.get('entity_types')
|
||||
use_llm_for_profiles = data.get('use_llm_for_profiles', True)
|
||||
parallel_profile_count = data.get('parallel_profile_count', 20)
|
||||
parallel_profile_count = data.get('parallel_profile_count', 15)
|
||||
|
||||
# ========== Đồng bộ lấy số lượng thực thể (trước khi chạy tác vụ nền) ==========
|
||||
# Nhờ đó frontend có thể lấy ngay tổng số Agent dự kiến sau khi gọi prepare
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ class Project:
|
|||
simulation_requirement: Optional[str] = None
|
||||
chunk_size: int = 500
|
||||
chunk_overlap: int = 50
|
||||
llm_model_name: Optional[str] = None
|
||||
|
||||
# Thông tin lỗi
|
||||
error: Optional[str] = None
|
||||
|
|
@ -64,7 +63,6 @@ class Project:
|
|||
"updated_at": self.updated_at,
|
||||
"files": self.files,
|
||||
"total_text_length": self.total_text_length,
|
||||
"llm_model_name": self.llm_model_name,
|
||||
"ontology": self.ontology,
|
||||
"analysis_summary": self.analysis_summary,
|
||||
"graph_id": self.graph_id,
|
||||
|
|
@ -90,7 +88,6 @@ class Project:
|
|||
updated_at=data.get('updated_at', ''),
|
||||
files=data.get('files', []),
|
||||
total_text_length=data.get('total_text_length', 0),
|
||||
llm_model_name=data.get('llm_model_name'),
|
||||
ontology=data.get('ontology'),
|
||||
analysis_summary=data.get('analysis_summary'),
|
||||
graph_id=data.get('graph_id'),
|
||||
|
|
@ -154,8 +151,7 @@ class ProjectManager:
|
|||
name=name,
|
||||
status=ProjectStatus.CREATED,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
llm_model_name=Config.LLM_MODEL_NAME
|
||||
updated_at=now
|
||||
)
|
||||
|
||||
# Thiết lập các thư mục con trong không gian thư mục của project
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
"""
|
||||
Dịch vụ xây dựng Đồ thị Tri thức (Knowledge Graph)
|
||||
API 2: Sử dụng Zep API để xây dựng một Standalone Graph (Đồ thị độc lập)
|
||||
API 2: Dùng graphiti_core (Neo4j backend) để xây dựng một Standalone Graph (Đồ thị độc lập)
|
||||
"""
|
||||
|
||||
import os
|
||||
import re # changed
|
||||
import uuid
|
||||
import time
|
||||
import uuid
|
||||
import threading
|
||||
from typing import Dict, Any, List, Optional, Callable
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any, List, Optional, Callable, Type
|
||||
from dataclasses import dataclass
|
||||
|
||||
from zep_cloud.client import Zep
|
||||
from zep_cloud import EpisodeData, EntityEdgeSourceTarget
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from graphiti_core.nodes import EntityNode, EpisodeType
|
||||
|
||||
from ..config import Config
|
||||
from ..models.task import TaskManager, TaskStatus
|
||||
from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges
|
||||
from ..utils.graphiti_client import get_graphiti, run_async
|
||||
from .text_processor import TextProcessor
|
||||
|
||||
|
||||
|
|
@ -40,16 +42,20 @@ class GraphInfo:
|
|||
class GraphBuilderService:
|
||||
"""
|
||||
Dịch vụ tạo lập Graph
|
||||
Đảm nhiệm logic gọi request lên Zep API để thiết lập Graph
|
||||
Đảm nhiệm logic dùng graphiti_core (Neo4j) để thiết lập Graph
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
self.api_key = api_key or Config.ZEP_API_KEY
|
||||
if not self.api_key:
|
||||
raise ValueError("ZEP_API_KEY has not been configured.")
|
||||
|
||||
self.client = Zep(api_key=self.api_key)
|
||||
# api_key giữ lại trong chữ ký để tương thích caller cũ, nhưng không còn dùng
|
||||
# (Graphiti dùng Neo4j + LLM config, lấy lazy qua get_graphiti()).
|
||||
self.api_key = api_key
|
||||
self.task_manager = TaskManager()
|
||||
|
||||
# Ontology được build sẵn (Pydantic thuần) và truyền vào mỗi add_episode,
|
||||
# vì Graphiti không có set_ontology server-side như Zep.
|
||||
self._entity_types: Dict[str, Type[BaseModel]] = {}
|
||||
self._edge_types: Dict[str, Type[BaseModel]] = {}
|
||||
self._edge_type_map: Dict[tuple, List[str]] = {}
|
||||
|
||||
def build_graph_async(
|
||||
self,
|
||||
|
|
@ -191,115 +197,95 @@ class GraphBuilderService:
|
|||
self.task_manager.fail_task(task_id, error_msg)
|
||||
|
||||
def create_graph(self, name: str) -> str:
|
||||
"""Graph Creation: Initializes a new graph in Zep Cloud with a unique ID"""
|
||||
"""Sinh graph_id mới (dùng làm group_id của Graphiti).
|
||||
|
||||
# Generate unique graph_id
|
||||
Graphiti không cần đăng ký partition trước — group_id là string tự do,
|
||||
sẽ được tạo ngầm khi add_episode đầu tiên ghi vào. Vì vậy method này
|
||||
chỉ sinh & trả về id, không gọi API nào.
|
||||
"""
|
||||
graph_id = f"mirofish_{uuid.uuid4().hex[:16]}"
|
||||
|
||||
# Call Zep API create()
|
||||
self.client.graph.create(
|
||||
graph_id=graph_id,
|
||||
name=name,
|
||||
description="MiroFish Social Simulation Graph"
|
||||
)
|
||||
|
||||
return graph_id
|
||||
|
||||
def set_ontology(self, graph_id: str, ontology: Dict[str, Any]):
|
||||
"""
|
||||
Cấu hình dữ liệu Ontology (Bản thể học) cho Graph trên server Zep (Public access)
|
||||
Ontology Setup: Defines the schema for entities and relationships using dynamic class creation.
|
||||
Build schema Ontology (entity/edge types) dưới dạng Pydantic model thuần.
|
||||
|
||||
Graphiti không có API set_ontology server-side như Zep. Thay vào đó,
|
||||
entity_types/edge_types/edge_type_map được truyền vào MỖI lần add_episode.
|
||||
Method này build sẵn và lưu trên self để add_text_batches dùng lại.
|
||||
"""
|
||||
import warnings
|
||||
from typing import Optional
|
||||
from pydantic import Field
|
||||
from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel
|
||||
|
||||
# Ẩn bỏ đi các Warning (Cảnh báo) của thư viện Pydantic v2 liên quan đến Field(default=None)
|
||||
# Vì đây là format bắt buộc phải có từ Zep SDK, các cảnh báo này phát sinh do tự động khởi tạo lớp ảo, hoàn toàn có thể bỏ qua được.
|
||||
warnings.filterwarnings('ignore', category=UserWarning, module='pydantic')
|
||||
|
||||
# Danh sách các tên định danh (variable/name) trùng với từ khoá bảo lưu của Zep, không được dùng làm tên thuộc tính
|
||||
RESERVED_NAMES = {'uuid', 'name', 'group_id', 'name_embedding', 'summary', 'created_at'}
|
||||
|
||||
|
||||
# Các tên field trùng với EntityNode.model_fields của graphiti — không được dùng
|
||||
# làm attribute (graphiti raise EntityTypeValidationError nếu trùng).
|
||||
RESERVED_NAMES = {
|
||||
'uuid', 'name', 'group_id', 'labels',
|
||||
'name_embedding', 'summary', 'attributes', 'created_at',
|
||||
}
|
||||
|
||||
def safe_attr_name(attr_name: str) -> str:
|
||||
"""Hàm thay đổi các tên thuộc tính bị trùng với keyword của hệ thống để an toàn hơn"""
|
||||
"""Đổi tên attribute nếu trùng field bảo lưu của graphiti."""
|
||||
if attr_name.lower() in RESERVED_NAMES:
|
||||
return f"entity_{attr_name}"
|
||||
return attr_name
|
||||
|
||||
# Khởi tạo động (Dynamic Class Creation) các Model Loại Thực thể từ JSON đầu vào
|
||||
entity_types = {}
|
||||
|
||||
# Processes each entity type from ontology definition
|
||||
# --- Entity types (Pydantic thuần, key = tên PascalCase từ ontology) ---
|
||||
entity_types: Dict[str, Type[BaseModel]] = {}
|
||||
for entity_def in ontology.get("entity_types", []):
|
||||
name = entity_def["name"]
|
||||
description = entity_def.get("description", f"A {name} entity.")
|
||||
|
||||
# Chuẩn bị file từ điển cho Attribute và kiểu chú thích (Theo chuẩn Pydantic v2)
|
||||
attrs = {"__doc__": description}
|
||||
annotations = {}
|
||||
|
||||
|
||||
attrs: Dict[str, Any] = {"__doc__": description}
|
||||
annotations: Dict[str, Any] = {}
|
||||
|
||||
for attr_def in entity_def.get("attributes", []):
|
||||
attr_name = safe_attr_name(attr_def["name"]) # Áp dụng hàm chống bị trùng từ khoá
|
||||
attr_name = safe_attr_name(attr_def["name"])
|
||||
attr_desc = attr_def.get("description", attr_name)
|
||||
# Zep API bắt buộc phải nhận vào field description
|
||||
attrs[attr_name] = Field(description=attr_desc, default=None)
|
||||
annotations[attr_name] = Optional[EntityText] # Chú thích kiểu dữ liệu
|
||||
|
||||
attrs[attr_name] = Field(default=None, description=attr_desc)
|
||||
annotations[attr_name] = Optional[str]
|
||||
|
||||
attrs["__annotations__"] = annotations
|
||||
|
||||
# Dynamic class generation - Create Pydnatic model class for entity type at runtime
|
||||
entity_class = type(name, (EntityModel,), attrs)
|
||||
entity_class = type(name, (BaseModel,), attrs)
|
||||
entity_class.__doc__ = description
|
||||
entity_types[name] = entity_class # Store in entity_types dict
|
||||
|
||||
# Tương tự, dựa vào JSON để khởi tạo động khai báo các Model Loại Quan Hệ
|
||||
edge_definitions = {}
|
||||
entity_types[name] = entity_class
|
||||
|
||||
# --- Edge types (key = tên UPPER_SNAKE_CASE) + edge_type_map ---
|
||||
edge_types: Dict[str, Type[BaseModel]] = {}
|
||||
edge_type_map: Dict[tuple, List[str]] = {}
|
||||
|
||||
for edge_def in ontology.get("edge_types", []):
|
||||
name = edge_def["name"]
|
||||
description = edge_def.get("description", f"A {name} relationship.")
|
||||
|
||||
# Dọn các attribute dictionary và typing tương tự
|
||||
|
||||
attrs = {"__doc__": description}
|
||||
annotations = {}
|
||||
|
||||
for attr_def in edge_def.get("attributes", []):
|
||||
attr_name = safe_attr_name(attr_def["name"]) # Filter an toàn
|
||||
attr_name = safe_attr_name(attr_def["name"])
|
||||
attr_desc = attr_def.get("description", attr_name)
|
||||
# Đảm bảo giữ format Zep API
|
||||
attrs[attr_name] = Field(description=attr_desc, default=None)
|
||||
annotations[attr_name] = Optional[str] # Định dạng Data cho thuộc tình của loại Quan Hệ là chuỗi String
|
||||
|
||||
attrs[attr_name] = Field(default=None, description=attr_desc)
|
||||
annotations[attr_name] = Optional[str]
|
||||
|
||||
attrs["__annotations__"] = annotations
|
||||
|
||||
# Khởi tạo Class động với Tên chuẩn format (PascalCase)
|
||||
# Class name PascalCase chỉ để debug; KEY của dict mới là cái graphiti dùng.
|
||||
class_name = ''.join(word.capitalize() for word in name.split('_'))
|
||||
# Creates Pydantic model class for relationship type
|
||||
edge_class = type(class_name, (EdgeModel,), attrs)
|
||||
edge_class = type(class_name, (BaseModel,), attrs)
|
||||
edge_class.__doc__ = description
|
||||
|
||||
# Mapping thông số luồng thực thể gắn kết với Quan Hệ (Source/Targets config)
|
||||
source_targets = []
|
||||
for st in edge_def.get("source_targets", []):
|
||||
source_targets.append(
|
||||
EntityEdgeSourceTarget(
|
||||
source=st.get("source", "Entity"),
|
||||
target=st.get("target", "Entity")
|
||||
)
|
||||
)
|
||||
|
||||
edge_types[name] = edge_class
|
||||
|
||||
# Build edge_type_map: (source_label, target_label) -> [edge_name, ...]
|
||||
source_targets = edge_def.get("source_targets", [])
|
||||
if source_targets:
|
||||
edge_definitions[name] = (edge_class, source_targets) # Store in edge_definitions dict
|
||||
|
||||
# Action Gọi lệnh thay đổi Ontology cho môi trường GraphID của Zep
|
||||
if entity_types or edge_definitions:
|
||||
# Call Zep set_ontology()
|
||||
self.client.graph.set_ontology(
|
||||
graph_ids=[graph_id],
|
||||
entities=entity_types if entity_types else None,
|
||||
edges=edge_definitions if edge_definitions else None,
|
||||
)
|
||||
for st in source_targets:
|
||||
key = (st.get("source", "Entity"), st.get("target", "Entity"))
|
||||
edge_type_map.setdefault(key, []).append(name)
|
||||
else:
|
||||
# Không khai báo source/target cụ thể → catch-all (Entity, Entity)
|
||||
edge_type_map.setdefault(("Entity", "Entity"), []).append(name)
|
||||
|
||||
# Lưu lại để truyền vào add_episode (không gọi API nào ở đây).
|
||||
self._entity_types = entity_types
|
||||
self._edge_types = edge_types
|
||||
self._edge_type_map = edge_type_map
|
||||
|
||||
def add_text_batches(
|
||||
self,
|
||||
|
|
@ -308,71 +294,70 @@ class GraphBuilderService:
|
|||
batch_size: int = 3,
|
||||
progress_callback: Optional[Callable] = None
|
||||
) -> List[str]:
|
||||
"""Tải các đoạn văn bản (text chunks) lên Graph theo từng gói nhỏ (batch) và trả về id (Episode UUID) của mọi phân đoạn dữ liệu gửi đi."""
|
||||
episode_uuids = []
|
||||
"""Nạp từng chunk văn bản vào graph qua graphiti.add_episode (tuần tự).
|
||||
|
||||
Graphiti add_episode xử lý đồng bộ (await xong = đã extract & ghi Neo4j) và
|
||||
khuyến nghị chạy tuần tự để giữ ngữ cảnh temporal giữa các episode liên tiếp.
|
||||
Trả về list episode uuid.
|
||||
|
||||
`batch_size` không còn nghĩa "nhiều episode/1 request" (mỗi chunk = 1 episode);
|
||||
giữ param cho tương thích caller, chỉ dùng để định nhịp progress.
|
||||
"""
|
||||
graphiti = get_graphiti()
|
||||
episode_uuids: List[str] = []
|
||||
total_chunks = len(chunks)
|
||||
|
||||
for i in range(0, total_chunks, batch_size):
|
||||
batch_chunks = chunks[i:i + batch_size]
|
||||
batch_num = i // batch_size + 1
|
||||
total_batches = (total_chunks + batch_size - 1) // batch_size
|
||||
|
||||
if total_chunks == 0:
|
||||
return episode_uuids
|
||||
|
||||
# entity/edge types: truyền None nếu rỗng để graphiti dùng default.
|
||||
entity_types = self._entity_types or None
|
||||
edge_types = self._edge_types or None
|
||||
edge_type_map = self._edge_type_map or None
|
||||
|
||||
for i, chunk in enumerate(chunks):
|
||||
if progress_callback:
|
||||
progress = (i + len(batch_chunks)) / total_chunks
|
||||
progress_callback(
|
||||
f"Sending data batch {batch_num}/{total_batches} ({len(batch_chunks)} chunks)...",
|
||||
progress
|
||||
f"Adding chunk {i + 1}/{total_chunks} to graph...",
|
||||
(i + 1) / total_chunks,
|
||||
)
|
||||
|
||||
# Create EpisodeData objects
|
||||
episodes = [
|
||||
EpisodeData(data=chunk, type="text")
|
||||
for chunk in batch_chunks
|
||||
]
|
||||
|
||||
# Khởi chạy gửi cho Zep Server
|
||||
max_retries = 10 # changed
|
||||
for attempt in range(max_retries): # changed
|
||||
|
||||
# Lớp retry mỏng phòng LLM backend rate-limit / lỗi tạm thời.
|
||||
max_retries = 3
|
||||
delay = 2.0
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# UPLOAD BATCH TO ZEP - Sends batch of episodes for entity extraction
|
||||
batch_result = self.client.graph.add_batch(
|
||||
graph_id=graph_id,
|
||||
episodes=episodes
|
||||
)
|
||||
|
||||
# Cập nhật và thu thập lại UUID của các Episode được trả về sau khi tạo mới
|
||||
if batch_result and isinstance(batch_result, list):
|
||||
for ep in batch_result:
|
||||
ep_uuid = getattr(ep, 'uuid_', None) or getattr(ep, 'uuid', None)
|
||||
if ep_uuid:
|
||||
episode_uuids.append(ep_uuid)
|
||||
|
||||
# Cài thời gian chờ (delay) nhỏ để tránh rate-limit bị quá tải số lượng requests
|
||||
time.sleep(3)
|
||||
break # changed: thoát retry loop nếu thành công
|
||||
|
||||
except Exception as e: # changed
|
||||
err_str = str(e)
|
||||
if "episode usage limit" in err_str or ("status_code: 429" in err_str): # changed: bắt lỗi rate-limit
|
||||
# Đọc thời điểm reset từ error message để biết cần chờ bao lâu
|
||||
reset_match = re.search(r"x-ratelimit-reset['\"]:\s*['\"]?(\d+)", err_str) # changed
|
||||
if reset_match: # changed
|
||||
wait_seconds = max(int(reset_match.group(1)) - int(time.time()) + 2, 5) # changed
|
||||
else: # changed
|
||||
wait_seconds = 20 # changed: fallback 12s (5 calls/phút → cách nhau 12s)
|
||||
if progress_callback: # changed
|
||||
progress_callback( # changed
|
||||
f"Rate limited by Zep. Waiting {wait_seconds}s then retry (attempt {attempt + 1}/{max_retries})...", # changed
|
||||
(i + len(batch_chunks)) / total_chunks # changed
|
||||
) # changed
|
||||
time.sleep(wait_seconds) # changed
|
||||
else: # changed: lỗi khác thì raise luôn, không retry
|
||||
result = run_async(graphiti.add_episode(
|
||||
name=f"chunk_{i}",
|
||||
episode_body=chunk,
|
||||
source_description="MiroFish text chunk",
|
||||
reference_time=datetime.now(timezone.utc),
|
||||
source=EpisodeType.text,
|
||||
group_id=graph_id,
|
||||
entity_types=entity_types,
|
||||
edge_types=edge_types,
|
||||
edge_type_map=edge_type_map,
|
||||
))
|
||||
ep_uuid = getattr(result.episode, "uuid", None)
|
||||
if ep_uuid:
|
||||
episode_uuids.append(ep_uuid)
|
||||
break
|
||||
except Exception as e:
|
||||
if attempt < max_retries - 1:
|
||||
if progress_callback:
|
||||
progress_callback(f"Failed to send batch {batch_num}: {err_str}", 0)
|
||||
raise # changed
|
||||
else: # changed: for...else — chạy khi hết max_retries mà vẫn chưa break
|
||||
raise Exception(f"Batch {batch_num} failed after {max_retries} retries due to rate limiting") # changed
|
||||
|
||||
progress_callback(
|
||||
f"Chunk {i + 1} failed ({str(e)[:80]}), retry in {delay:.0f}s...",
|
||||
(i + 1) / total_chunks,
|
||||
)
|
||||
time.sleep(delay)
|
||||
delay *= 2
|
||||
else:
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
f"Chunk {i + 1} failed after {max_retries} attempts: {str(e)[:120]}",
|
||||
(i + 1) / total_chunks,
|
||||
)
|
||||
raise
|
||||
|
||||
return episode_uuids
|
||||
|
||||
def _wait_for_episodes(
|
||||
|
|
@ -381,69 +366,24 @@ class GraphBuilderService:
|
|||
progress_callback: Optional[Callable] = None,
|
||||
timeout: int = 1000
|
||||
):
|
||||
"""Chạy vòng lặp để kiểm tra và chờ cho tới khi mọi Episode (các khối Text) đều hoàn tất quá trình process từ hệ thống"""
|
||||
if not episode_uuids:
|
||||
if progress_callback:
|
||||
progress_callback("No episodes to scan (Progress 100%)", 1.0)
|
||||
return
|
||||
|
||||
start_time = time.time()
|
||||
pending_episodes = set(episode_uuids)
|
||||
completed_count = 0
|
||||
total_episodes = len(episode_uuids)
|
||||
|
||||
"""No-op với Graphiti: add_episode đã xử lý đồng bộ (extract + ghi Neo4j
|
||||
xong trước khi return), nên không cần poll trạng thái processing.
|
||||
|
||||
Giữ method để tương thích caller (_build_graph_worker).
|
||||
"""
|
||||
if progress_callback:
|
||||
progress_callback(f"Waiting for analysis of {total_episodes} text chunks to begin...", 0)
|
||||
|
||||
while pending_episodes:
|
||||
# Ngắt thoát và trả về lỗi nếu bị Timeout (Chạy quá thời gian cho phép)
|
||||
if time.time() - start_time > timeout:
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
f"Some text segments have timed out, but {completed_count}/{total_episodes} have completed successfully",
|
||||
completed_count / total_episodes
|
||||
)
|
||||
break
|
||||
|
||||
# Duyệt vòng lặp mỗi episode uuid để lấy cập nhật tiến trình check của từng episode một
|
||||
for ep_uuid in list(pending_episodes):
|
||||
try:
|
||||
# CHECK EPISODE STATUS - Queries individual episode processing status
|
||||
episode = self.client.graph.episode.get(uuid_=ep_uuid)
|
||||
# CHECK PROCESSED FLAG - Determines if Zep
|
||||
is_processed = getattr(episode, 'processed', False)
|
||||
|
||||
if is_processed:
|
||||
# if processed: remove from set
|
||||
pending_episodes.remove(ep_uuid)
|
||||
completed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
# Tạm thời bỏ qua nếu request lỗi, vòng lặp kế theo sẽ tự động call tiếp để get status
|
||||
pass
|
||||
|
||||
elapsed = int(time.time() - start_time)
|
||||
# update progress callback
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
f"Zep is processing in the background... {completed_count}/{total_episodes} done, {len(pending_episodes)} tasks remaining ({elapsed}s elapsed)",
|
||||
completed_count / total_episodes if total_episodes > 0 else 0
|
||||
)
|
||||
|
||||
if pending_episodes:
|
||||
time.sleep(3) # Lặp chu kỳ check mỗi 3 giây
|
||||
|
||||
if progress_callback:
|
||||
# Final completion message
|
||||
progress_callback(f"Data upload process completed: {completed_count}/{total_episodes}", 1.0)
|
||||
|
||||
progress_callback("Processing complete (synchronous)", 1.0)
|
||||
return
|
||||
|
||||
def _get_graph_info(self, graph_id: str) -> GraphInfo:
|
||||
"""Lấy/Get dữ liệu Graph Info hiện tại"""
|
||||
# Load các điểm nút/Entity đang có (Qua trình duyệt web/paging)
|
||||
nodes = fetch_all_nodes(self.client, graph_id)
|
||||
driver = get_graphiti().driver
|
||||
|
||||
# Load các điểm nút/Entity đang có (phân trang)
|
||||
nodes = fetch_all_nodes(driver, graph_id)
|
||||
|
||||
# Lấy theo mảng phân trang thông tin các Edges/Mối Liên Kết
|
||||
edges = fetch_all_edges(self.client, graph_id)
|
||||
edges = fetch_all_edges(driver, graph_id)
|
||||
|
||||
# Nối lại và thống kê những Entity Types
|
||||
entity_types = set()
|
||||
|
|
@ -471,13 +411,14 @@ class GraphBuilderService:
|
|||
Một object Dictionary bao hàm thông tin dữ liệu về Mạng lưới Cụm (nodes) và Cạnh (edges),
|
||||
và toàn bộ chi tiết đi kèm khác (Time khởi tạo, Property).
|
||||
"""
|
||||
nodes = fetch_all_nodes(self.client, graph_id)
|
||||
edges = fetch_all_edges(self.client, graph_id)
|
||||
driver = get_graphiti().driver
|
||||
nodes = fetch_all_nodes(driver, graph_id)
|
||||
edges = fetch_all_edges(driver, graph_id)
|
||||
|
||||
# Giữ một map tra cứu để phục vụ lấy 'Tên' nhanh theo ID UUID
|
||||
node_map = {}
|
||||
for node in nodes:
|
||||
node_map[node.uuid_] = node.name or ""
|
||||
node_map[node.uuid] = node.name or ""
|
||||
|
||||
nodes_data = []
|
||||
for node in nodes:
|
||||
|
|
@ -487,7 +428,7 @@ class GraphBuilderService:
|
|||
created_at = str(created_at)
|
||||
|
||||
nodes_data.append({
|
||||
"uuid": node.uuid_,
|
||||
"uuid": node.uuid,
|
||||
"name": node.name,
|
||||
"labels": node.labels or [],
|
||||
"summary": node.summary or "",
|
||||
|
|
@ -514,7 +455,7 @@ class GraphBuilderService:
|
|||
fact_type = getattr(edge, 'fact_type', None) or edge.name or ""
|
||||
|
||||
edges_data.append({
|
||||
"uuid": edge.uuid_,
|
||||
"uuid": edge.uuid,
|
||||
"name": edge.name or "",
|
||||
"fact": edge.fact or "",
|
||||
"fact_type": fact_type,
|
||||
|
|
@ -539,6 +480,7 @@ class GraphBuilderService:
|
|||
}
|
||||
|
||||
def delete_graph(self, graph_id: str):
|
||||
"""删除图谱"""
|
||||
self.client.graph.delete(graph_id=graph_id)
|
||||
"""Xóa toàn bộ node/edge/episode thuộc group_id (DETACH DELETE cascade)."""
|
||||
driver = get_graphiti().driver
|
||||
run_async(EntityNode.delete_by_group_id(driver, graph_id))
|
||||
|
||||
|
|
|
|||
|
|
@ -30,11 +30,16 @@ from dataclasses import dataclass, field
|
|||
from datetime import datetime
|
||||
|
||||
from openai import OpenAI
|
||||
from zep_cloud.client import Zep
|
||||
|
||||
from graphiti_core.search.search_config_recipes import (
|
||||
EDGE_HYBRID_SEARCH_RRF,
|
||||
NODE_HYBRID_SEARCH_RRF,
|
||||
)
|
||||
|
||||
from ..config import Config
|
||||
from ..utils.logger import get_logger
|
||||
from ..utils.llm_cost import create_tracked_chat_completion
|
||||
from ..utils.graphiti_client import get_graphiti, run_async
|
||||
from .zep_entity_reader import EntityNode, ZepEntityReader
|
||||
|
||||
logger = get_logger('mirofish.oasis_profile')
|
||||
|
|
@ -259,19 +264,18 @@ class OasisProfileGenerator:
|
|||
"phase": "generate_profiles",
|
||||
}
|
||||
|
||||
# --- Khởi tạo Zep client (dùng để vector search tìm thêm context) ---
|
||||
# --- Cấu hình graph search (dùng để bổ sung context cho entity) ---
|
||||
# graph_id được truyền vào từ simulation_manager để tất cả search đều
|
||||
# trỏ đúng vào graph của simulation hiện tại
|
||||
self.zep_api_key = zep_api_key or Config.ZEP_API_KEY
|
||||
self.zep_client = None
|
||||
# trỏ đúng vào graph (group_id) của simulation hiện tại.
|
||||
# zep_api_key giữ lại trong chữ ký để tương thích caller cũ, không còn dùng.
|
||||
self.zep_api_key = zep_api_key
|
||||
self.graph_id = graph_id
|
||||
|
||||
if self.zep_api_key:
|
||||
try:
|
||||
self.zep_client = Zep(api_key=self.zep_api_key)
|
||||
except Exception as e:
|
||||
# Không raise — Zep search là tính năng bổ sung, không bắt buộc
|
||||
logger.warning(f"Failed to initialize Zep client: {e}")
|
||||
# graph search là tính năng bổ sung, chỉ bật khi Neo4j được cấu hình.
|
||||
# Graphiti instance lấy lazy per-thread qua get_graphiti() khi search.
|
||||
self.graph_search_enabled = bool(Config.NEO4J_URI and Config.NEO4J_PASSWORD)
|
||||
if not self.graph_search_enabled:
|
||||
logger.warning("Neo4j not configured — entity graph search disabled (optional feature)")
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PUBLIC: generate_profile_from_entity — Entry point sinh 1 profile đơn lẻ
|
||||
|
|
@ -409,8 +413,8 @@ class OasisProfileGenerator:
|
|||
"""
|
||||
import concurrent.futures
|
||||
|
||||
# Nếu không có Zep client hoặc graph_id → trả về rỗng, không làm gì
|
||||
if not self.zep_client:
|
||||
# Nếu graph search bị tắt (Neo4j chưa cấu hình) → trả về rỗng, không làm gì
|
||||
if not self.graph_search_enabled:
|
||||
return {"facts": [], "node_summaries": [], "context": ""}
|
||||
|
||||
entity_name = entity.name
|
||||
|
|
@ -428,55 +432,41 @@ class OasisProfileGenerator:
|
|||
# Query tổng quát để Zep semantic search trả về nhiều kết quả nhất
|
||||
comprehensive_query = f"Provide all facts, activities, relationships, and context about: {entity_name}"
|
||||
|
||||
def search_edges():
|
||||
"""Tìm các fact/quan hệ qua edge search — có retry."""
|
||||
def _search_with_retry(config_recipe, limit: int, kind: str):
|
||||
"""Chạy Graphiti hybrid search (RRF) với retry — trả về SearchResults hoặc None.
|
||||
|
||||
Mỗi lần gọi nằm trong worker thread riêng (ThreadPoolExecutor), nên
|
||||
get_graphiti()/run_async cấp Graphiti instance + event loop riêng cho thread đó.
|
||||
"""
|
||||
max_retries = 3
|
||||
last_exception = None
|
||||
delay = 2.0
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return self.zep_client.graph.search(
|
||||
query=comprehensive_query,
|
||||
graph_id=self.graph_id,
|
||||
limit=30, # Lấy tối đa 30 facts
|
||||
scope="edges",
|
||||
reranker="rrf" # Reciprocal Rank Fusion — kết hợp nhiều ranking strategies
|
||||
)
|
||||
graphiti = get_graphiti()
|
||||
cfg = config_recipe.model_copy(deep=True)
|
||||
cfg.limit = limit
|
||||
return run_async(graphiti.search_(
|
||||
comprehensive_query,
|
||||
config=cfg,
|
||||
group_ids=[self.graph_id],
|
||||
))
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries - 1:
|
||||
logger.debug(f"Zep Edge search failed on attempt {attempt + 1}: {str(e)[:80]}, retrying...")
|
||||
logger.debug(f"Graph {kind} search failed on attempt {attempt + 1}: {str(e)[:80]}, retrying...")
|
||||
time.sleep(delay)
|
||||
delay *= 2
|
||||
else:
|
||||
logger.debug(f"Zep Edge search entirely failed after {max_retries} attempts: {e}")
|
||||
logger.debug(f"Graph {kind} search entirely failed after {max_retries} attempts: {e}")
|
||||
return None
|
||||
|
||||
def search_edges():
|
||||
"""Tìm các fact/quan hệ qua edge search — có retry."""
|
||||
return _search_with_retry(EDGE_HYBRID_SEARCH_RRF, 30, "edge")
|
||||
|
||||
def search_nodes():
|
||||
"""Tìm các entity liên quan qua node search — có retry."""
|
||||
max_retries = 3
|
||||
last_exception = None
|
||||
delay = 2.0
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return self.zep_client.graph.search(
|
||||
query=comprehensive_query,
|
||||
graph_id=self.graph_id,
|
||||
limit=20, # Lấy tối đa 20 node summaries
|
||||
scope="nodes",
|
||||
reranker="rrf"
|
||||
)
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries - 1:
|
||||
logger.debug(f"Zep Node search failed on attempt {attempt + 1}: {str(e)[:80]}, retrying...")
|
||||
time.sleep(delay)
|
||||
delay *= 2
|
||||
else:
|
||||
logger.debug(f"Zep Node search entirely failed after {max_retries} attempts: {e}")
|
||||
return None
|
||||
return _search_with_retry(NODE_HYBRID_SEARCH_RRF, 20, "node")
|
||||
|
||||
try:
|
||||
# Chạy song song 2 search — giảm thời gian chờ từ ~2T xuống ~T
|
||||
|
|
@ -513,12 +503,12 @@ class OasisProfileGenerator:
|
|||
context_parts.append("Related Entities:\n" + "\n".join(f"- {s}" for s in results["node_summaries"][:10]))
|
||||
results["context"] = "\n\n".join(context_parts)
|
||||
|
||||
logger.info(f"Zep unified search completed: {entity_name}, fetched {len(results['facts'])} facts, {len(results['node_summaries'])} related nodes")
|
||||
logger.info(f"Graph unified search completed: {entity_name}, fetched {len(results['facts'])} facts, {len(results['node_summaries'])} related nodes")
|
||||
|
||||
except concurrent.futures.TimeoutError:
|
||||
logger.warning(f"Zep Retrieval Time-Out ({entity_name})")
|
||||
logger.warning(f"Graph Retrieval Time-Out ({entity_name})")
|
||||
except Exception as e:
|
||||
logger.warning(f"Zep Retrieval Failed ({entity_name}): {e}")
|
||||
logger.warning(f"Graph Retrieval Failed ({entity_name}): {e}")
|
||||
|
||||
return results
|
||||
|
||||
|
|
@ -1205,7 +1195,7 @@ QUAN TRỌNG:
|
|||
use_llm: bool = True,
|
||||
progress_callback: Optional[callable] = None,
|
||||
graph_id: Optional[str] = None,
|
||||
parallel_count: int = 5,
|
||||
parallel_count: int = 15,
|
||||
realtime_output_path: Optional[str] = None,
|
||||
output_platform: str = "reddit",
|
||||
metadata_platform: Optional[str] = None,
|
||||
|
|
|
|||
|
|
@ -286,8 +286,9 @@ class OntologyGenerator:
|
|||
|
||||
return result
|
||||
|
||||
# Định mức giới hạn độ dài ký tự tối đa của đoạn văn bản có thể gửi cho LLM (10 vạn chữ)
|
||||
MAX_TEXT_LENGTH_FOR_LLM = 100000
|
||||
# Định mức giới hạn độ dài ký tự tối đa của đoạn văn bản có thể gửi cho LLM
|
||||
# (~4 ký tự/token; 400000 ký tự ≈ 100K token input, vẫn dư trong cửa sổ 262144 token của Qwen3.6-27B)
|
||||
MAX_TEXT_LENGTH_FOR_LLM = 400000
|
||||
|
||||
def _build_user_message(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ class SimulationManager:
|
|||
defined_entity_types: Optional[List[str]] = None,
|
||||
use_llm_for_profiles: bool = True,
|
||||
progress_callback: Optional[callable] = None,
|
||||
parallel_profile_count: int = 3
|
||||
parallel_profile_count: int = 15
|
||||
) -> SimulationState:
|
||||
"""
|
||||
Giai đoạn chuẩn bị dữ liệu tạo giả lập mô phỏng (Tiến trình Automation 100%)
|
||||
|
|
|
|||
|
|
@ -34,11 +34,14 @@ import time
|
|||
from typing import Dict, Any, List, Optional, Set, Callable, TypeVar
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from zep_cloud.client import Zep
|
||||
# Graphiti classes import dưới alias để không đụng dataclass EntityNode định nghĩa bên dưới.
|
||||
from graphiti_core.nodes import EntityNode as GraphitiEntityNode
|
||||
from graphiti_core.edges import EntityEdge as GraphitiEntityEdge
|
||||
|
||||
from ..config import Config
|
||||
from ..utils.logger import get_logger
|
||||
from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges
|
||||
from ..utils.graphiti_client import get_graphiti, run_async
|
||||
|
||||
logger = get_logger('mirofish.zep_entity_reader')
|
||||
|
||||
|
|
@ -161,12 +164,9 @@ class ZepEntityReader:
|
|||
"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
# Ưu tiên api_key được truyền vào trực tiếp; fallback về biến môi trường
|
||||
self.api_key = api_key or Config.ZEP_API_KEY
|
||||
if not self.api_key:
|
||||
raise ValueError("ZEP_API_KEY is not configured")
|
||||
|
||||
self.client = Zep(api_key=self.api_key)
|
||||
# api_key giữ lại trong chữ ký để tương thích caller cũ, nhưng không còn dùng:
|
||||
# Graphiti lấy cấu hình Neo4j + LLM qua get_graphiti() (lazy, per-thread).
|
||||
self.api_key = api_key
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PRIVATE: _call_with_retry — Retry wrapper cho các API call Zep đơn lẻ
|
||||
|
|
@ -253,14 +253,13 @@ class ZepEntityReader:
|
|||
"""
|
||||
logger.info(f"Fetching all nodes for graph {graph_id}...")
|
||||
|
||||
nodes = fetch_all_nodes(self.client, graph_id)
|
||||
nodes = fetch_all_nodes(get_graphiti().driver, graph_id)
|
||||
|
||||
# Chuẩn hoá từ Zep SDK object sang Python dict thuần
|
||||
# getattr với 2 tên vì Zep SDK đôi khi dùng uuid_ thay vì uuid để tránh conflict keyword
|
||||
# Chuẩn hoá từ graphiti EntityNode sang Python dict thuần
|
||||
nodes_data = []
|
||||
for node in nodes:
|
||||
nodes_data.append({
|
||||
"uuid": getattr(node, 'uuid_', None) or getattr(node, 'uuid', ''),
|
||||
"uuid": getattr(node, 'uuid', ''),
|
||||
"name": node.name or "",
|
||||
"labels": node.labels or [],
|
||||
"summary": node.summary or "",
|
||||
|
|
@ -286,12 +285,12 @@ class ZepEntityReader:
|
|||
"""
|
||||
logger.info(f"Fetching all edges for graph {graph_id}...")
|
||||
|
||||
edges = fetch_all_edges(self.client, graph_id)
|
||||
edges = fetch_all_edges(get_graphiti().driver, graph_id)
|
||||
|
||||
edges_data = []
|
||||
for edge in edges:
|
||||
edges_data.append({
|
||||
"uuid": getattr(edge, 'uuid_', None) or getattr(edge, 'uuid', ''),
|
||||
"uuid": getattr(edge, 'uuid', ''),
|
||||
"name": edge.name or "",
|
||||
"fact": edge.fact or "", # Mô tả quan hệ dạng câu (ví dụ: "A làm việc tại B")
|
||||
"source_node_uuid": edge.source_node_uuid, # UUID node nguồn
|
||||
|
|
@ -327,15 +326,16 @@ class ZepEntityReader:
|
|||
Trả về [] nếu lỗi (không raise exception — caller tự xử lý thiếu data).
|
||||
"""
|
||||
try:
|
||||
driver = get_graphiti().driver
|
||||
edges = self._call_with_retry(
|
||||
func=lambda: self.client.graph.node.get_entity_edges(node_uuid=node_uuid),
|
||||
func=lambda: run_async(GraphitiEntityEdge.get_by_node_uuid(driver, node_uuid)),
|
||||
operation_name=f"Fetch node edges(node={node_uuid[:8]}...)"
|
||||
)
|
||||
|
||||
edges_data = []
|
||||
for edge in edges:
|
||||
edges_data.append({
|
||||
"uuid": getattr(edge, 'uuid_', None) or getattr(edge, 'uuid', ''),
|
||||
"uuid": getattr(edge, 'uuid', ''),
|
||||
"name": edge.name or "",
|
||||
"fact": edge.fact or "",
|
||||
"source_node_uuid": edge.source_node_uuid,
|
||||
|
|
@ -579,9 +579,12 @@ class ZepEntityReader:
|
|||
EntityNode đầy đủ hoặc None nếu không tìm thấy / lỗi
|
||||
"""
|
||||
try:
|
||||
# Fetch node theo UUID với retry
|
||||
# Fetch node theo UUID với retry.
|
||||
# GraphitiEntityNode.get_by_uuid raise NodeNotFoundError nếu không có →
|
||||
# được bắt bởi except phía dưới (log + trả None).
|
||||
driver = get_graphiti().driver
|
||||
node = self._call_with_retry(
|
||||
func=lambda: self.client.graph.node.get(uuid_=entity_uuid),
|
||||
func=lambda: run_async(GraphitiEntityNode.get_by_uuid(driver, entity_uuid)),
|
||||
operation_name=f"Fetch node detail(uuid={entity_uuid[:8]}...)"
|
||||
)
|
||||
|
||||
|
|
@ -629,7 +632,7 @@ class ZepEntityReader:
|
|||
})
|
||||
|
||||
return EntityNode(
|
||||
uuid=getattr(node, 'uuid_', None) or getattr(node, 'uuid', ''),
|
||||
uuid=getattr(node, 'uuid', ''),
|
||||
name=node.name or "",
|
||||
labels=node.labels or [],
|
||||
summary=node.summary or "",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
Dịch vụ cập nhật bộ nhớ đồ thị Zep
|
||||
Cập nhật động các hoạt động của Agent trong mô phỏng lên đồ thị Zep
|
||||
Dịch vụ cập nhật bộ nhớ đồ thị (Graphiti + Neo4j backend)
|
||||
Cập nhật động các hoạt động của Agent trong mô phỏng lên đồ thị tri thức
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -9,13 +9,14 @@ import threading
|
|||
import json
|
||||
from typing import Dict, Any, List, Optional, Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from queue import Queue, Empty
|
||||
|
||||
from zep_cloud.client import Zep
|
||||
from graphiti_core.nodes import EpisodeType
|
||||
|
||||
from ..config import Config
|
||||
from ..utils.logger import get_logger
|
||||
from ..utils.graphiti_client import get_graphiti, run_async
|
||||
|
||||
logger = get_logger('mirofish.zep_graph_memory_updater')
|
||||
|
||||
|
|
@ -231,19 +232,18 @@ class ZepGraphMemoryUpdater:
|
|||
def __init__(self, graph_id: str, api_key: Optional[str] = None):
|
||||
"""
|
||||
Khởi tạo trình cập nhật
|
||||
|
||||
|
||||
Args:
|
||||
graph_id: ID của đồ thị Zep
|
||||
api_key: Zep API Key (tự chọn, mặc định lấy từ config)
|
||||
graph_id: group_id của đồ thị Graphiti (Neo4j)
|
||||
api_key: giữ lại trong chữ ký để tương thích caller cũ, không còn dùng
|
||||
(Graphiti lấy cấu hình Neo4j + LLM qua get_graphiti()).
|
||||
"""
|
||||
self.graph_id = graph_id
|
||||
self.api_key = api_key or Config.ZEP_API_KEY
|
||||
|
||||
if not self.api_key:
|
||||
raise ValueError("ZEP_API_KEY is not configured")
|
||||
|
||||
self.client = Zep(api_key=self.api_key)
|
||||
|
||||
self.api_key = api_key
|
||||
|
||||
# Graphiti instance lấy lazy per-thread qua get_graphiti() trong _send_batch_activities,
|
||||
# không khởi tạo client ở đây để tránh chia sẻ event loop giữa các thread.
|
||||
|
||||
# Hàng đợi hoạt động
|
||||
self._activity_queue: Queue = Queue()
|
||||
|
||||
|
|
@ -389,28 +389,34 @@ class ZepGraphMemoryUpdater:
|
|||
|
||||
def _send_batch_activities(self, activities: List[AgentActivity], platform: str):
|
||||
"""
|
||||
Gửi hàng loạt các hoạt động lên đồ thị Zep (Gộp chung vào một đoạn text)
|
||||
|
||||
Gửi hàng loạt các hoạt động lên đồ thị tri thức (Gộp chung vào một episode)
|
||||
|
||||
Args:
|
||||
activities: Danh sách hoạt động của Agent
|
||||
platform: Tên nền tảng
|
||||
"""
|
||||
if not activities:
|
||||
return
|
||||
|
||||
|
||||
# Gộp nhiều hoạt động vào một văn bản chung, tách nhau bởi xuống dòng
|
||||
episode_texts = [activity.to_episode_text() for activity in activities]
|
||||
combined_text = "\n".join(episode_texts)
|
||||
|
||||
|
||||
# Gửi với cơ chế thử lại
|
||||
for attempt in range(self.MAX_RETRIES):
|
||||
try:
|
||||
self.client.graph.add(
|
||||
graph_id=self.graph_id,
|
||||
type="text",
|
||||
data=combined_text
|
||||
)
|
||||
|
||||
# Graphiti add_episode xử lý đồng bộ (extract + ghi Neo4j xong khi await trả về).
|
||||
# group_id=self.graph_id để episode ghi vào đúng partition của simulation.
|
||||
graphiti = get_graphiti()
|
||||
run_async(graphiti.add_episode(
|
||||
name=f"sim_{platform}_round_{activities[0].round_num}",
|
||||
episode_body=combined_text,
|
||||
source_description=f"MiroFish simulation activity ({platform})",
|
||||
reference_time=datetime.now(timezone.utc),
|
||||
source=EpisodeType.text,
|
||||
group_id=self.graph_id,
|
||||
))
|
||||
|
||||
self._total_sent += 1
|
||||
self._total_items_sent += len(activities)
|
||||
display_name = self._get_platform_display_name(platform)
|
||||
|
|
@ -420,10 +426,10 @@ class ZepGraphMemoryUpdater:
|
|||
|
||||
except Exception as e:
|
||||
if attempt < self.MAX_RETRIES - 1:
|
||||
logger.warning(f"Failed to send batch to Zep (attempt {attempt + 1}/{self.MAX_RETRIES}): {e}")
|
||||
logger.warning(f"Failed to send batch to graph (attempt {attempt + 1}/{self.MAX_RETRIES}): {e}")
|
||||
time.sleep(self.RETRY_DELAY * (attempt + 1))
|
||||
else:
|
||||
logger.error(f"Failed to send batch to Zep after {self.MAX_RETRIES} attempts: {e}")
|
||||
logger.error(f"Failed to send batch to graph after {self.MAX_RETRIES} attempts: {e}")
|
||||
self._failed_count += 1
|
||||
|
||||
def _flush_remaining(self):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Dịch vụ cung cấp các công cụ tìm kiếm trên nền tảng Zep Cloud.
|
||||
Dịch vụ cung cấp các công cụ tìm kiếm trên đồ thị tri thức (Graphiti + Neo4j backend).
|
||||
Đóng gói các công cụ tìm kiếm đồ thị (graph search), đọc thông tin node, truy vấn cạnh (edge), v.v., để Report Agent sử dụng.
|
||||
|
||||
Các công cụ tìm kiếm cốt lõi (sau khi tối ưu hóa):
|
||||
|
|
@ -13,12 +13,17 @@ import json
|
|||
from typing import Dict, Any, List, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from zep_cloud.client import Zep
|
||||
from graphiti_core.nodes import EntityNode as GraphitiEntityNode
|
||||
from graphiti_core.search.search_config_recipes import (
|
||||
EDGE_HYBRID_SEARCH_RRF,
|
||||
NODE_HYBRID_SEARCH_RRF,
|
||||
)
|
||||
|
||||
from ..config import Config
|
||||
from ..utils.logger import get_logger
|
||||
from ..utils.llm_client import LLMClient
|
||||
from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges
|
||||
from ..utils.graphiti_client import get_graphiti, run_async
|
||||
|
||||
logger = get_logger('mirofish.zep_tools')
|
||||
|
||||
|
|
@ -423,11 +428,9 @@ class ZepToolsService:
|
|||
RETRY_DELAY = 2.0
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None, llm_client: Optional[LLMClient] = None):
|
||||
self.api_key = api_key or Config.ZEP_API_KEY
|
||||
if not self.api_key:
|
||||
raise ValueError("ZEP_API_KEY is not configured")
|
||||
|
||||
self.client = Zep(api_key=self.api_key)
|
||||
# api_key giữ lại trong chữ ký để tương thích caller cũ, không còn dùng:
|
||||
# Graphiti lấy cấu hình Neo4j + LLM qua get_graphiti() (lazy, per-thread).
|
||||
self.api_key = api_key
|
||||
# LLM client được sử dụng bởi InsightForge để sinh ra các sub-queries
|
||||
self._llm_client = llm_client
|
||||
logger.info("ZepToolsService initialized successfully")
|
||||
|
|
@ -487,42 +490,36 @@ class ZepToolsService:
|
|||
SearchResult: Đối tượng chứa kết quả tìm kiếm đã phân tích
|
||||
"""
|
||||
logger.info(f"Graph search: graph_id={graph_id}, query={query[:50]}...")
|
||||
|
||||
# Thử sử dụng API Zep Cloud Search
|
||||
|
||||
# Thử dùng Graphiti hybrid search (semantic + BM25 + RRF rerank).
|
||||
try:
|
||||
search_results = self._call_with_retry(
|
||||
func=lambda: self.client.graph.search(
|
||||
graph_id=graph_id,
|
||||
query=query,
|
||||
limit=limit,
|
||||
scope=scope,
|
||||
reranker="cross_encoder"
|
||||
),
|
||||
func=lambda: self._graphiti_search(graph_id, query, limit, scope),
|
||||
operation_name=f"Graph Search(graph={graph_id})"
|
||||
)
|
||||
|
||||
|
||||
facts = []
|
||||
edges = []
|
||||
nodes = []
|
||||
|
||||
|
||||
# Phân tích kết quả tìm kiếm cạnh (edges/relationships)
|
||||
if hasattr(search_results, 'edges') and search_results.edges:
|
||||
for edge in search_results.edges:
|
||||
if hasattr(edge, 'fact') and edge.fact:
|
||||
facts.append(edge.fact)
|
||||
edges.append({
|
||||
"uuid": getattr(edge, 'uuid_', None) or getattr(edge, 'uuid', ''),
|
||||
"uuid": getattr(edge, 'uuid', ''),
|
||||
"name": getattr(edge, 'name', ''),
|
||||
"fact": getattr(edge, 'fact', ''),
|
||||
"source_node_uuid": getattr(edge, 'source_node_uuid', ''),
|
||||
"target_node_uuid": getattr(edge, 'target_node_uuid', ''),
|
||||
})
|
||||
|
||||
|
||||
# Phân tích kết quả tìm kiếm thực thể (nodes)
|
||||
if hasattr(search_results, 'nodes') and search_results.nodes:
|
||||
for node in search_results.nodes:
|
||||
nodes.append({
|
||||
"uuid": getattr(node, 'uuid_', None) or getattr(node, 'uuid', ''),
|
||||
"uuid": getattr(node, 'uuid', ''),
|
||||
"name": getattr(node, 'name', ''),
|
||||
"labels": getattr(node, 'labels', []),
|
||||
"summary": getattr(node, 'summary', ''),
|
||||
|
|
@ -530,9 +527,9 @@ class ZepToolsService:
|
|||
# Phần tóm tắt (summary) của node cũng được coi là một fact
|
||||
if hasattr(node, 'summary') and node.summary:
|
||||
facts.append(f"[{node.name}]: {node.summary}")
|
||||
|
||||
|
||||
logger.info(f"Search completed: Found {len(facts)} related facts")
|
||||
|
||||
|
||||
return SearchResult(
|
||||
facts=facts,
|
||||
edges=edges,
|
||||
|
|
@ -540,11 +537,40 @@ class ZepToolsService:
|
|||
query=query,
|
||||
total_count=len(facts)
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Zep Search API failed, gracefully degrading to local search: {str(e)}")
|
||||
logger.warning(f"Graph Search failed, gracefully degrading to local search: {str(e)}")
|
||||
# Hạ cấp: Sử dụng tìm kiếm theo từ khóa cục bộ
|
||||
return self._local_search(graph_id, query, limit, scope)
|
||||
|
||||
def _graphiti_search(self, graph_id: str, query: str, limit: int, scope: str):
|
||||
"""Gọi Graphiti hybrid search và gộp kết quả node/edge.
|
||||
|
||||
Map scope (Zep) → search recipe (Graphiti):
|
||||
- "edges" → EDGE_HYBRID_SEARCH_RRF (chỉ trả edges/facts)
|
||||
- "nodes" → NODE_HYBRID_SEARCH_RRF (chỉ trả nodes)
|
||||
- "both" → chạy cả hai rồi gộp lại
|
||||
|
||||
Trả về SearchResults (có .edges và .nodes) để caller xử lý đồng nhất.
|
||||
"""
|
||||
from graphiti_core.search.search_config import SearchResults
|
||||
|
||||
graphiti = get_graphiti()
|
||||
group_ids = [graph_id]
|
||||
|
||||
async def _run():
|
||||
results: List[SearchResults] = []
|
||||
if scope in ("edges", "both"):
|
||||
cfg = EDGE_HYBRID_SEARCH_RRF.model_copy(deep=True)
|
||||
cfg.limit = limit
|
||||
results.append(await graphiti.search_(query, config=cfg, group_ids=group_ids))
|
||||
if scope in ("nodes", "both"):
|
||||
cfg = NODE_HYBRID_SEARCH_RRF.model_copy(deep=True)
|
||||
cfg.limit = limit
|
||||
results.append(await graphiti.search_(query, config=cfg, group_ids=group_ids))
|
||||
return SearchResults.merge(results) if results else SearchResults()
|
||||
|
||||
return run_async(_run())
|
||||
|
||||
def _local_search(
|
||||
self,
|
||||
|
|
@ -662,11 +688,11 @@ class ZepToolsService:
|
|||
"""
|
||||
logger.info(f"Fetching all nodes for graph {graph_id}...")
|
||||
|
||||
nodes = fetch_all_nodes(self.client, graph_id)
|
||||
nodes = fetch_all_nodes(get_graphiti().driver, graph_id)
|
||||
|
||||
result = []
|
||||
for node in nodes:
|
||||
node_uuid = getattr(node, 'uuid_', None) or getattr(node, 'uuid', None) or ""
|
||||
node_uuid = getattr(node, 'uuid', None) or ""
|
||||
result.append(NodeInfo(
|
||||
uuid=str(node_uuid) if node_uuid else "",
|
||||
name=node.name or "",
|
||||
|
|
@ -691,11 +717,11 @@ class ZepToolsService:
|
|||
"""
|
||||
logger.info(f"Fetching all edges for graph {graph_id}...")
|
||||
|
||||
edges = fetch_all_edges(self.client, graph_id)
|
||||
edges = fetch_all_edges(get_graphiti().driver, graph_id)
|
||||
|
||||
result = []
|
||||
for edge in edges:
|
||||
edge_uuid = getattr(edge, 'uuid_', None) or getattr(edge, 'uuid', None) or ""
|
||||
edge_uuid = getattr(edge, 'uuid', None) or ""
|
||||
edge_info = EdgeInfo(
|
||||
uuid=str(edge_uuid) if edge_uuid else "",
|
||||
name=edge.name or "",
|
||||
|
|
@ -704,12 +730,15 @@ class ZepToolsService:
|
|||
target_node_uuid=edge.target_node_uuid or ""
|
||||
)
|
||||
|
||||
# Bổ sung thông tin thời gian hợp lệ (temporal info)
|
||||
# Bổ sung thông tin thời gian hợp lệ (temporal info).
|
||||
# Graphiti trả về datetime → ép sang str cho đồng nhất với EdgeInfo (Optional[str]).
|
||||
if include_temporal:
|
||||
edge_info.created_at = getattr(edge, 'created_at', None)
|
||||
edge_info.valid_at = getattr(edge, 'valid_at', None)
|
||||
edge_info.invalid_at = getattr(edge, 'invalid_at', None)
|
||||
edge_info.expired_at = getattr(edge, 'expired_at', None)
|
||||
def _ts(value):
|
||||
return str(value) if value is not None else None
|
||||
edge_info.created_at = _ts(getattr(edge, 'created_at', None))
|
||||
edge_info.valid_at = _ts(getattr(edge, 'valid_at', None))
|
||||
edge_info.invalid_at = _ts(getattr(edge, 'invalid_at', None))
|
||||
edge_info.expired_at = _ts(getattr(edge, 'expired_at', None))
|
||||
|
||||
result.append(edge_info)
|
||||
|
||||
|
|
@ -729,16 +758,19 @@ class ZepToolsService:
|
|||
logger.info(f"Fetching node detail: {node_uuid[:8]}...")
|
||||
|
||||
try:
|
||||
# GraphitiEntityNode.get_by_uuid raise NodeNotFoundError nếu không có →
|
||||
# được bắt bởi except phía dưới (log + trả None).
|
||||
driver = get_graphiti().driver
|
||||
node = self._call_with_retry(
|
||||
func=lambda: self.client.graph.node.get(uuid_=node_uuid),
|
||||
func=lambda: run_async(GraphitiEntityNode.get_by_uuid(driver, node_uuid)),
|
||||
operation_name=f"Fetching node detail (uuid={node_uuid[:8]}...)"
|
||||
)
|
||||
|
||||
|
||||
if not node:
|
||||
return None
|
||||
|
||||
|
||||
return NodeInfo(
|
||||
uuid=getattr(node, 'uuid_', None) or getattr(node, 'uuid', ''),
|
||||
uuid=getattr(node, 'uuid', ''),
|
||||
name=node.name or "",
|
||||
labels=node.labels or [],
|
||||
summary=node.summary or "",
|
||||
|
|
@ -1818,10 +1850,11 @@ Ràng buộc định dạng (BẮT BUỘC tuân thủ):
|
|||
- Sử dụng dấu ngoặc kép kiểu Việt Nam 「」 khi trích dẫn nguyên văn lời của người được phỏng vấn.
|
||||
- Có thể sử dụng dấu **in đậm** cho các từ khóa, nhưng không sử dụng bất kỳ cú pháp Markdown nào khác."""
|
||||
|
||||
interview_content = "\n\n".join(interview_texts)
|
||||
user_prompt = f"""Chủ đề phỏng vấn: {interview_requirement}
|
||||
|
||||
Nội dung phỏng vấn:
|
||||
{"\n\n".join(interview_texts)}
|
||||
{interview_content}
|
||||
|
||||
Hãy tạo bản tóm tắt phỏng vấn."""
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class LLMClient:
|
|||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 16000,
|
||||
max_tokens: int = 32000,
|
||||
response_format: Optional[Dict] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
|
|
@ -88,7 +88,7 @@ class LLMClient:
|
|||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float = 0.3,
|
||||
max_tokens: int = 50000,
|
||||
max_tokens: int = 32000,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from graphiti_core.edges import EntityEdge
|
|||
from graphiti_core.errors import GroupsEdgesNotFoundError
|
||||
from graphiti_core.nodes import EntityNode
|
||||
|
||||
from .graphiti_client import run_async
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger('mirofish.graph_paging')
|
||||
|
|
@ -142,7 +143,9 @@ def fetch_all_nodes(
|
|||
retry_delay: float = _DEFAULT_RETRY_DELAY,
|
||||
) -> list[Any]:
|
||||
"""Lấy toàn bộ EntityNode theo group_id, tối đa max_items (mặc định 2000)."""
|
||||
return asyncio.run(_fetch_all_nodes_async(driver, graph_id, page_size, max_items, max_retries, retry_delay))
|
||||
# run_async (loop thread-local) thay vì asyncio.run (loop mới) để Neo4j driver
|
||||
# luôn chạy trên cùng event loop đã gắn — tránh "Future attached to a different loop".
|
||||
return run_async(_fetch_all_nodes_async(driver, graph_id, page_size, max_items, max_retries, retry_delay))
|
||||
|
||||
|
||||
def fetch_all_edges(
|
||||
|
|
@ -153,4 +156,4 @@ def fetch_all_edges(
|
|||
retry_delay: float = _DEFAULT_RETRY_DELAY,
|
||||
) -> list[Any]:
|
||||
"""Lấy toàn bộ EntityEdge theo group_id."""
|
||||
return asyncio.run(_fetch_all_edges_async(driver, graph_id, page_size, max_retries, retry_delay))
|
||||
return run_async(_fetch_all_edges_async(driver, graph_id, page_size, max_retries, retry_delay))
|
||||
|
|
|
|||
|
|
@ -1125,7 +1125,7 @@ def create_model(config: Dict[str, Any], use_boost: bool = False):
|
|||
)
|
||||
|
||||
|
||||
def configure_agent_memory_limits(agent_graph, token_limit=150000, message_window_size=25):
|
||||
def configure_agent_memory_limits(agent_graph, token_limit=150000, message_window_size=50):
|
||||
"""
|
||||
Cấu hình giới hạn memory cho tất cả agent trong agent_graph.
|
||||
|
||||
|
|
@ -1337,7 +1337,7 @@ async def run_twitter_simulation(
|
|||
result.env = oasis.make(
|
||||
agent_graph=result.agent_graph,
|
||||
platform=twitter_platform,
|
||||
semaphore=20,
|
||||
semaphore=30,
|
||||
)
|
||||
|
||||
await result.env.reset()
|
||||
|
|
@ -1532,7 +1532,7 @@ async def run_reddit_simulation(
|
|||
agent_graph=result.agent_graph,
|
||||
platform=oasis.DefaultPlatformType.REDDIT,
|
||||
database_path=db_path,
|
||||
semaphore=20,
|
||||
semaphore=30,
|
||||
)
|
||||
|
||||
await result.env.reset()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
# Xem Knowledge Graph trên Neo4j Browser
|
||||
|
||||
Hướng dẫn mở Neo4j Browser và xem graph (entity + quan hệ) theo từng `group_id`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Mở Neo4j Browser
|
||||
|
||||
Vào trình duyệt:
|
||||
|
||||
**http://localhost:7474/browser/**
|
||||
|
||||
Đăng nhập (lấy từ `.env` của project):
|
||||
|
||||
| Trường | Giá trị |
|
||||
|---|---|
|
||||
| Connect URL | `bolt://localhost:7687` |
|
||||
| Username | `neo4j` |
|
||||
| Password | `team10diem` |
|
||||
|
||||
> Sau khi connect, gõ Cypher vào ô lệnh trên cùng → bấm ▶ hoặc `Ctrl+Enter` để chạy.
|
||||
|
||||
---
|
||||
|
||||
## 2. `group_id` là gì — và tại sao BẮT BUỘC dùng
|
||||
|
||||
Mỗi lần build graph tạo ra một **`group_id`** riêng (= `graph_id`, dạng `mirofish_<hex>`).
|
||||
Tất cả node/edge của một graph được gắn cùng `group_id` đó.
|
||||
|
||||
⚠️ **Luôn lọc theo `group_id` trong MỌI query.** Nếu không, Neo4j trả về node của **tất cả graph** trộn lẫn → rối và sai.
|
||||
|
||||
### Liệt kê các `group_id` đang có
|
||||
|
||||
```cypher
|
||||
MATCH (n:Entity)
|
||||
RETURN n.group_id AS graph_id, count(*) AS so_entity
|
||||
ORDER BY so_entity DESC
|
||||
```
|
||||
|
||||
Các graph hiện có (ví dụ — của bạn có thể khác):
|
||||
|
||||
| group_id | số entity |
|
||||
|---|---|
|
||||
| `mirofish_47869debf1e64df1` | 53 |
|
||||
| `mirofish_ffef92194e574283` | 31 |
|
||||
| `mirofish_8770c4041fb141d3` | 30 |
|
||||
| `mirofish_ea729b57027c4ceb` | 23 |
|
||||
|
||||
> 💡 Copy `group_id` muốn xem, rồi thay vào `$GID` trong các query bên dưới.
|
||||
> `graph_id` cũng được in ra khi build graph xong (`"graph_id": "mirofish_..."`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Hai kiểu xem chính
|
||||
|
||||
### Kiểu A — Xem TRỰC QUAN (entity + quan hệ, dạng đồ thị kéo thả)
|
||||
|
||||
Đổi `group_id` cho đúng graph bạn muốn:
|
||||
|
||||
```cypher
|
||||
MATCH (n:Entity {group_id: 'mirofish_8770c4041fb141d3'})-[r:RELATES_TO]->(m:Entity)
|
||||
RETURN n, r, m
|
||||
```
|
||||
|
||||
→ Neo4j Browser hiện các **node tròn (entity)** nối với nhau bằng **mũi tên (quan hệ)**, kéo thả được.
|
||||
Bấm vào 1 node/edge để xem chi tiết (name, fact, summary...).
|
||||
|
||||
### Kiểu B — Xem dạng BẢNG (tên + loại + tóm tắt entity)
|
||||
|
||||
```cypher
|
||||
MATCH (n:Entity {group_id: 'mirofish_8770c4041fb141d3'})
|
||||
RETURN n.name AS ten, labels(n) AS loai, n.summary AS tom_tat
|
||||
```
|
||||
|
||||
→ Hiện bảng: tên thực thể | loại (entity type) | tóm tắt.
|
||||
(Nếu Browser mở ở chế độ Graph, bấm icon **Table** bên trái kết quả để xem dạng bảng.)
|
||||
|
||||
---
|
||||
|
||||
## 4. Đổi graph khác để xem
|
||||
|
||||
Chỉ cần thay chuỗi `group_id` trong query. Ví dụ xem graph 53-entity:
|
||||
|
||||
```cypher
|
||||
MATCH (n:Entity {group_id: 'mirofish_47869debf1e64df1'})-[r:RELATES_TO]->(m:Entity)
|
||||
RETURN n, r, m
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Một số query hữu ích khác (tùy chọn)
|
||||
|
||||
**Xem cả chunk (Episodic) + entity mà chunk đó trích ra:**
|
||||
```cypher
|
||||
MATCH (e:Episodic {group_id: 'mirofish_8770c4041fb141d3'})-[r:MENTIONS]->(n:Entity)
|
||||
RETURN e, r, n
|
||||
```
|
||||
|
||||
**Xem TẤT CẢ node + mọi quan hệ của 1 graph (graph đầy đủ):**
|
||||
```cypher
|
||||
MATCH (n {group_id: 'mirofish_8770c4041fb141d3'})-[r]->(m {group_id: 'mirofish_8770c4041fb141d3'})
|
||||
RETURN n, r, m
|
||||
```
|
||||
|
||||
**Xem các fact (quan hệ) dạng bảng — nguồn → fact → đích:**
|
||||
```cypher
|
||||
MATCH (n:Entity {group_id: 'mirofish_8770c4041fb141d3'})-[r:RELATES_TO]->(m:Entity)
|
||||
RETURN n.name AS tu, r.fact AS quan_he, m.name AS den
|
||||
```
|
||||
|
||||
**Chỉ xem fact CÒN hiệu lực (bỏ fact đã bị vô hiệu hóa / expired):**
|
||||
```cypher
|
||||
MATCH (n:Entity {group_id: 'mirofish_8770c4041fb141d3'})-[r:RELATES_TO]->(m:Entity)
|
||||
WHERE r.expired_at IS NULL
|
||||
RETURN n.name AS tu, r.fact AS quan_he, m.name AS den
|
||||
```
|
||||
|
||||
**Giới hạn cho nhẹ (graph lớn):** thêm `LIMIT 50` vào cuối query.
|
||||
|
||||
---
|
||||
|
||||
## Mẹo
|
||||
|
||||
- Loại node: `Entity` (thực thể có tên), `Episodic` (chunk text gốc), quan hệ chính là `RELATES_TO`.
|
||||
- Trong Browser, kết quả có 2 nút chuyển: **Graph view** (hình tròn) và **Table view** (bảng) — ở cạnh trái khung kết quả.
|
||||
- Mỗi `:RELATES_TO` có field `fact` (câu mô tả quan hệ), `valid_at` / `invalid_at` / `expired_at` (thông tin thời gian — temporal graph).
|
||||
|
|
@ -10,7 +10,7 @@ full_content = False
|
|||
# start_time = 2026-02-09
|
||||
# end_time = 2026-02-12
|
||||
start_time = 2026-04-13
|
||||
end_time = 2026-04-14
|
||||
end_time = 2026-04-16
|
||||
|
||||
# ─── Simulation settings ──────────────────────────────────────────────────────
|
||||
# What the simulation should analyse / predict (sent to the LLM)
|
||||
|
|
|
|||
Loading…
Reference in New Issue