666ghj commited on
Commit
2e359a3
ยท
1 Parent(s): 6ed8fd8

Introduce Project ID for context management, finalizing the stateful API pipeline from file submission to graph construction.

Browse files
.env.example ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Zep Cloud API Key
2
+ # ไปŽ https://app.getzep.com ่Žทๅ–
3
+ ZEP_API_KEY=
4
+
5
+ # LLM API Key
6
+ LLM_API_KEY=
7
+ LLM_BASE_URL=
8
+ LLM_MODEL_NAME=
.gitignore CHANGED
@@ -11,6 +11,7 @@
11
  .env.development.local
12
  .env.test.local
13
  .env.production.local
 
14
 
15
  .vscode
16
  .idea
@@ -19,4 +20,11 @@
19
  .cursor/
20
 
21
  mydoc/
22
- mytest/
 
 
 
 
 
 
 
 
11
  .env.development.local
12
  .env.test.local
13
  .env.production.local
14
+ __pycache__/
15
 
16
  .vscode
17
  .idea
 
20
  .cursor/
21
 
22
  mydoc/
23
+ mytest/
24
+
25
+ # ๆ—ฅๅฟ—ๆ–‡ไปถ
26
+ backend/logs/
27
+ *.log
28
+
29
+ # ไธŠไผ ๆ–‡ไปถ
30
+ backend/uploads/
backend/README.md ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MiroFish Backend
2
+
3
+ ็คพไผš่ˆ†่ฎบๆจกๆ‹Ÿ็ณป็ปŸๅŽ็ซฏๆœๅŠก๏ผŒๅŸบไบŽFlaskๆก†ๆžถใ€‚
4
+
5
+ ## ้กน็›ฎ็ป“ๆž„
6
+
7
+ ```
8
+ backend/
9
+ โ”œโ”€โ”€ app/
10
+ โ”‚ โ”œโ”€โ”€ __init__.py # Flaskๅบ”็”จๅทฅๅŽ‚
11
+ โ”‚ โ”œโ”€โ”€ config.py # ้…็ฝฎ็ฎก็†
12
+ โ”‚ โ”œโ”€โ”€ api/ # API่ทฏ็”ฑ
13
+ โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py
14
+ โ”‚ โ”‚ โ””โ”€โ”€ graph.py # ๅ›พ่ฐฑ็›ธๅ…ณๆŽฅๅฃ
15
+ โ”‚ โ”œโ”€โ”€ services/ # ไธšๅŠก้€ป่พ‘ๅฑ‚
16
+ โ”‚ โ”‚ โ”œโ”€โ”€ ontology_generator.py # ๆœฌไฝ“็”ŸๆˆๆœๅŠก
17
+ โ”‚ โ”‚ โ”œโ”€โ”€ graph_builder.py # ๅ›พ่ฐฑๆž„ๅปบๆœๅŠก
18
+ โ”‚ โ”‚ โ””โ”€โ”€ text_processor.py # ๆ–‡ๆœฌๅค„็†ๆœๅŠก
19
+ โ”‚ โ”œโ”€โ”€ models/ # ๆ•ฐๆฎๆจกๅž‹
20
+ โ”‚ โ”‚ โ”œโ”€โ”€ task.py # ไปปๅŠก็Šถๆ€็ฎก็†
21
+ โ”‚ โ”‚ โ””โ”€โ”€ project.py # ้กน็›ฎไธŠไธ‹ๆ–‡็ฎก็†
22
+ โ”‚ โ””โ”€โ”€ utils/ # ๅทฅๅ…ทๆจกๅ—
23
+ โ”‚ โ”œโ”€โ”€ file_parser.py # ๆ–‡ไปถ่งฃๆž
24
+ โ”‚ โ””โ”€โ”€ llm_client.py # LLMๅฎขๆˆท็ซฏ
25
+ โ”œโ”€โ”€ requirements.txt
26
+ โ””โ”€โ”€ run.py # ๅฏๅŠจๅ…ฅๅฃ
27
+ ```
28
+
29
+ ## ๅฎ‰่ฃ…
30
+
31
+ ```bash
32
+ conda activate MiroFish
33
+ cd backend
34
+ pip install -r requirements.txt
35
+ ```
36
+
37
+ ## ้…็ฝฎ
38
+
39
+ ๅœจ้กน็›ฎๆ น็›ฎๅฝ• `MiroFish/.env` ไธญ้…็ฝฎ๏ผš
40
+
41
+ ```bash
42
+ # LLM้…็ฝฎ
43
+ LLM_API_KEY=your-llm-api-key
44
+ LLM_BASE_URL=https://openrouter.ai/api/v1
45
+ LLM_MODEL_NAME=gpt-4o-mini
46
+
47
+ # Zep้…็ฝฎ
48
+ ZEP_API_KEY=your-zep-api-key
49
+ ```
50
+
51
+ ## ๅฏๅŠจๆœๅŠก
52
+
53
+ ```bash
54
+ python run.py
55
+ ```
56
+
57
+ ๆœๅŠก้ป˜่ฎค่ฟ่กŒๅœจ http://localhost:5000
58
+
59
+ ---
60
+
61
+ ## APIๆŽฅๅฃ
62
+
63
+ ### ๆ ธๅฟƒๅทฅไฝœๆต็จ‹
64
+
65
+ ```
66
+ 1. ๅˆ›ๅปบ้กน็›ฎ๏ผˆๅฏ้€‰๏ผ‰
67
+ POST /api/graph/project/create
68
+
69
+ 2. ไธŠไผ ๆ–‡ไปถ + ็”Ÿๆˆๆœฌไฝ“๏ผˆๆŽฅๅฃ1๏ผ‰
70
+ POST /api/graph/ontology/generate
71
+ โ†’ ่ฟ”ๅ›ž project_id
72
+
73
+ 3. ๆž„ๅปบๅ›พ่ฐฑ๏ผˆๆŽฅๅฃ2๏ผ‰
74
+ POST /api/graph/build
75
+ โ†’ ไผ ๅ…ฅ project_id
76
+ โ†’ ่ฟ”ๅ›ž task_id
77
+
78
+ 4. ๆŸฅ่ฏขไปปๅŠก่ฟ›ๅบฆ
79
+ GET /api/graph/task/{task_id}
80
+
81
+ 5. ่Žทๅ–ๅ›พ่ฐฑๆ•ฐๆฎ
82
+ GET /api/graph/data/{graph_id}
83
+ ```
84
+
85
+ ---
86
+
87
+ ### ๆŽฅๅฃ1๏ผš็”Ÿๆˆๆœฌไฝ“ๅฎšไน‰
88
+
89
+ **POST** `/api/graph/ontology/generate`
90
+
91
+ ไธŠไผ ๆ–‡ๆกฃ๏ผŒๅˆ†ๆž็”Ÿๆˆ้€‚ๅˆ็คพไผšๆจกๆ‹Ÿ็š„ๅฎžไฝ“ๅ’Œๅ…ณ็ณป็ฑปๅž‹ๅฎšไน‰ใ€‚
92
+
93
+ **่ฏทๆฑ‚๏ผˆform-data๏ผ‰๏ผš**
94
+
95
+ | ๅญ—ๆฎต | ็ฑปๅž‹ | ๅฟ…ๅกซ | ่ฏดๆ˜Ž |
96
+ |------|------|------|------|
97
+ | `files` | File | โœ… | PDF/MD/TXTๆ–‡ไปถ๏ผŒๅฏๅคšไธช |
98
+ | `simulation_requirement` | Text | โœ… | ๆจกๆ‹Ÿ้œ€ๆฑ‚ๆ่ฟฐ |
99
+ | `project_name` | Text | โŒ | ้กน็›ฎๅ็งฐ |
100
+ | `additional_context` | Text | โŒ | ้ขๅค–่ฏดๆ˜Ž |
101
+
102
+ **ๅ“ๅบ”็คบไพ‹๏ผš**
103
+ ```json
104
+ {
105
+ "success": true,
106
+ "data": {
107
+ "project_id": "proj_abc123def456",
108
+ "project_name": "ๆญฆๆฑ‰ๅคงๅญฆ่ˆ†ๆƒ…ๅˆ†ๆž",
109
+ "ontology": {
110
+ "entity_types": [
111
+ {
112
+ "name": "Person",
113
+ "description": "Individuals who can express opinions",
114
+ "attributes": [...]
115
+ }
116
+ ],
117
+ "edge_types": [
118
+ {
119
+ "name": "AFFILIATED_WITH",
120
+ "description": "Indicates affiliation",
121
+ "source_targets": [...]
122
+ }
123
+ ]
124
+ },
125
+ "analysis_summary": "ๅˆ†ๆž่ฏดๆ˜Ž...",
126
+ "files": [
127
+ {"filename": "ๆŠฅๅ‘Š.pdf", "size": 123456}
128
+ ],
129
+ "total_text_length": 20833
130
+ }
131
+ }
132
+ ```
133
+
134
+ ---
135
+
136
+ ### ๆŽฅๅฃ2๏ผšๆž„ๅปบๅ›พ่ฐฑ
137
+
138
+ **POST** `/api/graph/build`
139
+
140
+ ๆ นๆฎ `project_id` ๆž„ๅปบZep็Ÿฅ่ฏ†ๅ›พ่ฐฑ๏ผˆๅผ‚ๆญฅไปปๅŠก๏ผ‰ใ€‚
141
+
142
+ **่ฏทๆฑ‚๏ผˆJSON๏ผ‰๏ผš**
143
+ ```json
144
+ {
145
+ "project_id": "proj_abc123def456",
146
+ "graph_name": "ๅ›พ่ฐฑๅ็งฐ",
147
+ "chunk_size": 500,
148
+ "chunk_overlap": 50
149
+ }
150
+ ```
151
+
152
+ | ๅญ—ๆฎต | ็ฑปๅž‹ | ๅฟ…ๅกซ | ่ฏดๆ˜Ž |
153
+ |------|------|------|------|
154
+ | `project_id` | string | โœ… | ๆฅ่‡ชๆŽฅๅฃ1็š„่ฟ”ๅ›ž |
155
+ | `graph_name` | string | โŒ | ๅ›พ่ฐฑๅ็งฐ |
156
+ | `chunk_size` | int | โŒ | ๆ–‡ๆœฌๅ—ๅคงๅฐ๏ผŒ้ป˜่ฎค500 |
157
+ | `chunk_overlap` | int | โŒ | ๅ—้‡ๅ ๅญ—็ฌฆ๏ผŒ้ป˜่ฎค50 |
158
+
159
+ **ๅ“ๅบ”๏ผš**
160
+ ```json
161
+ {
162
+ "success": true,
163
+ "data": {
164
+ "project_id": "proj_abc123def456",
165
+ "task_id": "task_xyz789",
166
+ "message": "ๅ›พ่ฐฑๆž„ๅปบไปปๅŠกๅทฒๅฏๅŠจ"
167
+ }
168
+ }
169
+ ```
170
+
171
+ ---
172
+
173
+ ### ไปปๅŠก็Šถๆ€ๆŸฅ่ฏข
174
+
175
+ **GET** `/api/graph/task/{task_id}`
176
+
177
+ ```json
178
+ {
179
+ "success": true,
180
+ "data": {
181
+ "task_id": "task_xyz789",
182
+ "status": "processing",
183
+ "progress": 45,
184
+ "message": "ๆทปๅŠ ๆ–‡ๆœฌๅ— (15/30)...",
185
+ "result": null
186
+ }
187
+ }
188
+ ```
189
+
190
+ **็Šถๆ€ๅ€ผ๏ผš**
191
+ - `pending` - ็ญ‰ๅพ…ไธญ
192
+ - `processing` - ๅค„็†ไธญ
193
+ - `completed` - ๅทฒๅฎŒๆˆ
194
+ - `failed` - ๅคฑ่ดฅ
195
+
196
+ ---
197
+
198
+ ### ้กน็›ฎ็ฎก็†ๆŽฅๅฃ
199
+
200
+ | ๆ–นๆณ• | ่ทฏๅพ„ | ่ฏดๆ˜Ž |
201
+ |------|------|------|
202
+ | POST | `/api/graph/project/create` | ๅˆ›ๅปบ้กน็›ฎ |
203
+ | GET | `/api/graph/project/{project_id}` | ่Žทๅ–้กน็›ฎ่ฏฆๆƒ… |
204
+ | GET | `/api/graph/project/list` | ๅˆ—ๅ‡บๆ‰€ๆœ‰้กน็›ฎ |
205
+ | DELETE | `/api/graph/project/{project_id}` | ๅˆ ้™ค้กน็›ฎ |
206
+
207
+ ---
208
+
209
+ ### ๅ›พ่ฐฑๆ•ฐๆฎๆŽฅๅฃ
210
+
211
+ | ๆ–นๆณ• | ่ทฏๅพ„ | ่ฏดๆ˜Ž |
212
+ |------|------|------|
213
+ | GET | `/api/graph/data/{graph_id}` | ่Žทๅ–ๅ›พ่ฐฑ่Š‚็‚นๅ’Œ่พน |
214
+ | DELETE | `/api/graph/delete/{graph_id}` | ๅˆ ้™คZepๅ›พ่ฐฑ |
215
+
216
+ ---
217
+
218
+ ## ๅฎžไฝ“่ฎพ่ฎกๅŽŸๅˆ™
219
+
220
+ ๆœฌ็ณป็ปŸไธ“ไธบ็คพไผš่ˆ†่ฎบๆจกๆ‹Ÿ่ฎพ่ฎก๏ผŒๅฎžไฝ“ๅฟ…้กปๆ˜ฏ๏ผš
221
+
222
+ **โœ… ๅฏไปฅๆ˜ฏ๏ผš**
223
+ - ๅ…ทไฝ“็š„ไธชไบบ๏ผˆๆœ‰ๅๆœ‰ๅง“๏ผ‰
224
+ - ๆณจๅ†Œ็š„ๅ…ฌๅธใ€็ป„็ป‡ใ€ๆœบๆž„
225
+ - ๅช’ไฝ“ๆœบๆž„
226
+ - ๆ”ฟๅบœ้ƒจ้—จ
227
+
228
+ **โŒ ไธๅฏไปฅๆ˜ฏ๏ผš**
229
+ - ๆŠฝ่ฑกๆฆ‚ๅฟต๏ผˆๅฆ‚"ๆŠ€ๆœฏ"ใ€"ๅˆ›ๆ–ฐ"๏ผ‰
230
+ - ๆƒ…็ปชใ€่ง‚็‚นใ€่ถ‹ๅŠฟ
231
+ - ๆณ›ๆŒ‡็š„็พคไฝ“๏ผˆๅฆ‚"็”จๆˆท"ใ€"ๆถˆ่ดน่€…"๏ผ‰
232
+
233
+ ่ฟ™ๆ˜ฏๅ› ไธบๅŽ็ปญ้œ€่ฆๆจกๆ‹Ÿๅ„ๅฎžไฝ“ๅฏน่ˆ†่ฎบ็š„ๅๅบ”ๅ’Œไผ ๆ’ญ๏ผŒๆŠฝ่ฑกๆฆ‚ๅฟตๆ— ๆณ•ๅ‚ไธŽ่ฟ™็งๆจกๆ‹Ÿใ€‚
234
+
235
+ ---
236
+
237
+ ## ้กน็›ฎ็Šถๆ€ๆต่ฝฌ
238
+
239
+ ```
240
+ created โ†’ ontology_generated โ†’ graph_building โ†’ graph_completed
241
+ โ†“
242
+ failed
243
+ ```
backend/app/__init__.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MiroFish Backend - Flaskๅบ”็”จๅทฅๅŽ‚
3
+ """
4
+
5
+ from flask import Flask, request
6
+ from flask_cors import CORS
7
+
8
+ from .config import Config
9
+ from .utils.logger import setup_logger, get_logger
10
+
11
+
12
+ def create_app(config_class=Config):
13
+ """Flaskๅบ”็”จๅทฅๅŽ‚ๅ‡ฝๆ•ฐ"""
14
+ app = Flask(__name__)
15
+ app.config.from_object(config_class)
16
+
17
+ # ่ฎพ็ฝฎๆ—ฅๅฟ—
18
+ logger = setup_logger('mirofish')
19
+ logger.info("=" * 50)
20
+ logger.info("MiroFish Backend ๅฏๅŠจไธญ...")
21
+ logger.info("=" * 50)
22
+
23
+ # ๅฏ็”จCORS
24
+ CORS(app, resources={r"/api/*": {"origins": "*"}})
25
+
26
+ # ่ฏทๆฑ‚ๆ—ฅๅฟ—ไธญ้—ดไปถ
27
+ @app.before_request
28
+ def log_request():
29
+ logger = get_logger('mirofish.request')
30
+ logger.debug(f"่ฏทๆฑ‚: {request.method} {request.path}")
31
+ if request.content_type and 'json' in request.content_type:
32
+ logger.debug(f"่ฏทๆฑ‚ไฝ“: {request.get_json(silent=True)}")
33
+
34
+ @app.after_request
35
+ def log_response(response):
36
+ logger = get_logger('mirofish.request')
37
+ logger.debug(f"ๅ“ๅบ”: {response.status_code}")
38
+ return response
39
+
40
+ # ๆณจๅ†Œ่“ๅ›พ
41
+ from .api import graph_bp
42
+ app.register_blueprint(graph_bp, url_prefix='/api/graph')
43
+
44
+ # ๅฅๅบทๆฃ€ๆŸฅ
45
+ @app.route('/health')
46
+ def health():
47
+ return {'status': 'ok', 'service': 'MiroFish Backend'}
48
+
49
+ logger.info("MiroFish Backend ๅฏๅŠจๅฎŒๆˆ")
50
+
51
+ return app
52
+
backend/app/api/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API่ทฏ็”ฑๆจกๅ—
3
+ """
4
+
5
+ from flask import Blueprint
6
+
7
+ graph_bp = Blueprint('graph', __name__)
8
+
9
+ from . import graph # noqa: E402, F401
10
+
backend/app/api/graph.py ADDED
@@ -0,0 +1,657 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๅ›พ่ฐฑ็›ธๅ…ณAPI่ทฏ็”ฑ
3
+ ้‡‡็”จ้กน็›ฎไธŠไธ‹ๆ–‡ๆœบๅˆถ๏ผŒๆœๅŠก็ซฏๆŒไน…ๅŒ–็Šถๆ€
4
+ """
5
+
6
+ import os
7
+ import traceback
8
+ import threading
9
+ from flask import request, jsonify
10
+
11
+ from . import graph_bp
12
+ from ..config import Config
13
+ from ..services.ontology_generator import OntologyGenerator
14
+ from ..services.graph_builder import GraphBuilderService
15
+ from ..services.text_processor import TextProcessor
16
+ from ..utils.file_parser import FileParser
17
+ from ..utils.logger import get_logger
18
+ from ..models.task import TaskManager, TaskStatus
19
+ from ..models.project import ProjectManager, ProjectStatus
20
+
21
+ # ่Žทๅ–ๆ—ฅๅฟ—ๅ™จ
22
+ logger = get_logger('mirofish.api')
23
+
24
+
25
+ def allowed_file(filename: str) -> bool:
26
+ """ๆฃ€ๆŸฅๆ–‡ไปถๆ‰ฉๅฑ•ๅๆ˜ฏๅฆๅ…่ฎธ"""
27
+ if not filename or '.' not in filename:
28
+ return False
29
+ ext = os.path.splitext(filename)[1].lower().lstrip('.')
30
+ return ext in Config.ALLOWED_EXTENSIONS
31
+
32
+
33
+ # ============== ้กน็›ฎ็ฎก็†ๆŽฅๅฃ ==============
34
+
35
+ @graph_bp.route('/project/create', methods=['POST'])
36
+ def create_project():
37
+ """
38
+ ๅˆ›ๅปบๆ–ฐ้กน็›ฎ
39
+
40
+ ่ฏทๆฑ‚๏ผˆJSON๏ผ‰๏ผš
41
+ {
42
+ "name": "้กน็›ฎๅ็งฐ" // ๅฏ้€‰
43
+ }
44
+
45
+ ่ฟ”ๅ›ž๏ผš
46
+ {
47
+ "success": true,
48
+ "data": {
49
+ "project_id": "proj_xxxx",
50
+ "name": "...",
51
+ "status": "created",
52
+ ...
53
+ }
54
+ }
55
+ """
56
+ try:
57
+ data = request.get_json() or {}
58
+ name = data.get('name', 'Unnamed Project')
59
+
60
+ project = ProjectManager.create_project(name=name)
61
+
62
+ return jsonify({
63
+ "success": True,
64
+ "data": project.to_dict()
65
+ })
66
+
67
+ except Exception as e:
68
+ return jsonify({
69
+ "success": False,
70
+ "error": str(e),
71
+ "traceback": traceback.format_exc()
72
+ }), 500
73
+
74
+
75
+ @graph_bp.route('/project/<project_id>', methods=['GET'])
76
+ def get_project(project_id: str):
77
+ """
78
+ ่Žทๅ–้กน็›ฎ่ฏฆๆƒ…
79
+ """
80
+ project = ProjectManager.get_project(project_id)
81
+
82
+ if not project:
83
+ return jsonify({
84
+ "success": False,
85
+ "error": f"้กน็›ฎไธๅญ˜ๅœจ: {project_id}"
86
+ }), 404
87
+
88
+ return jsonify({
89
+ "success": True,
90
+ "data": project.to_dict()
91
+ })
92
+
93
+
94
+ @graph_bp.route('/project/list', methods=['GET'])
95
+ def list_projects():
96
+ """
97
+ ๅˆ—ๅ‡บๆ‰€ๆœ‰้กน็›ฎ
98
+ """
99
+ limit = request.args.get('limit', 50, type=int)
100
+ projects = ProjectManager.list_projects(limit=limit)
101
+
102
+ return jsonify({
103
+ "success": True,
104
+ "data": [p.to_dict() for p in projects],
105
+ "count": len(projects)
106
+ })
107
+
108
+
109
+ @graph_bp.route('/project/<project_id>', methods=['DELETE'])
110
+ def delete_project(project_id: str):
111
+ """
112
+ ๅˆ ้™ค้กน็›ฎ
113
+ """
114
+ success = ProjectManager.delete_project(project_id)
115
+
116
+ if not success:
117
+ return jsonify({
118
+ "success": False,
119
+ "error": f"้กน็›ฎไธๅญ˜ๅœจๆˆ–ๅˆ ้™คๅคฑ่ดฅ: {project_id}"
120
+ }), 404
121
+
122
+ return jsonify({
123
+ "success": True,
124
+ "message": f"้กน็›ฎๅทฒๅˆ ้™ค: {project_id}"
125
+ })
126
+
127
+
128
+ @graph_bp.route('/project/<project_id>/reset', methods=['POST'])
129
+ def reset_project(project_id: str):
130
+ """
131
+ ้‡็ฝฎ้กน็›ฎ็Šถๆ€๏ผˆ็”จไบŽ้‡ๆ–ฐๆž„ๅปบๅ›พ่ฐฑ๏ผ‰
132
+ """
133
+ project = ProjectManager.get_project(project_id)
134
+
135
+ if not project:
136
+ return jsonify({
137
+ "success": False,
138
+ "error": f"้กน็›ฎไธๅญ˜ๅœจ: {project_id}"
139
+ }), 404
140
+
141
+ # ้‡็ฝฎๅˆฐๆœฌไฝ“ๅทฒ็”Ÿๆˆ็Šถๆ€
142
+ if project.ontology:
143
+ project.status = ProjectStatus.ONTOLOGY_GENERATED
144
+ else:
145
+ project.status = ProjectStatus.CREATED
146
+
147
+ project.graph_id = None
148
+ project.graph_build_task_id = None
149
+ project.error = None
150
+ ProjectManager.save_project(project)
151
+
152
+ return jsonify({
153
+ "success": True,
154
+ "message": f"้กน็›ฎๅทฒ้‡็ฝฎ: {project_id}",
155
+ "data": project.to_dict()
156
+ })
157
+
158
+
159
+ # ============== ๆŽฅๅฃ1๏ผšไธŠไผ ๆ–‡ไปถๅนถ็”Ÿๆˆๆœฌไฝ“ ==============
160
+
161
+ @graph_bp.route('/ontology/generate', methods=['POST'])
162
+ def generate_ontology():
163
+ """
164
+ ๆŽฅๅฃ1๏ผšไธŠไผ ๆ–‡ไปถ๏ผŒๅˆ†ๆž็”Ÿๆˆๆœฌไฝ“ๅฎšไน‰
165
+
166
+ ่ฏทๆฑ‚ๆ–นๅผ๏ผšmultipart/form-data
167
+
168
+ ๅ‚ๆ•ฐ๏ผš
169
+ files: ไธŠไผ ็š„ๆ–‡ไปถ๏ผˆPDF/MD/TXT๏ผ‰๏ผŒๅฏๅคšไธช
170
+ simulation_requirement: ๆจกๆ‹Ÿ้œ€ๆฑ‚ๆ่ฟฐ๏ผˆๅฟ…ๅกซ๏ผ‰
171
+ project_name: ้กน็›ฎๅ็งฐ๏ผˆๅฏ้€‰๏ผ‰
172
+ additional_context: ้ขๅค–่ฏดๆ˜Ž๏ผˆๅฏ้€‰๏ผ‰
173
+
174
+ ่ฟ”ๅ›ž๏ผš
175
+ {
176
+ "success": true,
177
+ "data": {
178
+ "project_id": "proj_xxxx",
179
+ "ontology": {
180
+ "entity_types": [...],
181
+ "edge_types": [...],
182
+ "analysis_summary": "..."
183
+ },
184
+ "files": [...],
185
+ "total_text_length": 12345
186
+ }
187
+ }
188
+ """
189
+ try:
190
+ logger.info("=== ๅผ€ๅง‹็”Ÿๆˆๆœฌไฝ“ๅฎš๏ฟฝ๏ฟฝ ===")
191
+
192
+ # ่Žทๅ–ๅ‚ๆ•ฐ
193
+ simulation_requirement = request.form.get('simulation_requirement', '')
194
+ project_name = request.form.get('project_name', 'Unnamed Project')
195
+ additional_context = request.form.get('additional_context', '')
196
+
197
+ logger.debug(f"้กน็›ฎๅ็งฐ: {project_name}")
198
+ logger.debug(f"ๆจกๆ‹Ÿ้œ€ๆฑ‚: {simulation_requirement[:100]}...")
199
+
200
+ if not simulation_requirement:
201
+ return jsonify({
202
+ "success": False,
203
+ "error": "่ฏทๆไพ›ๆจกๆ‹Ÿ้œ€ๆฑ‚ๆ่ฟฐ (simulation_requirement)"
204
+ }), 400
205
+
206
+ # ่Žทๅ–ไธŠไผ ็š„ๆ–‡ไปถ
207
+ uploaded_files = request.files.getlist('files')
208
+ if not uploaded_files or all(not f.filename for f in uploaded_files):
209
+ return jsonify({
210
+ "success": False,
211
+ "error": "่ฏท่‡ณๅฐ‘ไธŠไผ ไธ€ไธชๆ–‡ๆกฃๆ–‡ไปถ"
212
+ }), 400
213
+
214
+ # ๅˆ›ๅปบ้กน็›ฎ
215
+ project = ProjectManager.create_project(name=project_name)
216
+ project.simulation_requirement = simulation_requirement
217
+ logger.info(f"ๅˆ›ๅปบ้กน็›ฎ: {project.project_id}")
218
+
219
+ # ไฟๅญ˜ๆ–‡ไปถๅนถๆๅ–ๆ–‡ๆœฌ
220
+ document_texts = []
221
+ all_text = ""
222
+
223
+ for file in uploaded_files:
224
+ if file and file.filename and allowed_file(file.filename):
225
+ # ไฟๅญ˜ๆ–‡ไปถๅˆฐ้กน็›ฎ็›ฎๅฝ•
226
+ file_info = ProjectManager.save_file_to_project(
227
+ project.project_id,
228
+ file,
229
+ file.filename
230
+ )
231
+ project.files.append({
232
+ "filename": file_info["original_filename"],
233
+ "size": file_info["size"]
234
+ })
235
+
236
+ # ๆๅ–ๆ–‡ๆœฌ
237
+ text = FileParser.extract_text(file_info["path"])
238
+ text = TextProcessor.preprocess_text(text)
239
+ document_texts.append(text)
240
+ all_text += f"\n\n=== {file_info['original_filename']} ===\n{text}"
241
+
242
+ if not document_texts:
243
+ ProjectManager.delete_project(project.project_id)
244
+ return jsonify({
245
+ "success": False,
246
+ "error": "ๆฒกๆœ‰ๆˆๅŠŸๅค„็†ไปปไฝ•ๆ–‡ๆกฃ๏ผŒ่ฏทๆฃ€ๆŸฅๆ–‡ไปถๆ ผๅผ"
247
+ }), 400
248
+
249
+ # ไฟๅญ˜ๆๅ–็š„ๆ–‡ๆœฌ
250
+ project.total_text_length = len(all_text)
251
+ ProjectManager.save_extracted_text(project.project_id, all_text)
252
+ logger.info(f"ๆ–‡ๆœฌๆๅ–ๅฎŒๆˆ๏ผŒๅ…ฑ {len(all_text)} ๅญ—็ฌฆ")
253
+
254
+ # ็”Ÿๆˆๆœฌไฝ“
255
+ logger.info("่ฐƒ็”จ LLM ็”Ÿๆˆๆœฌไฝ“ๅฎšไน‰...")
256
+ generator = OntologyGenerator()
257
+ ontology = generator.generate(
258
+ document_texts=document_texts,
259
+ simulation_requirement=simulation_requirement,
260
+ additional_context=additional_context if additional_context else None
261
+ )
262
+
263
+ # ไฟๅญ˜ๆœฌไฝ“ๅˆฐ้กน็›ฎ
264
+ entity_count = len(ontology.get("entity_types", []))
265
+ edge_count = len(ontology.get("edge_types", []))
266
+ logger.info(f"ๆœฌไฝ“็”ŸๆˆๅฎŒๆˆ: {entity_count} ไธชๅฎžไฝ“็ฑปๅž‹, {edge_count} ไธชๅ…ณ็ณป็ฑปๅž‹")
267
+
268
+ project.ontology = {
269
+ "entity_types": ontology.get("entity_types", []),
270
+ "edge_types": ontology.get("edge_types", [])
271
+ }
272
+ project.analysis_summary = ontology.get("analysis_summary", "")
273
+ project.status = ProjectStatus.ONTOLOGY_GENERATED
274
+ ProjectManager.save_project(project)
275
+ logger.info(f"=== ๆœฌไฝ“็”ŸๆˆๅฎŒๆˆ === ้กน็›ฎID: {project.project_id}")
276
+
277
+ return jsonify({
278
+ "success": True,
279
+ "data": {
280
+ "project_id": project.project_id,
281
+ "project_name": project.name,
282
+ "ontology": project.ontology,
283
+ "analysis_summary": project.analysis_summary,
284
+ "files": project.files,
285
+ "total_text_length": project.total_text_length
286
+ }
287
+ })
288
+
289
+ except Exception as e:
290
+ return jsonify({
291
+ "success": False,
292
+ "error": str(e),
293
+ "traceback": traceback.format_exc()
294
+ }), 500
295
+
296
+
297
+ # ============== ๆŽฅๅฃ2๏ผšๆž„ๅปบๅ›พ่ฐฑ ==============
298
+
299
+ @graph_bp.route('/build', methods=['POST'])
300
+ def build_graph():
301
+ """
302
+ ๆŽฅๅฃ2๏ผšๆ นๆฎproject_idๆž„ๅปบๅ›พ่ฐฑ
303
+
304
+ ่ฏทๆฑ‚๏ผˆJSON๏ผ‰๏ผš
305
+ {
306
+ "project_id": "proj_xxxx", // ๅฟ…ๅกซ๏ผŒๆฅ่‡ชๆŽฅๅฃ1
307
+ "graph_name": "ๅ›พ่ฐฑๅ็งฐ", // ๅฏ้€‰
308
+ "chunk_size": 500, // ๅฏ้€‰๏ผŒ้ป˜่ฎค500
309
+ "chunk_overlap": 50 // ๅฏ้€‰๏ผŒ้ป˜่ฎค50
310
+ }
311
+
312
+ ่ฟ”ๅ›ž๏ผš
313
+ {
314
+ "success": true,
315
+ "data": {
316
+ "project_id": "proj_xxxx",
317
+ "task_id": "task_xxxx",
318
+ "message": "ๅ›พ่ฐฑๆž„ๅปบไปปๅŠกๅทฒๅฏๅŠจ"
319
+ }
320
+ }
321
+ """
322
+ try:
323
+ logger.info("=== ๅผ€ๅง‹ๆž„๏ฟฝ๏ฟฝๅ›พ่ฐฑ ===")
324
+
325
+ # ๆฃ€ๆŸฅ้…็ฝฎ
326
+ errors = []
327
+ if not Config.ZEP_API_KEY:
328
+ errors.append("ZEP_API_KEYๆœช้…็ฝฎ")
329
+ if errors:
330
+ logger.error(f"้…็ฝฎ้”™่ฏฏ: {errors}")
331
+ return jsonify({
332
+ "success": False,
333
+ "error": "้…็ฝฎ้”™่ฏฏ: " + "; ".join(errors)
334
+ }), 500
335
+
336
+ # ่งฃๆž่ฏทๆฑ‚
337
+ data = request.get_json() or {}
338
+ project_id = data.get('project_id')
339
+ logger.debug(f"่ฏทๆฑ‚ๅ‚ๆ•ฐ: project_id={project_id}")
340
+
341
+ if not project_id:
342
+ return jsonify({
343
+ "success": False,
344
+ "error": "่ฏทๆไพ› project_id"
345
+ }), 400
346
+
347
+ # ่Žทๅ–้กน็›ฎ
348
+ project = ProjectManager.get_project(project_id)
349
+ if not project:
350
+ return jsonify({
351
+ "success": False,
352
+ "error": f"้กน็›ฎไธๅญ˜ๅœจ: {project_id}"
353
+ }), 404
354
+
355
+ # ๆฃ€ๆŸฅ้กน็›ฎ็Šถๆ€
356
+ force = data.get('force', False) # ๅผบๅˆถ้‡ๆ–ฐๆž„ๅปบ
357
+
358
+ if project.status == ProjectStatus.CREATED:
359
+ return jsonify({
360
+ "success": False,
361
+ "error": "้กน็›ฎๅฐšๆœช็”Ÿๆˆๆœฌไฝ“๏ผŒ่ฏทๅ…ˆ่ฐƒ็”จ /ontology/generate"
362
+ }), 400
363
+
364
+ if project.status == ProjectStatus.GRAPH_BUILDING and not force:
365
+ return jsonify({
366
+ "success": False,
367
+ "error": "ๅ›พ่ฐฑๆญฃๅœจๆž„ๅปบไธญ๏ผŒ่ฏทๅ‹ฟ้‡ๅคๆไบคใ€‚ๅฆ‚้œ€ๅผบๅˆถ้‡ๅปบ๏ผŒ่ฏทๆทปๅŠ  force: true",
368
+ "task_id": project.graph_build_task_id
369
+ }), 400
370
+
371
+ # ๅฆ‚ๆžœๅผบๅˆถ้‡ๅปบ๏ผŒ้‡็ฝฎ็Šถๆ€
372
+ if force and project.status in [ProjectStatus.GRAPH_BUILDING, ProjectStatus.FAILED, ProjectStatus.GRAPH_COMPLETED]:
373
+ project.status = ProjectStatus.ONTOLOGY_GENERATED
374
+ project.graph_id = None
375
+ project.graph_build_task_id = None
376
+ project.error = None
377
+
378
+ # ่Žทๅ–้…็ฝฎ
379
+ graph_name = data.get('graph_name', project.name or 'MiroFish Graph')
380
+ chunk_size = data.get('chunk_size', project.chunk_size or Config.DEFAULT_CHUNK_SIZE)
381
+ chunk_overlap = data.get('chunk_overlap', project.chunk_overlap or Config.DEFAULT_CHUNK_OVERLAP)
382
+
383
+ # ๆ›ดๆ–ฐ้กน็›ฎ้…็ฝฎ
384
+ project.chunk_size = chunk_size
385
+ project.chunk_overlap = chunk_overlap
386
+
387
+ # ่Žทๅ–ๆๅ–็š„ๆ–‡ๆœฌ
388
+ text = ProjectManager.get_extracted_text(project_id)
389
+ if not text:
390
+ return jsonify({
391
+ "success": False,
392
+ "error": "ๆœชๆ‰พๅˆฐๆๅ–็š„ๆ–‡ๆœฌๅ†…ๅฎน"
393
+ }), 400
394
+
395
+ # ่Žทๅ–ๆœฌไฝ“
396
+ ontology = project.ontology
397
+ if not ontology:
398
+ return jsonify({
399
+ "success": False,
400
+ "error": "ๆœชๆ‰พๅˆฐๆœฌไฝ“ๅฎšไน‰"
401
+ }), 400
402
+
403
+ # ๅˆ›ๅปบๅผ‚ๆญฅไปปๅŠก
404
+ task_manager = TaskManager()
405
+ task_id = task_manager.create_task(f"ๆž„ๅปบๅ›พ่ฐฑ: {graph_name}")
406
+ logger.info(f"ๅˆ›ๅปบๅ›พ่ฐฑๆž„ๅปบไปปๅŠก: task_id={task_id}, project_id={project_id}")
407
+
408
+ # ๆ›ดๆ–ฐ้กน็›ฎ็Šถๆ€
409
+ project.status = ProjectStatus.GRAPH_BUILDING
410
+ project.graph_build_task_id = task_id
411
+ ProjectManager.save_project(project)
412
+
413
+ # ๅฏๅŠจๅŽๅฐไปปๅŠก
414
+ def build_task():
415
+ build_logger = get_logger('mirofish.build')
416
+ try:
417
+ build_logger.info(f"[{task_id}] ๅผ€ๅง‹ๆž„ๅปบๅ›พ่ฐฑ...")
418
+ task_manager.update_task(
419
+ task_id,
420
+ status=TaskStatus.PROCESSING,
421
+ message="ๅˆๅง‹ๅŒ–ๅ›พ่ฐฑๆž„ๅปบๆœๅŠก..."
422
+ )
423
+
424
+ # ๅˆ›ๅปบๅ›พ่ฐฑๆž„ๅปบๆœๅŠก
425
+ builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
426
+
427
+ # ๅˆ†ๅ—
428
+ task_manager.update_task(
429
+ task_id,
430
+ message="ๆ–‡ๆœฌๅˆ†ๅ—ไธญ...",
431
+ progress=5
432
+ )
433
+ chunks = TextProcessor.split_text(
434
+ text,
435
+ chunk_size=chunk_size,
436
+ overlap=chunk_overlap
437
+ )
438
+ total_chunks = len(chunks)
439
+
440
+ # ๅˆ›ๅปบๅ›พ่ฐฑ
441
+ task_manager.update_task(
442
+ task_id,
443
+ message="ๅˆ›ๅปบZepๅ›พ่ฐฑ...",
444
+ progress=10
445
+ )
446
+ graph_id = builder.create_graph(name=graph_name)
447
+
448
+ # ๆ›ดๆ–ฐ้กน็›ฎ็š„graph_id
449
+ project.graph_id = graph_id
450
+ ProjectManager.save_project(project)
451
+
452
+ # ่ฎพ็ฝฎๆœฌไฝ“
453
+ task_manager.update_task(
454
+ task_id,
455
+ message="่ฎพ็ฝฎๆœฌไฝ“ๅฎšไน‰...",
456
+ progress=15
457
+ )
458
+ builder.set_ontology(graph_id, ontology)
459
+
460
+ # ๆทปๅŠ ๆ–‡ๆœฌ๏ผˆprogress_callback ็ญพๅๆ˜ฏ (msg, progress_ratio)๏ผ‰
461
+ def add_progress_callback(msg, progress_ratio):
462
+ progress = 15 + int(progress_ratio * 40) # 15% - 55%
463
+ task_manager.update_task(
464
+ task_id,
465
+ message=msg,
466
+ progress=progress
467
+ )
468
+
469
+ task_manager.update_task(
470
+ task_id,
471
+ message=f"ๅผ€ๅง‹ๆทปๅŠ  {total_chunks} ไธชๆ–‡ๆœฌๅ—...",
472
+ progress=15
473
+ )
474
+
475
+ episode_uuids = builder.add_text_batches(
476
+ graph_id,
477
+ chunks,
478
+ batch_size=3,
479
+ progress_callback=add_progress_callback
480
+ )
481
+
482
+ # ็ญ‰ๅพ…Zepๅค„็†ๅฎŒๆˆ๏ผˆๆŸฅ่ฏขๆฏไธชepisode็š„processed็Šถๆ€๏ผ‰
483
+ task_manager.update_task(
484
+ task_id,
485
+ message="็ญ‰ๅพ…Zepๅค„็†ๆ•ฐๆฎ...",
486
+ progress=55
487
+ )
488
+
489
+ def wait_progress_callback(msg, progress_ratio):
490
+ progress = 55 + int(progress_ratio * 35) # 55% - 90%
491
+ task_manager.update_task(
492
+ task_id,
493
+ message=msg,
494
+ progress=progress
495
+ )
496
+
497
+ builder._wait_for_episodes(episode_uuids, wait_progress_callback)
498
+
499
+ # ่Žทๅ–ๅ›พ่ฐฑๆ•ฐๆฎ
500
+ task_manager.update_task(
501
+ task_id,
502
+ message="่Žทๅ–ๅ›พ่ฐฑๆ•ฐๆฎ...",
503
+ progress=95
504
+ )
505
+ graph_data = builder.get_graph_data(graph_id)
506
+
507
+ # ๆ›ดๆ–ฐ้กน็›ฎ็Šถๆ€
508
+ project.status = ProjectStatus.GRAPH_COMPLETED
509
+ ProjectManager.save_project(project)
510
+
511
+ node_count = graph_data.get("node_count", 0)
512
+ edge_count = graph_data.get("edge_count", 0)
513
+ build_logger.info(f"[{task_id}] ๅ›พ่ฐฑๆž„ๅปบๅฎŒๆˆ: graph_id={graph_id}, ่Š‚็‚น={node_count}, ่พน={edge_count}")
514
+
515
+ # ๅฎŒๆˆ
516
+ task_manager.update_task(
517
+ task_id,
518
+ status=TaskStatus.COMPLETED,
519
+ message="ๅ›พ่ฐฑๆž„ๅปบๅฎŒๆˆ",
520
+ progress=100,
521
+ result={
522
+ "project_id": project_id,
523
+ "graph_id": graph_id,
524
+ "node_count": node_count,
525
+ "edge_count": edge_count,
526
+ "chunk_count": total_chunks
527
+ }
528
+ )
529
+
530
+ except Exception as e:
531
+ # ๆ›ดๆ–ฐ้กน็›ฎ็Šถๆ€ไธบๅคฑ่ดฅ
532
+ build_logger.error(f"[{task_id}] ๅ›พ่ฐฑๆž„ๅปบๅคฑ่ดฅ: {str(e)}")
533
+ build_logger.debug(traceback.format_exc())
534
+
535
+ project.status = ProjectStatus.FAILED
536
+ project.error = str(e)
537
+ ProjectManager.save_project(project)
538
+
539
+ task_manager.update_task(
540
+ task_id,
541
+ status=TaskStatus.FAILED,
542
+ message=f"ๆž„ๅปบๅคฑ่ดฅ: {str(e)}",
543
+ error=traceback.format_exc()
544
+ )
545
+
546
+ # ๅฏๅŠจๅŽๅฐ็บฟ็จ‹
547
+ thread = threading.Thread(target=build_task, daemon=True)
548
+ thread.start()
549
+
550
+ return jsonify({
551
+ "success": True,
552
+ "data": {
553
+ "project_id": project_id,
554
+ "task_id": task_id,
555
+ "message": "ๅ›พ่ฐฑๆž„ๅปบไปปๅŠกๅทฒๅฏๅŠจ๏ผŒ่ฏท้€š่ฟ‡ /task/{task_id} ๆŸฅ่ฏข่ฟ›ๅบฆ"
556
+ }
557
+ })
558
+
559
+ except Exception as e:
560
+ return jsonify({
561
+ "success": False,
562
+ "error": str(e),
563
+ "traceback": traceback.format_exc()
564
+ }), 500
565
+
566
+
567
+ # ============== ไปปๅŠกๆŸฅ่ฏขๆŽฅๅฃ ==============
568
+
569
+ @graph_bp.route('/task/<task_id>', methods=['GET'])
570
+ def get_task(task_id: str):
571
+ """
572
+ ๆŸฅ่ฏขไปปๅŠก็Šถๆ€
573
+ """
574
+ task = TaskManager().get_task(task_id)
575
+
576
+ if not task:
577
+ return jsonify({
578
+ "success": False,
579
+ "error": f"ไปปๅŠกไธๅญ˜ๅœจ: {task_id}"
580
+ }), 404
581
+
582
+ return jsonify({
583
+ "success": True,
584
+ "data": task.to_dict()
585
+ })
586
+
587
+
588
+ @graph_bp.route('/tasks', methods=['GET'])
589
+ def list_tasks():
590
+ """
591
+ ๅˆ—ๅ‡บๆ‰€ๆœ‰ไปปๅŠก
592
+ """
593
+ tasks = TaskManager().list_tasks()
594
+
595
+ return jsonify({
596
+ "success": True,
597
+ "data": [t.to_dict() for t in tasks],
598
+ "count": len(tasks)
599
+ })
600
+
601
+
602
+ # ============== ๅ›พ่ฐฑๆ•ฐๆฎๆŽฅๅฃ ==============
603
+
604
+ @graph_bp.route('/data/<graph_id>', methods=['GET'])
605
+ def get_graph_data(graph_id: str):
606
+ """
607
+ ่Žทๅ–ๅ›พ่ฐฑๆ•ฐๆฎ๏ผˆ่Š‚็‚นๅ’Œ่พน๏ผ‰
608
+ """
609
+ try:
610
+ if not Config.ZEP_API_KEY:
611
+ return jsonify({
612
+ "success": False,
613
+ "error": "ZEP_API_KEYๆœช้…็ฝฎ"
614
+ }), 500
615
+
616
+ builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
617
+ graph_data = builder.get_graph_data(graph_id)
618
+
619
+ return jsonify({
620
+ "success": True,
621
+ "data": graph_data
622
+ })
623
+
624
+ except Exception as e:
625
+ return jsonify({
626
+ "success": False,
627
+ "error": str(e),
628
+ "traceback": traceback.format_exc()
629
+ }), 500
630
+
631
+
632
+ @graph_bp.route('/delete/<graph_id>', methods=['DELETE'])
633
+ def delete_graph(graph_id: str):
634
+ """
635
+ ๅˆ ้™คZepๅ›พ่ฐฑ
636
+ """
637
+ try:
638
+ if not Config.ZEP_API_KEY:
639
+ return jsonify({
640
+ "success": False,
641
+ "error": "ZEP_API_KEYๆœช้…็ฝฎ"
642
+ }), 500
643
+
644
+ builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
645
+ builder.delete_graph(graph_id)
646
+
647
+ return jsonify({
648
+ "success": True,
649
+ "message": f"ๅ›พ่ฐฑๅทฒๅˆ ้™ค: {graph_id}"
650
+ })
651
+
652
+ except Exception as e:
653
+ return jsonify({
654
+ "success": False,
655
+ "error": str(e),
656
+ "traceback": traceback.format_exc()
657
+ }), 500
backend/app/config.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ้…็ฝฎ็ฎก็†
3
+ ็ปŸไธ€ไปŽ้กน็›ฎๆ น็›ฎๅฝ•็š„ .env ๆ–‡ไปถๅŠ ่ฝฝ้…็ฝฎ
4
+ """
5
+
6
+ import os
7
+ from dotenv import load_dotenv
8
+
9
+ # ๅŠ ่ฝฝ้กน็›ฎๆ น็›ฎๅฝ•็š„ .env ๆ–‡ไปถ
10
+ # ่ทฏๅพ„: MiroFish/.env (็›ธๅฏนไบŽ backend/app/config.py)
11
+ project_root_env = os.path.join(os.path.dirname(__file__), '../../.env')
12
+
13
+ if os.path.exists(project_root_env):
14
+ load_dotenv(project_root_env)
15
+ else:
16
+ # ๅฆ‚ๆžœๆ น็›ฎๅฝ•ๆฒกๆœ‰ .env๏ผŒๅฐ่ฏ•ๅŠ ่ฝฝ็Žฏๅขƒๅ˜้‡๏ผˆ็”จไบŽ็”Ÿไบง็Žฏๅขƒ๏ผ‰
17
+ load_dotenv()
18
+
19
+
20
+ class Config:
21
+ """Flask้…็ฝฎ็ฑป"""
22
+
23
+ # Flask้…็ฝฎ
24
+ SECRET_KEY = os.environ.get('SECRET_KEY', 'mirofish-secret-key')
25
+ DEBUG = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true'
26
+
27
+ # LLM้…็ฝฎ๏ผˆ็ปŸไธ€ไฝฟ็”จOpenAIๆ ผๅผ๏ผ‰
28
+ LLM_API_KEY = os.environ.get('LLM_API_KEY')
29
+ LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'https://api.openai.com/v1')
30
+ LLM_MODEL_NAME = os.environ.get('LLM_MODEL_NAME', 'gpt-4o-mini')
31
+
32
+ # Zep้…็ฝฎ
33
+ ZEP_API_KEY = os.environ.get('ZEP_API_KEY')
34
+
35
+ # ๆ–‡ไปถไธŠไผ ้…็ฝฎ
36
+ MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB
37
+ UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '../uploads')
38
+ ALLOWED_EXTENSIONS = {'pdf', 'md', 'txt', 'markdown'}
39
+
40
+ # ๆ–‡ๆœฌๅค„็†้…็ฝฎ
41
+ DEFAULT_CHUNK_SIZE = 500 # ้ป˜่ฎคๅˆ‡ๅ—ๅคงๅฐ
42
+ DEFAULT_CHUNK_OVERLAP = 50 # ้ป˜่ฎค้‡ๅ ๅคงๅฐ
43
+
44
+ @classmethod
45
+ def validate(cls):
46
+ """้ชŒ่ฏๅฟ…่ฆ้…็ฝฎ"""
47
+ errors = []
48
+ if not cls.LLM_API_KEY:
49
+ errors.append("LLM_API_KEY ๆœช้…็ฝฎ")
50
+ if not cls.ZEP_API_KEY:
51
+ errors.append("ZEP_API_KEY ๆœช้…็ฝฎ")
52
+ return errors
53
+
backend/app/models/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๆ•ฐๆฎๆจกๅž‹ๆจกๅ—
3
+ """
4
+
5
+ from .task import TaskManager, TaskStatus
6
+ from .project import Project, ProjectStatus, ProjectManager
7
+
8
+ __all__ = ['TaskManager', 'TaskStatus', 'Project', 'ProjectStatus', 'ProjectManager']
9
+
backend/app/models/project.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ้กน็›ฎไธŠไธ‹ๆ–‡็ฎก็†
3
+ ็”จไบŽๅœจๆœๅŠก็ซฏๆŒไน…ๅŒ–้กน็›ฎ็Šถๆ€๏ผŒ้ฟๅ…ๅ‰็ซฏๅœจๆŽฅๅฃ้—ดไผ ้€’ๅคง้‡ๆ•ฐๆฎ
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import uuid
9
+ import shutil
10
+ from datetime import datetime
11
+ from typing import Dict, Any, List, Optional
12
+ from enum import Enum
13
+ from dataclasses import dataclass, field, asdict
14
+ from ..config import Config
15
+
16
+
17
+ class ProjectStatus(str, Enum):
18
+ """้กน็›ฎ็Šถๆ€"""
19
+ CREATED = "created" # ๅˆšๅˆ›ๅปบ๏ผŒๆ–‡ไปถๅทฒไธŠไผ 
20
+ ONTOLOGY_GENERATED = "ontology_generated" # ๆœฌไฝ“ๅทฒ็”Ÿๆˆ
21
+ GRAPH_BUILDING = "graph_building" # ๅ›พ่ฐฑๆž„ๅปบไธญ
22
+ GRAPH_COMPLETED = "graph_completed" # ๅ›พ่ฐฑๆž„ๅปบๅฎŒๆˆ
23
+ FAILED = "failed" # ๅคฑ่ดฅ
24
+
25
+
26
+ @dataclass
27
+ class Project:
28
+ """้กน็›ฎๆ•ฐๆฎๆจกๅž‹"""
29
+ project_id: str
30
+ name: str
31
+ status: ProjectStatus
32
+ created_at: str
33
+ updated_at: str
34
+
35
+ # ๆ–‡ไปถไฟกๆฏ
36
+ files: List[Dict[str, str]] = field(default_factory=list) # [{filename, path, size}]
37
+ total_text_length: int = 0
38
+
39
+ # ๆœฌไฝ“ไฟกๆฏ๏ผˆๆŽฅๅฃ1็”ŸๆˆๅŽๅกซๅ……๏ผ‰
40
+ ontology: Optional[Dict[str, Any]] = None
41
+ analysis_summary: Optional[str] = None
42
+
43
+ # ๅ›พ่ฐฑไฟกๆฏ๏ผˆๆŽฅๅฃ2ๅฎŒๆˆๅŽๅกซๅ……๏ผ‰
44
+ graph_id: Optional[str] = None
45
+ graph_build_task_id: Optional[str] = None
46
+
47
+ # ้…็ฝฎ
48
+ simulation_requirement: Optional[str] = None
49
+ chunk_size: int = 500
50
+ chunk_overlap: int = 50
51
+
52
+ # ้”™่ฏฏไฟกๆฏ
53
+ error: Optional[str] = None
54
+
55
+ def to_dict(self) -> Dict[str, Any]:
56
+ """่ฝฌๆขไธบๅญ—ๅ…ธ"""
57
+ return {
58
+ "project_id": self.project_id,
59
+ "name": self.name,
60
+ "status": self.status.value if isinstance(self.status, ProjectStatus) else self.status,
61
+ "created_at": self.created_at,
62
+ "updated_at": self.updated_at,
63
+ "files": self.files,
64
+ "total_text_length": self.total_text_length,
65
+ "ontology": self.ontology,
66
+ "analysis_summary": self.analysis_summary,
67
+ "graph_id": self.graph_id,
68
+ "graph_build_task_id": self.graph_build_task_id,
69
+ "simulation_requirement": self.simulation_requirement,
70
+ "chunk_size": self.chunk_size,
71
+ "chunk_overlap": self.chunk_overlap,
72
+ "error": self.error
73
+ }
74
+
75
+ @classmethod
76
+ def from_dict(cls, data: Dict[str, Any]) -> 'Project':
77
+ """ไปŽๅญ—ๅ…ธๅˆ›ๅปบ"""
78
+ status = data.get('status', 'created')
79
+ if isinstance(status, str):
80
+ status = ProjectStatus(status)
81
+
82
+ return cls(
83
+ project_id=data['project_id'],
84
+ name=data.get('name', 'Unnamed Project'),
85
+ status=status,
86
+ created_at=data.get('created_at', ''),
87
+ updated_at=data.get('updated_at', ''),
88
+ files=data.get('files', []),
89
+ total_text_length=data.get('total_text_length', 0),
90
+ ontology=data.get('ontology'),
91
+ analysis_summary=data.get('analysis_summary'),
92
+ graph_id=data.get('graph_id'),
93
+ graph_build_task_id=data.get('graph_build_task_id'),
94
+ simulation_requirement=data.get('simulation_requirement'),
95
+ chunk_size=data.get('chunk_size', 500),
96
+ chunk_overlap=data.get('chunk_overlap', 50),
97
+ error=data.get('error')
98
+ )
99
+
100
+
101
+ class ProjectManager:
102
+ """้กน็›ฎ็ฎก็†ๅ™จ - ่ดŸ่ดฃ้กน็›ฎ็š„ๆŒไน…ๅŒ–ๅญ˜ๅ‚จๅ’Œๆฃ€็ดข"""
103
+
104
+ # ้กน็›ฎๅญ˜ๅ‚จๆ น็›ฎๅฝ•
105
+ PROJECTS_DIR = os.path.join(Config.UPLOAD_FOLDER, 'projects')
106
+
107
+ @classmethod
108
+ def _ensure_projects_dir(cls):
109
+ """็กฎไฟ้กน็›ฎ็›ฎๅฝ•ๅญ˜ๅœจ"""
110
+ os.makedirs(cls.PROJECTS_DIR, exist_ok=True)
111
+
112
+ @classmethod
113
+ def _get_project_dir(cls, project_id: str) -> str:
114
+ """่Žทๅ–้กน็›ฎ็›ฎๅฝ•่ทฏๅพ„"""
115
+ return os.path.join(cls.PROJECTS_DIR, project_id)
116
+
117
+ @classmethod
118
+ def _get_project_meta_path(cls, project_id: str) -> str:
119
+ """่Žทๅ–้กน็›ฎๅ…ƒๆ•ฐๆฎๆ–‡ไปถ่ทฏๅพ„"""
120
+ return os.path.join(cls._get_project_dir(project_id), 'project.json')
121
+
122
+ @classmethod
123
+ def _get_project_files_dir(cls, project_id: str) -> str:
124
+ """่Žทๅ–้กน็›ฎๆ–‡ไปถๅญ˜ๅ‚จ็›ฎๅฝ•"""
125
+ return os.path.join(cls._get_project_dir(project_id), 'files')
126
+
127
+ @classmethod
128
+ def _get_project_text_path(cls, project_id: str) -> str:
129
+ """่Žทๅ–้กน็›ฎๆๅ–ๆ–‡ๆœฌๅญ˜ๅ‚จ่ทฏๅพ„"""
130
+ return os.path.join(cls._get_project_dir(project_id), 'extracted_text.txt')
131
+
132
+ @classmethod
133
+ def create_project(cls, name: str = "Unnamed Project") -> Project:
134
+ """
135
+ ๅˆ›ๅปบๆ–ฐ้กน็›ฎ
136
+
137
+ Args:
138
+ name: ้กน็›ฎๅ็งฐ
139
+
140
+ Returns:
141
+ ๆ–ฐๅˆ›ๅปบ็š„Projectๅฏน่ฑก
142
+ """
143
+ cls._ensure_projects_dir()
144
+
145
+ project_id = f"proj_{uuid.uuid4().hex[:12]}"
146
+ now = datetime.now().isoformat()
147
+
148
+ project = Project(
149
+ project_id=project_id,
150
+ name=name,
151
+ status=ProjectStatus.CREATED,
152
+ created_at=now,
153
+ updated_at=now
154
+ )
155
+
156
+ # ๅˆ›ๅปบ้กน็›ฎ็›ฎๅฝ•็ป“ๆž„
157
+ project_dir = cls._get_project_dir(project_id)
158
+ files_dir = cls._get_project_files_dir(project_id)
159
+ os.makedirs(project_dir, exist_ok=True)
160
+ os.makedirs(files_dir, exist_ok=True)
161
+
162
+ # ไฟๅญ˜้กน็›ฎๅ…ƒๆ•ฐๆฎ
163
+ cls.save_project(project)
164
+
165
+ return project
166
+
167
+ @classmethod
168
+ def save_project(cls, project: Project) -> None:
169
+ """ไฟๅญ˜้กน็›ฎๅ…ƒๆ•ฐๆฎ"""
170
+ project.updated_at = datetime.now().isoformat()
171
+ meta_path = cls._get_project_meta_path(project.project_id)
172
+
173
+ with open(meta_path, 'w', encoding='utf-8') as f:
174
+ json.dump(project.to_dict(), f, ensure_ascii=False, indent=2)
175
+
176
+ @classmethod
177
+ def get_project(cls, project_id: str) -> Optional[Project]:
178
+ """
179
+ ่Žทๅ–้กน็›ฎ
180
+
181
+ Args:
182
+ project_id: ้กน็›ฎID
183
+
184
+ Returns:
185
+ Projectๅฏน่ฑก๏ผŒๅฆ‚ๆžœไธๅญ˜ๅœจ่ฟ”ๅ›žNone
186
+ """
187
+ meta_path = cls._get_project_meta_path(project_id)
188
+
189
+ if not os.path.exists(meta_path):
190
+ return None
191
+
192
+ with open(meta_path, 'r', encoding='utf-8') as f:
193
+ data = json.load(f)
194
+
195
+ return Project.from_dict(data)
196
+
197
+ @classmethod
198
+ def list_projects(cls, limit: int = 50) -> List[Project]:
199
+ """
200
+ ๅˆ—ๅ‡บๆ‰€ๆœ‰้กน็›ฎ
201
+
202
+ Args:
203
+ limit: ่ฟ”ๅ›žๆ•ฐ้‡้™ๅˆถ
204
+
205
+ Returns:
206
+ ้กน็›ฎๅˆ—่กจ๏ผŒๆŒ‰ๅˆ›ๅปบๆ—ถ้—ดๅ€’ๅบ
207
+ """
208
+ cls._ensure_projects_dir()
209
+
210
+ projects = []
211
+ for project_id in os.listdir(cls.PROJECTS_DIR):
212
+ project = cls.get_project(project_id)
213
+ if project:
214
+ projects.append(project)
215
+
216
+ # ๆŒ‰ๅˆ›ๅปบๆ—ถ้—ดๅ€’ๅบๆŽ’ๅบ
217
+ projects.sort(key=lambda p: p.created_at, reverse=True)
218
+
219
+ return projects[:limit]
220
+
221
+ @classmethod
222
+ def delete_project(cls, project_id: str) -> bool:
223
+ """
224
+ ๅˆ ้™ค้กน็›ฎๅŠๅ…ถๆ‰€ๆœ‰ๆ–‡ไปถ
225
+
226
+ Args:
227
+ project_id: ้กน็›ฎID
228
+
229
+ Returns:
230
+ ๆ˜ฏๅฆๅˆ ้™คๆˆๅŠŸ
231
+ """
232
+ project_dir = cls._get_project_dir(project_id)
233
+
234
+ if not os.path.exists(project_dir):
235
+ return False
236
+
237
+ shutil.rmtree(project_dir)
238
+ return True
239
+
240
+ @classmethod
241
+ def save_file_to_project(cls, project_id: str, file_storage, original_filename: str) -> Dict[str, str]:
242
+ """
243
+ ไฟๅญ˜ไธŠไผ ็š„ๆ–‡ไปถๅˆฐ้กน็›ฎ็›ฎๅฝ•
244
+
245
+ Args:
246
+ project_id: ้กน็›ฎID
247
+ file_storage: Flask็š„FileStorageๅฏน่ฑก
248
+ original_filename: ๅŽŸๅง‹ๆ–‡ไปถๅ
249
+
250
+ Returns:
251
+ ๆ–‡ไปถไฟกๆฏๅญ—ๅ…ธ {filename, path, size}
252
+ """
253
+ files_dir = cls._get_project_files_dir(project_id)
254
+ os.makedirs(files_dir, exist_ok=True)
255
+
256
+ # ็”Ÿๆˆๅฎ‰ๅ…จ็š„ๆ–‡ไปถๅ
257
+ ext = os.path.splitext(original_filename)[1].lower()
258
+ safe_filename = f"{uuid.uuid4().hex[:8]}{ext}"
259
+ file_path = os.path.join(files_dir, safe_filename)
260
+
261
+ # ไฟๅญ˜ๆ–‡ไปถ
262
+ file_storage.save(file_path)
263
+
264
+ # ่Žทๅ–ๆ–‡ไปถๅคงๅฐ
265
+ file_size = os.path.getsize(file_path)
266
+
267
+ return {
268
+ "original_filename": original_filename,
269
+ "saved_filename": safe_filename,
270
+ "path": file_path,
271
+ "size": file_size
272
+ }
273
+
274
+ @classmethod
275
+ def save_extracted_text(cls, project_id: str, text: str) -> None:
276
+ """ไฟๅญ˜ๆๅ–็š„ๆ–‡ๆœฌ"""
277
+ text_path = cls._get_project_text_path(project_id)
278
+ with open(text_path, 'w', encoding='utf-8') as f:
279
+ f.write(text)
280
+
281
+ @classmethod
282
+ def get_extracted_text(cls, project_id: str) -> Optional[str]:
283
+ """่Žทๅ–ๆๅ–็š„ๆ–‡ๆœฌ"""
284
+ text_path = cls._get_project_text_path(project_id)
285
+
286
+ if not os.path.exists(text_path):
287
+ return None
288
+
289
+ with open(text_path, 'r', encoding='utf-8') as f:
290
+ return f.read()
291
+
292
+ @classmethod
293
+ def get_project_files(cls, project_id: str) -> List[str]:
294
+ """่Žทๅ–้กน็›ฎ็š„ๆ‰€ๆœ‰ๆ–‡ไปถ่ทฏๅพ„"""
295
+ files_dir = cls._get_project_files_dir(project_id)
296
+
297
+ if not os.path.exists(files_dir):
298
+ return []
299
+
300
+ return [
301
+ os.path.join(files_dir, f)
302
+ for f in os.listdir(files_dir)
303
+ if os.path.isfile(os.path.join(files_dir, f))
304
+ ]
305
+
backend/app/models/task.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ไปปๅŠก็Šถๆ€็ฎก็†
3
+ ็”จไบŽ่ทŸ่ธช้•ฟๆ—ถ้—ด่ฟ่กŒ็š„ไปปๅŠก๏ผˆๅฆ‚ๅ›พ่ฐฑๆž„ๅปบ๏ผ‰
4
+ """
5
+
6
+ import uuid
7
+ import threading
8
+ from datetime import datetime
9
+ from enum import Enum
10
+ from typing import Dict, Any, Optional
11
+ from dataclasses import dataclass, field
12
+
13
+
14
+ class TaskStatus(str, Enum):
15
+ """ไปปๅŠก็Šถๆ€ๆžšไธพ"""
16
+ PENDING = "pending" # ็ญ‰ๅพ…ไธญ
17
+ PROCESSING = "processing" # ๅค„็†ไธญ
18
+ COMPLETED = "completed" # ๅทฒๅฎŒๆˆ
19
+ FAILED = "failed" # ๅคฑ่ดฅ
20
+
21
+
22
+ @dataclass
23
+ class Task:
24
+ """ไปปๅŠกๆ•ฐๆฎ็ฑป"""
25
+ task_id: str
26
+ task_type: str
27
+ status: TaskStatus
28
+ created_at: datetime
29
+ updated_at: datetime
30
+ progress: int = 0 # ่ฟ›ๅบฆ็™พๅˆ†ๆฏ” 0-100
31
+ message: str = "" # ็Šถๆ€ๆถˆๆฏ
32
+ result: Optional[Dict] = None # ไปปๅŠก็ป“ๆžœ
33
+ error: Optional[str] = None # ้”™่ฏฏไฟกๆฏ
34
+ metadata: Dict = field(default_factory=dict) # ้ขๅค–ๅ…ƒๆ•ฐๆฎ
35
+
36
+ def to_dict(self) -> Dict[str, Any]:
37
+ """่ฝฌๆขไธบๅญ—ๅ…ธ"""
38
+ return {
39
+ "task_id": self.task_id,
40
+ "task_type": self.task_type,
41
+ "status": self.status.value,
42
+ "created_at": self.created_at.isoformat(),
43
+ "updated_at": self.updated_at.isoformat(),
44
+ "progress": self.progress,
45
+ "message": self.message,
46
+ "result": self.result,
47
+ "error": self.error,
48
+ "metadata": self.metadata,
49
+ }
50
+
51
+
52
+ class TaskManager:
53
+ """
54
+ ไปปๅŠก็ฎก็†ๅ™จ
55
+ ็บฟ็จ‹ๅฎ‰ๅ…จ็š„ไปปๅŠก็Šถๆ€็ฎก็†
56
+ """
57
+
58
+ _instance = None
59
+ _lock = threading.Lock()
60
+
61
+ def __new__(cls):
62
+ """ๅ•ไพ‹ๆจกๅผ"""
63
+ if cls._instance is None:
64
+ with cls._lock:
65
+ if cls._instance is None:
66
+ cls._instance = super().__new__(cls)
67
+ cls._instance._tasks: Dict[str, Task] = {}
68
+ cls._instance._task_lock = threading.Lock()
69
+ return cls._instance
70
+
71
+ def create_task(self, task_type: str, metadata: Optional[Dict] = None) -> str:
72
+ """
73
+ ๅˆ›ๅปบๆ–ฐไปปๅŠก
74
+
75
+ Args:
76
+ task_type: ไปปๅŠก็ฑปๅž‹
77
+ metadata: ้ขๅค–ๅ…ƒๆ•ฐๆฎ
78
+
79
+ Returns:
80
+ ไปปๅŠกID
81
+ """
82
+ task_id = str(uuid.uuid4())
83
+ now = datetime.now()
84
+
85
+ task = Task(
86
+ task_id=task_id,
87
+ task_type=task_type,
88
+ status=TaskStatus.PENDING,
89
+ created_at=now,
90
+ updated_at=now,
91
+ metadata=metadata or {}
92
+ )
93
+
94
+ with self._task_lock:
95
+ self._tasks[task_id] = task
96
+
97
+ return task_id
98
+
99
+ def get_task(self, task_id: str) -> Optional[Task]:
100
+ """่Žทๅ–ไปปๅŠก"""
101
+ with self._task_lock:
102
+ return self._tasks.get(task_id)
103
+
104
+ def update_task(
105
+ self,
106
+ task_id: str,
107
+ status: Optional[TaskStatus] = None,
108
+ progress: Optional[int] = None,
109
+ message: Optional[str] = None,
110
+ result: Optional[Dict] = None,
111
+ error: Optional[str] = None
112
+ ):
113
+ """
114
+ ๆ›ดๆ–ฐไปปๅŠก็Šถๆ€
115
+
116
+ Args:
117
+ task_id: ไปปๅŠกID
118
+ status: ๆ–ฐ็Šถๆ€
119
+ progress: ่ฟ›ๅบฆ
120
+ message: ๆถˆๆฏ
121
+ result: ็ป“ๆžœ
122
+ error: ้”™่ฏฏไฟกๆฏ
123
+ """
124
+ with self._task_lock:
125
+ task = self._tasks.get(task_id)
126
+ if task:
127
+ task.updated_at = datetime.now()
128
+ if status is not None:
129
+ task.status = status
130
+ if progress is not None:
131
+ task.progress = progress
132
+ if message is not None:
133
+ task.message = message
134
+ if result is not None:
135
+ task.result = result
136
+ if error is not None:
137
+ task.error = error
138
+
139
+ def complete_task(self, task_id: str, result: Dict):
140
+ """ๆ ‡่ฎฐไปปๅŠกๅฎŒๆˆ"""
141
+ self.update_task(
142
+ task_id,
143
+ status=TaskStatus.COMPLETED,
144
+ progress=100,
145
+ message="ไปปๅŠกๅฎŒๆˆ",
146
+ result=result
147
+ )
148
+
149
+ def fail_task(self, task_id: str, error: str):
150
+ """ๆ ‡่ฎฐไปปๅŠกๅคฑ่ดฅ"""
151
+ self.update_task(
152
+ task_id,
153
+ status=TaskStatus.FAILED,
154
+ message="ไปปๅŠกๅคฑ่ดฅ",
155
+ error=error
156
+ )
157
+
158
+ def list_tasks(self, task_type: Optional[str] = None) -> list:
159
+ """ๅˆ—ๅ‡บไปปๅŠก"""
160
+ with self._task_lock:
161
+ tasks = list(self._tasks.values())
162
+ if task_type:
163
+ tasks = [t for t in tasks if t.task_type == task_type]
164
+ return [t.to_dict() for t in sorted(tasks, key=lambda x: x.created_at, reverse=True)]
165
+
166
+ def cleanup_old_tasks(self, max_age_hours: int = 24):
167
+ """ๆธ…็†ๆ—งไปปๅŠก"""
168
+ from datetime import timedelta
169
+ cutoff = datetime.now() - timedelta(hours=max_age_hours)
170
+
171
+ with self._task_lock:
172
+ old_ids = [
173
+ tid for tid, task in self._tasks.items()
174
+ if task.created_at < cutoff and task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED]
175
+ ]
176
+ for tid in old_ids:
177
+ del self._tasks[tid]
178
+
backend/app/services/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ไธšๅŠกๆœๅŠกๆจกๅ—
3
+ """
4
+
5
+ from .ontology_generator import OntologyGenerator
6
+ from .graph_builder import GraphBuilderService
7
+ from .text_processor import TextProcessor
8
+
9
+ __all__ = ['OntologyGenerator', 'GraphBuilderService', 'TextProcessor']
10
+
backend/app/services/graph_builder.py ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๅ›พ่ฐฑๆž„ๅปบๆœๅŠก
3
+ ๆŽฅๅฃ2๏ผšไฝฟ็”จZep APIๆž„ๅปบStandalone Graph
4
+ """
5
+
6
+ import os
7
+ import uuid
8
+ import time
9
+ import threading
10
+ from typing import Dict, Any, List, Optional, Callable
11
+ from dataclasses import dataclass
12
+
13
+ from zep_cloud.client import Zep
14
+ from zep_cloud import EpisodeData, EntityEdgeSourceTarget
15
+
16
+ from ..config import Config
17
+ from ..models.task import TaskManager, TaskStatus
18
+ from .text_processor import TextProcessor
19
+
20
+
21
+ @dataclass
22
+ class GraphInfo:
23
+ """ๅ›พ่ฐฑไฟกๆฏ"""
24
+ graph_id: str
25
+ node_count: int
26
+ edge_count: int
27
+ entity_types: List[str]
28
+
29
+ def to_dict(self) -> Dict[str, Any]:
30
+ return {
31
+ "graph_id": self.graph_id,
32
+ "node_count": self.node_count,
33
+ "edge_count": self.edge_count,
34
+ "entity_types": self.entity_types,
35
+ }
36
+
37
+
38
+ class GraphBuilderService:
39
+ """
40
+ ๅ›พ่ฐฑๆž„ๅปบๆœๅŠก
41
+ ่ดŸ่ดฃ่ฐƒ็”จZep APIๆž„ๅปบ็Ÿฅ่ฏ†ๅ›พ่ฐฑ
42
+ """
43
+
44
+ def __init__(self, api_key: Optional[str] = None):
45
+ self.api_key = api_key or Config.ZEP_API_KEY
46
+ if not self.api_key:
47
+ raise ValueError("ZEP_API_KEY ๆœช้…็ฝฎ")
48
+
49
+ self.client = Zep(api_key=self.api_key)
50
+ self.task_manager = TaskManager()
51
+
52
+ def build_graph_async(
53
+ self,
54
+ text: str,
55
+ ontology: Dict[str, Any],
56
+ graph_name: str = "MiroFish Graph",
57
+ chunk_size: int = 500,
58
+ chunk_overlap: int = 50,
59
+ batch_size: int = 3
60
+ ) -> str:
61
+ """
62
+ ๅผ‚ๆญฅๆž„ๅปบๅ›พ่ฐฑ
63
+
64
+ Args:
65
+ text: ่พ“ๅ…ฅๆ–‡ๆœฌ
66
+ ontology: ๆœฌไฝ“ๅฎšไน‰๏ผˆๆฅ่‡ชๆŽฅๅฃ1็š„่พ“ๅ‡บ๏ผ‰
67
+ graph_name: ๅ›พ่ฐฑๅ็งฐ
68
+ chunk_size: ๆ–‡ๆœฌๅ—ๅคงๅฐ
69
+ chunk_overlap: ๅ—้‡ๅ ๅคงๅฐ
70
+ batch_size: ๆฏๆ‰นๅ‘้€็š„ๅ—ๆ•ฐ้‡
71
+
72
+ Returns:
73
+ ไปปๅŠกID
74
+ """
75
+ # ๅˆ›ๅปบไปปๅŠก
76
+ task_id = self.task_manager.create_task(
77
+ task_type="graph_build",
78
+ metadata={
79
+ "graph_name": graph_name,
80
+ "chunk_size": chunk_size,
81
+ "text_length": len(text),
82
+ }
83
+ )
84
+
85
+ # ๅœจๅŽๅฐ็บฟ็จ‹ไธญๆ‰ง่กŒๆž„ๅปบ
86
+ thread = threading.Thread(
87
+ target=self._build_graph_worker,
88
+ args=(task_id, text, ontology, graph_name, chunk_size, chunk_overlap, batch_size)
89
+ )
90
+ thread.daemon = True
91
+ thread.start()
92
+
93
+ return task_id
94
+
95
+ def _build_graph_worker(
96
+ self,
97
+ task_id: str,
98
+ text: str,
99
+ ontology: Dict[str, Any],
100
+ graph_name: str,
101
+ chunk_size: int,
102
+ chunk_overlap: int,
103
+ batch_size: int
104
+ ):
105
+ """ๅ›พ่ฐฑๆž„ๅปบๅทฅไฝœ็บฟ็จ‹"""
106
+ try:
107
+ self.task_manager.update_task(
108
+ task_id,
109
+ status=TaskStatus.PROCESSING,
110
+ progress=5,
111
+ message="ๅผ€ๅง‹ๆž„ๅปบๅ›พ่ฐฑ..."
112
+ )
113
+
114
+ # 1. ๅˆ›ๅปบๅ›พ่ฐฑ
115
+ graph_id = self.create_graph(graph_name)
116
+ self.task_manager.update_task(
117
+ task_id,
118
+ progress=10,
119
+ message=f"ๅ›พ่ฐฑๅทฒๅˆ›ๅปบ: {graph_id}"
120
+ )
121
+
122
+ # 2. ่ฎพ็ฝฎๆœฌไฝ“
123
+ self.set_ontology(graph_id, ontology)
124
+ self.task_manager.update_task(
125
+ task_id,
126
+ progress=15,
127
+ message="ๆœฌไฝ“ๅทฒ่ฎพ็ฝฎ"
128
+ )
129
+
130
+ # 3. ๆ–‡ๆœฌๅˆ†ๅ—
131
+ chunks = TextProcessor.split_text(text, chunk_size, chunk_overlap)
132
+ total_chunks = len(chunks)
133
+ self.task_manager.update_task(
134
+ task_id,
135
+ progress=20,
136
+ message=f"ๆ–‡ๆœฌๅทฒๅˆ†ๅ‰ฒไธบ {total_chunks} ไธชๅ—"
137
+ )
138
+
139
+ # 4. ๅˆ†ๆ‰นๅ‘้€ๆ•ฐๆฎ
140
+ episode_uuids = self.add_text_batches(
141
+ graph_id, chunks, batch_size,
142
+ lambda msg, prog: self.task_manager.update_task(
143
+ task_id,
144
+ progress=20 + int(prog * 0.4), # 20-60%
145
+ message=msg
146
+ )
147
+ )
148
+
149
+ # 5. ็ญ‰ๅพ…Zepๅค„็†ๅฎŒๆˆ
150
+ self.task_manager.update_task(
151
+ task_id,
152
+ progress=60,
153
+ message="็ญ‰ๅพ…Zepๅค„็†ๆ•ฐๆฎ..."
154
+ )
155
+
156
+ self._wait_for_episodes(
157
+ episode_uuids,
158
+ lambda msg, prog: self.task_manager.update_task(
159
+ task_id,
160
+ progress=60 + int(prog * 0.3), # 60-90%
161
+ message=msg
162
+ )
163
+ )
164
+
165
+ # 6. ่Žทๅ–ๅ›พ่ฐฑไฟกๆฏ
166
+ self.task_manager.update_task(
167
+ task_id,
168
+ progress=90,
169
+ message="่Žทๅ–ๅ›พ่ฐฑไฟกๆฏ..."
170
+ )
171
+
172
+ graph_info = self._get_graph_info(graph_id)
173
+
174
+ # ๅฎŒๆˆ
175
+ self.task_manager.complete_task(task_id, {
176
+ "graph_id": graph_id,
177
+ "graph_info": graph_info.to_dict(),
178
+ "chunks_processed": total_chunks,
179
+ })
180
+
181
+ except Exception as e:
182
+ import traceback
183
+ error_msg = f"{str(e)}\n{traceback.format_exc()}"
184
+ self.task_manager.fail_task(task_id, error_msg)
185
+
186
+ def create_graph(self, name: str) -> str:
187
+ """ๅˆ›ๅปบZepๅ›พ่ฐฑ๏ผˆๅ…ฌๅผ€ๆ–นๆณ•๏ผ‰"""
188
+ graph_id = f"mirofish_{uuid.uuid4().hex[:16]}"
189
+
190
+ self.client.graph.create(
191
+ graph_id=graph_id,
192
+ name=name,
193
+ description="MiroFish Social Simulation Graph"
194
+ )
195
+
196
+ return graph_id
197
+
198
+ def set_ontology(self, graph_id: str, ontology: Dict[str, Any]):
199
+ """่ฎพ็ฝฎๅ›พ่ฐฑๆœฌไฝ“๏ผˆๅ…ฌๅผ€ๆ–นๆณ•๏ผ‰"""
200
+ from typing import Optional
201
+ from pydantic import Field
202
+ from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel
203
+
204
+ # Zep ไฟ็•™ๅ็งฐ๏ผŒไธ่ƒฝไฝœไธบๅฑžๆ€งๅ
205
+ RESERVED_NAMES = {'uuid', 'name', 'group_id', 'name_embedding', 'summary', 'created_at'}
206
+
207
+ def safe_attr_name(attr_name: str) -> str:
208
+ """ๅฐ†ไฟ็•™ๅ็งฐ่ฝฌๆขไธบๅฎ‰ๅ…จๅ็งฐ"""
209
+ if attr_name.lower() in RESERVED_NAMES:
210
+ return f"entity_{attr_name}"
211
+ return attr_name
212
+
213
+ # ๅŠจๆ€ๅˆ›ๅปบๅฎžไฝ“็ฑปๅž‹
214
+ entity_types = {}
215
+ for entity_def in ontology.get("entity_types", []):
216
+ name = entity_def["name"]
217
+ description = entity_def.get("description", f"A {name} entity.")
218
+
219
+ # ๅˆ›ๅปบๅฑžๆ€งๅญ—ๅ…ธๅ’Œ็ฑปๅž‹ๆณจ่งฃ๏ผˆPydantic v2 ้œ€่ฆ๏ผ‰
220
+ attrs = {"__doc__": description}
221
+ annotations = {}
222
+
223
+ for attr_def in entity_def.get("attributes", []):
224
+ attr_name = safe_attr_name(attr_def["name"]) # ไฝฟ็”จๅฎ‰ๅ…จๅ็งฐ
225
+ attr_desc = attr_def.get("description", attr_name)
226
+ attrs[attr_name] = Field(description=attr_desc, default=None)
227
+ annotations[attr_name] = Optional[EntityText] # ็ฑปๅž‹ๆณจ่งฃ
228
+
229
+ attrs["__annotations__"] = annotations
230
+
231
+ # ๅŠจๆ€ๅˆ›ๅปบ็ฑป
232
+ entity_class = type(name, (EntityModel,), attrs)
233
+ entity_class.__doc__ = description
234
+ entity_types[name] = entity_class
235
+
236
+ # ๅŠจๆ€ๅˆ›ๅปบ่พน็ฑปๅž‹
237
+ edge_definitions = {}
238
+ for edge_def in ontology.get("edge_types", []):
239
+ name = edge_def["name"]
240
+ description = edge_def.get("description", f"A {name} relationship.")
241
+
242
+ # ๅˆ›ๅปบๅฑžๆ€งๅญ—ๅ…ธๅ’Œ็ฑปๅž‹ๆณจ่งฃ
243
+ attrs = {"__doc__": description}
244
+ annotations = {}
245
+
246
+ for attr_def in edge_def.get("attributes", []):
247
+ attr_name = safe_attr_name(attr_def["name"]) # ไฝฟ็”จๅฎ‰ๅ…จๅ็งฐ
248
+ attr_desc = attr_def.get("description", attr_name)
249
+ attrs[attr_name] = Field(description=attr_desc, default=None)
250
+ annotations[attr_name] = Optional[str] # ่พนๅฑžๆ€ง็”จstr็ฑปๅž‹
251
+
252
+ attrs["__annotations__"] = annotations
253
+
254
+ # ๅŠจๆ€ๅˆ›ๅปบ็ฑป
255
+ class_name = ''.join(word.capitalize() for word in name.split('_'))
256
+ edge_class = type(class_name, (EdgeModel,), attrs)
257
+ edge_class.__doc__ = description
258
+
259
+ # ๆž„ๅปบsource_targets
260
+ source_targets = []
261
+ for st in edge_def.get("source_targets", []):
262
+ source_targets.append(
263
+ EntityEdgeSourceTarget(
264
+ source=st.get("source", "Entity"),
265
+ target=st.get("target", "Entity")
266
+ )
267
+ )
268
+
269
+ if source_targets:
270
+ edge_definitions[name] = (edge_class, source_targets)
271
+
272
+ # ่ฐƒ็”จZep API่ฎพ็ฝฎๆœฌไฝ“
273
+ if entity_types or edge_definitions:
274
+ self.client.graph.set_ontology(
275
+ graph_ids=[graph_id],
276
+ entities=entity_types if entity_types else None,
277
+ edges=edge_definitions if edge_definitions else None,
278
+ )
279
+
280
+ def add_text_batches(
281
+ self,
282
+ graph_id: str,
283
+ chunks: List[str],
284
+ batch_size: int = 3,
285
+ progress_callback: Optional[Callable] = None
286
+ ) -> List[str]:
287
+ """ๅˆ†ๆ‰นๆทปๅŠ ๆ–‡ๆœฌๅˆฐๅ›พ่ฐฑ๏ผŒ่ฟ”ๅ›žๆ‰€ๆœ‰ episode ็š„ uuid ๅˆ—่กจ"""
288
+ episode_uuids = []
289
+ total_chunks = len(chunks)
290
+
291
+ for i in range(0, total_chunks, batch_size):
292
+ batch_chunks = chunks[i:i + batch_size]
293
+ batch_num = i // batch_size + 1
294
+ total_batches = (total_chunks + batch_size - 1) // batch_size
295
+
296
+ if progress_callback:
297
+ progress = (i + len(batch_chunks)) / total_chunks
298
+ progress_callback(
299
+ f"ๅ‘้€็ฌฌ {batch_num}/{total_batches} ๆ‰นๆ•ฐๆฎ ({len(batch_chunks)} ๅ—)...",
300
+ progress
301
+ )
302
+
303
+ # ๆž„ๅปบepisodeๆ•ฐๆฎ
304
+ episodes = [
305
+ EpisodeData(data=chunk, type="text")
306
+ for chunk in batch_chunks
307
+ ]
308
+
309
+ # ๅ‘้€ๅˆฐZep
310
+ try:
311
+ batch_result = self.client.graph.add_batch(
312
+ graph_id=graph_id,
313
+ episodes=episodes
314
+ )
315
+
316
+ # ๆ”ถ้›†่ฟ”ๅ›ž็š„ episode uuid
317
+ if batch_result and isinstance(batch_result, list):
318
+ for ep in batch_result:
319
+ ep_uuid = getattr(ep, 'uuid_', None) or getattr(ep, 'uuid', None)
320
+ if ep_uuid:
321
+ episode_uuids.append(ep_uuid)
322
+
323
+ # ้ฟๅ…่ฏทๆฑ‚่ฟ‡ๅฟซ
324
+ time.sleep(1)
325
+
326
+ except Exception as e:
327
+ if progress_callback:
328
+ progress_callback(f"ๆ‰นๆฌก {batch_num} ๅ‘้€ๅคฑ่ดฅ: {str(e)}", 0)
329
+ raise
330
+
331
+ return episode_uuids
332
+
333
+ def _wait_for_episodes(
334
+ self,
335
+ episode_uuids: List[str],
336
+ progress_callback: Optional[Callable] = None,
337
+ timeout: int = 600
338
+ ):
339
+ """็ญ‰ๅพ…ๆ‰€ๆœ‰ episode ๅค„็†ๅฎŒๆˆ๏ผˆ้€š่ฟ‡ๆŸฅ่ฏขๆฏไธช episode ็š„ processed ็Šถๆ€๏ผ‰"""
340
+ if not episode_uuids:
341
+ if progress_callback:
342
+ progress_callback("ๆ— ้œ€็ญ‰ๅพ…๏ผˆๆฒกๆœ‰ episode๏ผ‰", 1.0)
343
+ return
344
+
345
+ start_time = time.time()
346
+ pending_episodes = set(episode_uuids)
347
+ completed_count = 0
348
+ total_episodes = len(episode_uuids)
349
+
350
+ if progress_callback:
351
+ progress_callback(f"ๅผ€ๅง‹็ญ‰ๅพ… {total_episodes} ไธชๆ–‡ๆœฌๅ—ๅค„็†...", 0)
352
+
353
+ while pending_episodes:
354
+ if time.time() - start_time > timeout:
355
+ if progress_callback:
356
+ progress_callback(
357
+ f"้ƒจๅˆ†ๆ–‡ๆœฌๅ—่ถ…ๆ—ถ๏ผŒๅทฒๅฎŒๆˆ {completed_count}/{total_episodes}",
358
+ completed_count / total_episodes
359
+ )
360
+ break
361
+
362
+ # ๆฃ€ๆŸฅๆฏไธช episode ็š„ๅค„็†็Šถๆ€
363
+ for ep_uuid in list(pending_episodes):
364
+ try:
365
+ episode = self.client.graph.episode.get(uuid_=ep_uuid)
366
+ is_processed = getattr(episode, 'processed', False)
367
+
368
+ if is_processed:
369
+ pending_episodes.remove(ep_uuid)
370
+ completed_count += 1
371
+
372
+ except Exception as e:
373
+ # ๅฟฝ็•ฅๅ•ไธชๆŸฅ่ฏข้”™่ฏฏ๏ผŒ็ปง็ปญ
374
+ pass
375
+
376
+ elapsed = int(time.time() - start_time)
377
+ if progress_callback:
378
+ progress_callback(
379
+ f"Zepๅค„็†ไธญ... {completed_count}/{total_episodes} ๅฎŒๆˆ, {len(pending_episodes)} ๅพ…ๅค„็† ({elapsed}็ง’)",
380
+ completed_count / total_episodes if total_episodes > 0 else 0
381
+ )
382
+
383
+ if pending_episodes:
384
+ time.sleep(3) # ๆฏ3็ง’ๆฃ€ๆŸฅไธ€ๆฌก
385
+
386
+ if progress_callback:
387
+ progress_callback(f"ๅค„็†ๅฎŒๆˆ: {completed_count}/{total_episodes}", 1.0)
388
+
389
+ def _get_graph_info(self, graph_id: str) -> GraphInfo:
390
+ """่Žทๅ–ๅ›พ่ฐฑไฟกๆฏ"""
391
+ # ่Žทๅ–่Š‚็‚น
392
+ nodes = self.client.graph.node.get_by_graph_id(graph_id=graph_id)
393
+
394
+ # ่Žทๅ–่พน
395
+ edges = self.client.graph.edge.get_by_graph_id(graph_id=graph_id)
396
+
397
+ # ็ปŸ่ฎกๅฎžไฝ“็ฑปๅž‹
398
+ entity_types = set()
399
+ for node in nodes:
400
+ if node.labels:
401
+ for label in node.labels:
402
+ if label not in ["Entity", "Node"]:
403
+ entity_types.add(label)
404
+
405
+ return GraphInfo(
406
+ graph_id=graph_id,
407
+ node_count=len(nodes),
408
+ edge_count=len(edges),
409
+ entity_types=list(entity_types)
410
+ )
411
+
412
+ def get_graph_data(self, graph_id: str) -> Dict[str, Any]:
413
+ """
414
+ ่Žทๅ–ๅฎŒๆ•ดๅ›พ่ฐฑๆ•ฐๆฎ
415
+
416
+ Args:
417
+ graph_id: ๅ›พ่ฐฑID
418
+
419
+ Returns:
420
+ ๅŒ…ๅซnodesๅ’Œedges็š„ๅญ—ๅ…ธ
421
+ """
422
+ nodes = self.client.graph.node.get_by_graph_id(graph_id=graph_id)
423
+ edges = self.client.graph.edge.get_by_graph_id(graph_id=graph_id)
424
+
425
+ nodes_data = []
426
+ for node in nodes:
427
+ nodes_data.append({
428
+ "uuid": node.uuid_,
429
+ "name": node.name,
430
+ "labels": node.labels or [],
431
+ "summary": node.summary or "",
432
+ "attributes": node.attributes or {},
433
+ })
434
+
435
+ edges_data = []
436
+ for edge in edges:
437
+ edges_data.append({
438
+ "uuid": edge.uuid_,
439
+ "name": edge.name or "",
440
+ "fact": edge.fact or "",
441
+ "source_node_uuid": edge.source_node_uuid,
442
+ "target_node_uuid": edge.target_node_uuid,
443
+ "attributes": edge.attributes or {},
444
+ })
445
+
446
+ return {
447
+ "graph_id": graph_id,
448
+ "nodes": nodes_data,
449
+ "edges": edges_data,
450
+ "node_count": len(nodes_data),
451
+ "edge_count": len(edges_data),
452
+ }
453
+
454
+ def delete_graph(self, graph_id: str):
455
+ """ๅˆ ้™คๅ›พ่ฐฑ"""
456
+ self.client.graph.delete(graph_id=graph_id)
457
+
backend/app/services/ontology_generator.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๆœฌไฝ“็”ŸๆˆๆœๅŠก
3
+ ๆŽฅๅฃ1๏ผšๅˆ†ๆžๆ–‡ๆœฌๅ†…ๅฎน๏ผŒ็”Ÿๆˆ้€‚ๅˆ็คพไผšๆจกๆ‹Ÿ็š„ๅฎžไฝ“ๅ’Œๅ…ณ็ณป็ฑปๅž‹ๅฎšไน‰
4
+ """
5
+
6
+ import json
7
+ from typing import Dict, Any, List, Optional
8
+ from ..utils.llm_client import LLMClient
9
+
10
+
11
+ # ๆœฌไฝ“็”Ÿๆˆ็š„็ณป็ปŸๆ็คบ่ฏ
12
+ ONTOLOGY_SYSTEM_PROMPT = """ไฝ ๆ˜ฏไธ€ไธชไธ“ไธš็š„็Ÿฅ่ฏ†ๅ›พ่ฐฑๆœฌไฝ“่ฎพ่ฎกไธ“ๅฎถใ€‚ไฝ ็š„ไปปๅŠกๆ˜ฏๅˆ†ๆž็ป™ๅฎš็š„ๆ–‡ๆœฌๅ†…ๅฎนๅ’Œๆจกๆ‹Ÿ้œ€ๆฑ‚๏ผŒ่ฎพ่ฎก้€‚ๅˆ**็คพไบคๅช’ไฝ“่ˆ†่ฎบๆจกๆ‹Ÿ**็š„ๅฎžไฝ“็ฑปๅž‹ๅ’Œๅ…ณ็ณป็ฑปๅž‹ใ€‚
13
+
14
+ **้‡่ฆ๏ผšไฝ ๅฟ…้กป่พ“ๅ‡บๆœ‰ๆ•ˆ็š„JSONๆ ผๅผๆ•ฐๆฎ๏ผŒไธ่ฆ่พ“ๅ‡บไปปไฝ•ๅ…ถไป–ๅ†…ๅฎนใ€‚**
15
+
16
+ ## ๆ ธๅฟƒไปปๅŠก่ƒŒๆ™ฏ
17
+
18
+ ๆˆ‘ไปฌๆญฃๅœจๆž„ๅปบไธ€ไธช**็คพไบคๅช’ไฝ“่ˆ†่ฎบๆจกๆ‹Ÿ็ณป็ปŸ**ใ€‚ๅœจ่ฟ™ไธช็ณป็ปŸไธญ๏ผš
19
+ - ๆฏไธชๅฎžไฝ“้ƒฝๆ˜ฏไธ€ไธชๅฏไปฅๅœจ็คพไบคๅช’ไฝ“ไธŠๅ‘ๅฃฐใ€ไบ’ๅŠจใ€ไผ ๆ’ญไฟกๆฏ็š„"่ดฆๅท"ๆˆ–"ไธปไฝ“"
20
+ - ๅฎžไฝ“ไน‹้—ดไผš็›ธไบ’ๅฝฑๅ“ใ€่ฝฌๅ‘ใ€่ฏ„่ฎบใ€ๅ›žๅบ”
21
+ - ๆˆ‘ไปฌ้œ€่ฆๆจกๆ‹Ÿ่ˆ†่ฎบไบ‹ไปถไธญๅ„ๆ–น็š„ๅๅบ”ๅ’Œไฟกๆฏไผ ๆ’ญ่ทฏๅพ„
22
+
23
+ ๅ› ๆญค๏ผŒ**ๅฎžไฝ“ๅฟ…้กปๆ˜ฏ็Žฐๅฎžไธญ็œŸๅฎžๅญ˜ๅœจ็š„ใ€ๅฏไปฅๅœจ็คพๅช’ไธŠๅ‘ๅฃฐๅ’Œไบ’ๅŠจ็š„ไธปไฝ“**๏ผš
24
+
25
+ **ๅฏไปฅๆ˜ฏ๏ผˆ้ผ“ๅŠฑๅคšๆ ทๅŒ–ๅˆ’ๅˆ†๏ผ‰**๏ผš
26
+ - ๅ…ทไฝ“็š„ไธชไบบ๏ผˆๅ…ฌไผ—ไบบ็‰ฉใ€ๅฝ“ไบ‹ไบบใ€ๆ„่ง้ข†่ข–ใ€ไธ“ๅฎถๅญฆ่€…๏ผ‰
27
+ - ๅ…ฌๅธใ€ไผไธš๏ผˆๅŒ…ๆ‹ฌๅ…ถๅฎ˜ๆ–น่ดฆๅท๏ผ‰
28
+ - ็ป„็ป‡ๆœบๆž„๏ผˆๅคงๅญฆใ€ๅไผšใ€NGOใ€ๅทฅไผš็ญ‰๏ผ‰
29
+ - ๆ”ฟๅบœ้ƒจ้—จใ€็›‘็ฎกๆœบๆž„
30
+ - ๅช’ไฝ“ๆœบๆž„๏ผˆๆŠฅ็บธใ€็”ต่ง†ๅฐใ€่‡ชๅช’ไฝ“ใ€็ฝ‘็ซ™๏ผ‰
31
+ - ็คพไบคๅช’ไฝ“ๅนณๅฐๆœฌ่บซ
32
+ - ็‰นๅฎš็พคไฝ“ไปฃ่กจ๏ผˆๅฆ‚ๆ กๅ‹ไผšใ€็ฒ‰ไธๅ›ขใ€็ปดๆƒ็พคไฝ“็ญ‰๏ผ‰
33
+
34
+ **ไธๅฏไปฅๆ˜ฏ**๏ผš
35
+ - ๆŠฝ่ฑกๆฆ‚ๅฟต๏ผˆๅฆ‚"่ˆ†่ฎบ"ใ€"ๆƒ…็ปช"ใ€"่ถ‹ๅŠฟ"๏ผ‰
36
+ - ไธป้ข˜/่ฏ้ข˜๏ผˆๅฆ‚"ๅญฆๆœฏ่ฏšไฟก"ใ€"ๆ•™่‚ฒๆ”น้ฉ"๏ผ‰
37
+ - ่ง‚็‚น/ๆ€ๅบฆ๏ผˆๅฆ‚"ๆ”ฏๆŒๆ–น"ใ€"ๅๅฏนๆ–น"๏ผ‰
38
+ - ๆณ›ๆŒ‡็พคไฝ“๏ผˆๅฆ‚"็ฝ‘ๅ‹"ใ€"ๅ…ฌไผ—"ใ€"ๅญฆ็”Ÿ็พคไฝ“"๏ผ‰
39
+
40
+ ## ่พ“ๅ‡บๆ ผๅผ
41
+
42
+ ่ฏท่พ“ๅ‡บJSONๆ ผๅผ๏ผŒๅŒ…ๅซไปฅไธ‹็ป“ๆž„๏ผš
43
+
44
+ ```json
45
+ {
46
+ "entity_types": [
47
+ {
48
+ "name": "ๅฎžไฝ“็ฑปๅž‹ๅ็งฐ๏ผˆ่‹ฑๆ–‡๏ผŒPascalCase๏ผ‰",
49
+ "description": "็ฎ€็Ÿญๆ่ฟฐ๏ผˆ่‹ฑๆ–‡๏ผŒไธ่ถ…่ฟ‡100ๅญ—็ฌฆ๏ผ‰",
50
+ "attributes": [
51
+ {
52
+ "name": "ๅฑžๆ€งๅ๏ผˆ่‹ฑๆ–‡๏ผŒsnake_case๏ผ‰",
53
+ "type": "text",
54
+ "description": "ๅฑžๆ€งๆ่ฟฐ"
55
+ }
56
+ ],
57
+ "examples": ["็คบไพ‹ๅฎžไฝ“1", "็คบไพ‹ๅฎžไฝ“2"]
58
+ }
59
+ ],
60
+ "edge_types": [
61
+ {
62
+ "name": "ๅ…ณ็ณป็ฑปๅž‹ๅ็งฐ๏ผˆ่‹ฑๆ–‡๏ผŒUPPER_SNAKE_CASE๏ผ‰",
63
+ "description": "็ฎ€็Ÿญๆ่ฟฐ๏ผˆ่‹ฑๆ–‡๏ผŒไธ่ถ…่ฟ‡100ๅญ—็ฌฆ๏ผ‰",
64
+ "source_targets": [
65
+ {"source": "ๆบๅฎžไฝ“็ฑปๅž‹", "target": "็›ฎๆ ‡ๅฎžไฝ“็ฑปๅž‹"}
66
+ ],
67
+ "attributes": []
68
+ }
69
+ ],
70
+ "analysis_summary": "ๅฏนๆ–‡ๆœฌๅ†…ๅฎน็š„็ฎ€่ฆๅˆ†ๆž่ฏดๆ˜Ž๏ผˆไธญๆ–‡๏ผ‰"
71
+ }
72
+ ```
73
+
74
+ ## ่ฎพ่ฎกๆŒ‡ๅ—
75
+
76
+ 1. **ๅฎžไฝ“็ฑปๅž‹่ฎพ่ฎก๏ผˆ้‡่ฆ๏ผ่ฏทๅฐฝ้‡ๅคšๅˆ’ๅˆ†๏ผ‰**๏ผš
77
+ - **ๆ•ฐ้‡่ฆๆฑ‚๏ผš่‡ณๅฐ‘5ไธช๏ผŒๆœ€ๅคš10ไธชๅฎžไฝ“็ฑปๅž‹**
78
+ - ๆฏไธชๅฎžไฝ“็ฑปๅž‹ไปฃ่กจไธ€็ฑปๅฏไปฅๅœจ็คพๅช’ไธŠๅ‘ๅฃฐ็š„ไธปไฝ“
79
+ - ๅฐฝ้‡็ป†ๅˆ†ไธๅŒ่ง’่‰ฒ๏ผŒไพ‹ๅฆ‚๏ผš
80
+ - ไธ่ฆๅช็”จ"Person"๏ผŒๅฏไปฅ็ป†ๅˆ†ไธบ"PublicFigure"ใ€"Expert"ใ€"Whistleblower"็ญ‰
81
+ - ไธ่ฆๅช็”จ"Organization"๏ผŒๅฏไปฅ็ป†ๅˆ†ไธบ"University"ใ€"Company"ใ€"NGO"็ญ‰
82
+ - descriptionๅฟ…้กปๆธ…ๆ™ฐ่ฏดๆ˜Žไป€ไนˆๆ ท็š„ๅฎžไฝ“ๅบ”่ฏฅ่ขซๆๅ–
83
+ - ๆฏไธช็ฑปๅž‹ๆไพ›2-3ไธชๅ…ทไฝ“็คบไพ‹
84
+
85
+ 2. **ๅ…ณ็ณป็ฑปๅž‹่ฎพ่ฎก**๏ผš
86
+ - ๅ…ณ็ณปๅบ”่ฏฅๅๆ˜ ็คพๅช’ไบ’ๅŠจไธญ็š„็œŸๅฎž่”็ณป
87
+ - ๅ…ณๆณจๅฏ่ƒฝๅฝฑๅ“่ˆ†่ฎบไผ ๆ’ญ็š„ๅ…ณ็ณป๏ผš
88
+ - ไฟกๆฏไผ ๆ’ญ๏ผšREPORTS_ON, COMMENTS_ON, SHARES
89
+ - ็ป„็ป‡ๅ…ณ็ณป๏ผšAFFILIATED_WITH, WORKS_FOR, REPRESENTS
90
+ - ไบ’ๅŠจๅ…ณ็ณป๏ผšRESPONDS_TO, SUPPORTS, OPPOSES
91
+ - ๅ…ณ็ณป็ฑปๅž‹๏ผš5-10ไธชไธบๅฎœ
92
+
93
+ 3. **ๅฑžๆ€ง่ฎพ่ฎก**๏ผš
94
+ - ๆฏไธชๅฎžไฝ“็ฑปๅž‹1-3ไธชๅ…ณ้”ฎๅฑžๆ€ง
95
+ - ๅฑžๆ€งๅบ”ๆœ‰ๅŠฉไบŽ่ฏ†ๅˆซๅฎžไฝ“็š„็คพๅช’ๅฝฑๅ“ๅŠ›๏ผˆๅฆ‚roleใ€influence_level็ญ‰๏ผ‰
96
+
97
+ ## ๅฎžไฝ“็ฑปๅž‹ๅ‚่€ƒ๏ผˆ่ฏทๆ นๆฎๆ–‡ๆœฌๅ†…ๅฎน็ตๆดป้€‰ๆ‹ฉๅ’Œๆ‰ฉๅฑ•๏ผ‰
98
+
99
+ - Person: ๆ™ฎ้€šไธชไบบ
100
+ - PublicFigure: ๅ…ฌไผ—ไบบ็‰ฉ๏ผˆๆ˜Žๆ˜Ÿใ€็ฝ‘็บขใ€ๆ„่ง้ข†่ข–๏ผ‰
101
+ - Expert: ไธ“ๅฎถๅญฆ่€…
102
+ - Journalist: ่ฎฐ่€…
103
+ - Company: ๅ…ฌๅธไผไธš
104
+ - University: ้ซ˜ๆ ก
105
+ - GovernmentAgency: ๆ”ฟๅบœๆœบๆž„
106
+ - MediaOutlet: ไผ ็ปŸๅช’ไฝ“
107
+ - SelfMedia: ่‡ชๅช’ไฝ“่ดฆๅท
108
+ - SocialPlatform: ็คพไบคๅช’ไฝ“ๅนณๅฐ
109
+ - NGO: ้žๆ”ฟๅบœ็ป„็ป‡
110
+ - IndustryAssociation: ่กŒไธšๅไผš
111
+ - AlumniAssociation: ๆ กๅ‹ไผš
112
+ - FanGroup: ็ฒ‰ไธ็พคไฝ“/ๆ”ฏๆŒ็พคไฝ“
113
+
114
+ ## ๅ…ณ็ณป็ฑปๅž‹ๅ‚่€ƒ
115
+
116
+ - WORKS_FOR: ๅทฅไฝœไบŽ
117
+ - AFFILIATED_WITH: ้šถๅฑžไบŽ
118
+ - REPRESENTS: ไปฃ่กจ
119
+ - REGULATES: ็›‘็ฎก
120
+ - REPORTS_ON: ๆŠฅ้“
121
+ - COMMENTS_ON: ่ฏ„่ฎบ
122
+ - RESPONDS_TO: ๅ›žๅบ”
123
+ - SUPPORTS: ๆ”ฏๆŒ
124
+ - OPPOSES: ๅๅฏน
125
+ - COLLABORATES_WITH: ๅˆไฝœ
126
+ - COMPETES_WITH: ็ซžไบ‰
127
+ """
128
+
129
+
130
+ class OntologyGenerator:
131
+ """
132
+ ๆœฌไฝ“็”Ÿๆˆๅ™จ
133
+ ๅˆ†ๆžๆ–‡ๆœฌๅ†…ๅฎน๏ผŒ็”Ÿๆˆๅฎžไฝ“ๅ’Œๅ…ณ็ณป็ฑปๅž‹ๅฎšไน‰
134
+ """
135
+
136
+ def __init__(self, llm_client: Optional[LLMClient] = None):
137
+ self.llm_client = llm_client or LLMClient()
138
+
139
+ def generate(
140
+ self,
141
+ document_texts: List[str],
142
+ simulation_requirement: str,
143
+ additional_context: Optional[str] = None
144
+ ) -> Dict[str, Any]:
145
+ """
146
+ ็”Ÿๆˆๆœฌไฝ“ๅฎšไน‰
147
+
148
+ Args:
149
+ document_texts: ๆ–‡ๆกฃๆ–‡ๆœฌๅˆ—่กจ
150
+ simulation_requirement: ๆจกๆ‹Ÿ้œ€ๆฑ‚ๆ่ฟฐ
151
+ additional_context: ้ขๅค–ไธŠไธ‹ๆ–‡
152
+
153
+ Returns:
154
+ ๆœฌไฝ“ๅฎšไน‰๏ผˆentity_types, edge_types็ญ‰๏ผ‰
155
+ """
156
+ # ๆž„ๅปบ็”จๆˆทๆถˆๆฏ
157
+ user_message = self._build_user_message(
158
+ document_texts,
159
+ simulation_requirement,
160
+ additional_context
161
+ )
162
+
163
+ messages = [
164
+ {"role": "system", "content": ONTOLOGY_SYSTEM_PROMPT},
165
+ {"role": "user", "content": user_message}
166
+ ]
167
+
168
+ # ่ฐƒ็”จLLM
169
+ result = self.llm_client.chat_json(
170
+ messages=messages,
171
+ temperature=0.3,
172
+ max_tokens=4096
173
+ )
174
+
175
+ # ้ชŒ่ฏๅ’ŒๅŽๅค„็†
176
+ result = self._validate_and_process(result)
177
+
178
+ return result
179
+
180
+ # ไผ ็ป™ LLM ็š„ๆ–‡ๆœฌๆœ€ๅคง้•ฟๅบฆ๏ผˆ5ไธ‡ๅญ—๏ผ‰
181
+ MAX_TEXT_LENGTH_FOR_LLM = 50000
182
+
183
+ def _build_user_message(
184
+ self,
185
+ document_texts: List[str],
186
+ simulation_requirement: str,
187
+ additional_context: Optional[str]
188
+ ) -> str:
189
+ """ๆž„ๅปบ็”จๆˆทๆถˆๆฏ"""
190
+
191
+ # ๅˆๅนถๆ–‡ๆœฌ
192
+ combined_text = "\n\n---\n\n".join(document_texts)
193
+ original_length = len(combined_text)
194
+
195
+ # ๅฆ‚ๆžœๆ–‡ๆœฌ่ถ…่ฟ‡5ไธ‡ๅญ—๏ผŒๆˆชๆ–ญ๏ผˆไป…ๅฝฑๅ“ไผ ็ป™LLM็š„ๅ†…ๅฎน๏ผŒไธๅฝฑๅ“ๅ›พ่ฐฑๆž„ๅปบ๏ผ‰
196
+ if len(combined_text) > self.MAX_TEXT_LENGTH_FOR_LLM:
197
+ combined_text = combined_text[:self.MAX_TEXT_LENGTH_FOR_LLM]
198
+ combined_text += f"\n\n...(ๅŽŸๆ–‡ๅ…ฑ{original_length}ๅญ—๏ผŒๅทฒๆˆชๅ–ๅ‰{self.MAX_TEXT_LENGTH_FOR_LLM}ๅญ—็”จไบŽๆœฌไฝ“ๅˆ†ๆž)..."
199
+
200
+ message = f"""## ๆจกๆ‹Ÿ้œ€ๆฑ‚
201
+
202
+ {simulation_requirement}
203
+
204
+ ## ๆ–‡ๆกฃๅ†…ๅฎน
205
+
206
+ {combined_text}
207
+ """
208
+
209
+ if additional_context:
210
+ message += f"""
211
+ ## ้ขๅค–่ฏดๆ˜Ž
212
+
213
+ {additional_context}
214
+ """
215
+
216
+ message += """
217
+ ่ฏทๆ นๆฎไปฅไธŠๅ†…ๅฎน๏ผŒ่ฎพ่ฎก้€‚ๅˆ็คพไผš่ˆ†่ฎบๆจกๆ‹Ÿ็š„ๅฎžไฝ“็ฑปๅž‹ๅ’Œๅ…ณ็ณป็ฑปๅž‹ใ€‚
218
+ ่ฎฐไฝ๏ผšๆ‰€ๆœ‰ๅฎžไฝ“็ฑปๅž‹ๅฟ…้กปๆ˜ฏ็Žฐๅฎžไธญๅฏไปฅๅ‘ๅฃฐ็š„ไธปไฝ“๏ผŒไธ่ƒฝๆ˜ฏๆŠฝ่ฑกๆฆ‚ๅฟตใ€‚
219
+ """
220
+
221
+ return message
222
+
223
+ def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]:
224
+ """้ชŒ่ฏๅ’ŒๅŽๅค„็†็ป“ๆžœ"""
225
+
226
+ # ็กฎไฟๅฟ…่ฆๅญ—ๆฎตๅญ˜ๅœจ
227
+ if "entity_types" not in result:
228
+ result["entity_types"] = []
229
+ if "edge_types" not in result:
230
+ result["edge_types"] = []
231
+ if "analysis_summary" not in result:
232
+ result["analysis_summary"] = ""
233
+
234
+ # ้ชŒ่ฏๅฎžไฝ“็ฑปๅž‹
235
+ for entity in result["entity_types"]:
236
+ if "attributes" not in entity:
237
+ entity["attributes"] = []
238
+ if "examples" not in entity:
239
+ entity["examples"] = []
240
+ # ็กฎไฟdescriptionไธ่ถ…่ฟ‡100ๅญ—็ฌฆ
241
+ if len(entity.get("description", "")) > 100:
242
+ entity["description"] = entity["description"][:97] + "..."
243
+
244
+ # ้ชŒ่ฏๅ…ณ็ณป็ฑปๅž‹
245
+ for edge in result["edge_types"]:
246
+ if "source_targets" not in edge:
247
+ edge["source_targets"] = []
248
+ if "attributes" not in edge:
249
+ edge["attributes"] = []
250
+ if len(edge.get("description", "")) > 100:
251
+ edge["description"] = edge["description"][:97] + "..."
252
+
253
+ return result
254
+
255
+ def generate_python_code(self, ontology: Dict[str, Any]) -> str:
256
+ """
257
+ ๅฐ†ๆœฌไฝ“ๅฎšไน‰่ฝฌๆขไธบPythonไปฃ็ ๏ผˆ็ฑปไผผontology.py๏ผ‰
258
+
259
+ Args:
260
+ ontology: ๆœฌไฝ“ๅฎšไน‰
261
+
262
+ Returns:
263
+ Pythonไปฃ็ ๅญ—็ฌฆไธฒ
264
+ """
265
+ code_lines = [
266
+ '"""',
267
+ '่‡ชๅฎšไน‰ๅฎžไฝ“็ฑปๅž‹ๅฎšไน‰',
268
+ '็”ฑMiroFish่‡ชๅŠจ็”Ÿๆˆ๏ผŒ็”จไบŽ็คพไผš่ˆ†่ฎบๆจกๆ‹Ÿ',
269
+ '"""',
270
+ '',
271
+ 'from pydantic import Field',
272
+ 'from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel',
273
+ '',
274
+ '',
275
+ '# ============== ๅฎžไฝ“็ฑปๅž‹ๅฎšไน‰ ==============',
276
+ '',
277
+ ]
278
+
279
+ # ็”Ÿๆˆๅฎžไฝ“็ฑปๅž‹
280
+ for entity in ontology.get("entity_types", []):
281
+ name = entity["name"]
282
+ desc = entity.get("description", f"A {name} entity.")
283
+
284
+ code_lines.append(f'class {name}(EntityModel):')
285
+ code_lines.append(f' """{desc}"""')
286
+
287
+ attrs = entity.get("attributes", [])
288
+ if attrs:
289
+ for attr in attrs:
290
+ attr_name = attr["name"]
291
+ attr_desc = attr.get("description", attr_name)
292
+ code_lines.append(f' {attr_name}: EntityText = Field(')
293
+ code_lines.append(f' description="{attr_desc}",')
294
+ code_lines.append(f' default=None')
295
+ code_lines.append(f' )')
296
+ else:
297
+ code_lines.append(' pass')
298
+
299
+ code_lines.append('')
300
+ code_lines.append('')
301
+
302
+ code_lines.append('# ============== ๅ…ณ็ณป็ฑปๅž‹ๅฎšไน‰ ==============')
303
+ code_lines.append('')
304
+
305
+ # ็”Ÿๆˆๅ…ณ็ณป็ฑปๅž‹
306
+ for edge in ontology.get("edge_types", []):
307
+ name = edge["name"]
308
+ # ่ฝฌๆขไธบPascalCase็ฑปๅ
309
+ class_name = ''.join(word.capitalize() for word in name.split('_'))
310
+ desc = edge.get("description", f"A {name} relationship.")
311
+
312
+ code_lines.append(f'class {class_name}(EdgeModel):')
313
+ code_lines.append(f' """{desc}"""')
314
+
315
+ attrs = edge.get("attributes", [])
316
+ if attrs:
317
+ for attr in attrs:
318
+ attr_name = attr["name"]
319
+ attr_desc = attr.get("description", attr_name)
320
+ code_lines.append(f' {attr_name}: EntityText = Field(')
321
+ code_lines.append(f' description="{attr_desc}",')
322
+ code_lines.append(f' default=None')
323
+ code_lines.append(f' )')
324
+ else:
325
+ code_lines.append(' pass')
326
+
327
+ code_lines.append('')
328
+ code_lines.append('')
329
+
330
+ # ็”Ÿๆˆ็ฑปๅž‹ๅญ—ๅ…ธ
331
+ code_lines.append('# ============== ็ฑปๅž‹้…็ฝฎ ==============')
332
+ code_lines.append('')
333
+ code_lines.append('ENTITY_TYPES = {')
334
+ for entity in ontology.get("entity_types", []):
335
+ name = entity["name"]
336
+ code_lines.append(f' "{name}": {name},')
337
+ code_lines.append('}')
338
+ code_lines.append('')
339
+ code_lines.append('EDGE_TYPES = {')
340
+ for edge in ontology.get("edge_types", []):
341
+ name = edge["name"]
342
+ class_name = ''.join(word.capitalize() for word in name.split('_'))
343
+ code_lines.append(f' "{name}": {class_name},')
344
+ code_lines.append('}')
345
+ code_lines.append('')
346
+
347
+ # ็”Ÿๆˆ่พน็š„source_targetsๆ˜ ๅฐ„
348
+ code_lines.append('EDGE_SOURCE_TARGETS = {')
349
+ for edge in ontology.get("edge_types", []):
350
+ name = edge["name"]
351
+ source_targets = edge.get("source_targets", [])
352
+ if source_targets:
353
+ st_list = ', '.join([
354
+ f'{{"source": "{st.get("source", "Entity")}", "target": "{st.get("target", "Entity")}"}}'
355
+ for st in source_targets
356
+ ])
357
+ code_lines.append(f' "{name}": [{st_list}],')
358
+ code_lines.append('}')
359
+
360
+ return '\n'.join(code_lines)
361
+
backend/app/services/text_processor.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๆ–‡ๆœฌๅค„็†ๆœๅŠก
3
+ """
4
+
5
+ from typing import List, Optional
6
+ from ..utils.file_parser import FileParser, split_text_into_chunks
7
+
8
+
9
+ class TextProcessor:
10
+ """ๆ–‡ๆœฌๅค„็†ๅ™จ"""
11
+
12
+ @staticmethod
13
+ def extract_from_files(file_paths: List[str]) -> str:
14
+ """ไปŽๅคšไธชๆ–‡ไปถๆๅ–ๆ–‡ๆœฌ"""
15
+ return FileParser.extract_from_multiple(file_paths)
16
+
17
+ @staticmethod
18
+ def split_text(
19
+ text: str,
20
+ chunk_size: int = 500,
21
+ overlap: int = 50
22
+ ) -> List[str]:
23
+ """
24
+ ๅˆ†ๅ‰ฒๆ–‡ๆœฌ
25
+
26
+ Args:
27
+ text: ๅŽŸๅง‹ๆ–‡ๆœฌ
28
+ chunk_size: ๅ—ๅคงๅฐ
29
+ overlap: ้‡ๅ ๅคงๅฐ
30
+
31
+ Returns:
32
+ ๆ–‡ๆœฌๅ—ๅˆ—่กจ
33
+ """
34
+ return split_text_into_chunks(text, chunk_size, overlap)
35
+
36
+ @staticmethod
37
+ def preprocess_text(text: str) -> str:
38
+ """
39
+ ้ข„ๅค„็†ๆ–‡ๆœฌ
40
+ - ็งป้™คๅคšไฝ™็ฉบ็™ฝ
41
+ - ๆ ‡ๅ‡†ๅŒ–ๆข่กŒ
42
+
43
+ Args:
44
+ text: ๅŽŸๅง‹ๆ–‡ๆœฌ
45
+
46
+ Returns:
47
+ ๅค„็†ๅŽ็š„ๆ–‡ๆœฌ
48
+ """
49
+ import re
50
+
51
+ # ๆ ‡ๅ‡†ๅŒ–ๆข่กŒ
52
+ text = text.replace('\r\n', '\n').replace('\r', '\n')
53
+
54
+ # ็งป้™ค่ฟž็ปญ็ฉบ่กŒ๏ผˆไฟ็•™ๆœ€ๅคšไธคไธชๆข่กŒ๏ผ‰
55
+ text = re.sub(r'\n{3,}', '\n\n', text)
56
+
57
+ # ็งป้™ค่กŒ้ฆ–่กŒๅฐพ็ฉบ็™ฝ
58
+ lines = [line.strip() for line in text.split('\n')]
59
+ text = '\n'.join(lines)
60
+
61
+ return text.strip()
62
+
63
+ @staticmethod
64
+ def get_text_stats(text: str) -> dict:
65
+ """่Žทๅ–ๆ–‡ๆœฌ็ปŸ่ฎกไฟกๆฏ"""
66
+ return {
67
+ "total_chars": len(text),
68
+ "total_lines": text.count('\n') + 1,
69
+ "total_words": len(text.split()),
70
+ }
71
+
backend/app/utils/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๅทฅๅ…ทๆจกๅ—
3
+ """
4
+
5
+ from .file_parser import FileParser
6
+ from .llm_client import LLMClient
7
+
8
+ __all__ = ['FileParser', 'LLMClient']
9
+
backend/app/utils/file_parser.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๆ–‡ไปถ่งฃๆžๅทฅๅ…ท
3
+ ๆ”ฏๆŒPDFใ€Markdownใ€TXTๆ–‡ไปถ็š„ๆ–‡ๆœฌๆๅ–
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+ from typing import List, Optional
9
+
10
+
11
+ class FileParser:
12
+ """ๆ–‡ไปถ่งฃๆžๅ™จ"""
13
+
14
+ SUPPORTED_EXTENSIONS = {'.pdf', '.md', '.markdown', '.txt'}
15
+
16
+ @classmethod
17
+ def extract_text(cls, file_path: str) -> str:
18
+ """
19
+ ไปŽๆ–‡ไปถไธญๆๅ–ๆ–‡ๆœฌ
20
+
21
+ Args:
22
+ file_path: ๆ–‡ไปถ่ทฏๅพ„
23
+
24
+ Returns:
25
+ ๆๅ–็š„ๆ–‡ๆœฌๅ†…ๅฎน
26
+ """
27
+ path = Path(file_path)
28
+
29
+ if not path.exists():
30
+ raise FileNotFoundError(f"ๆ–‡ไปถไธๅญ˜ๅœจ: {file_path}")
31
+
32
+ suffix = path.suffix.lower()
33
+
34
+ if suffix not in cls.SUPPORTED_EXTENSIONS:
35
+ raise ValueError(f"ไธๆ”ฏๆŒ็š„ๆ–‡ไปถๆ ผๅผ: {suffix}")
36
+
37
+ if suffix == '.pdf':
38
+ return cls._extract_from_pdf(file_path)
39
+ elif suffix in {'.md', '.markdown'}:
40
+ return cls._extract_from_md(file_path)
41
+ elif suffix == '.txt':
42
+ return cls._extract_from_txt(file_path)
43
+
44
+ raise ValueError(f"ๆ— ๆณ•ๅค„็†็š„ๆ–‡ไปถๆ ผๅผ: {suffix}")
45
+
46
+ @staticmethod
47
+ def _extract_from_pdf(file_path: str) -> str:
48
+ """ไปŽPDFๆๅ–ๆ–‡ๆœฌ"""
49
+ try:
50
+ import fitz # PyMuPDF
51
+ except ImportError:
52
+ raise ImportError("้œ€่ฆๅฎ‰่ฃ…PyMuPDF: pip install PyMuPDF")
53
+
54
+ text_parts = []
55
+ with fitz.open(file_path) as doc:
56
+ for page in doc:
57
+ text = page.get_text()
58
+ if text.strip():
59
+ text_parts.append(text)
60
+
61
+ return "\n\n".join(text_parts)
62
+
63
+ @staticmethod
64
+ def _extract_from_md(file_path: str) -> str:
65
+ """ไปŽMarkdownๆๅ–ๆ–‡ๆœฌ"""
66
+ with open(file_path, 'r', encoding='utf-8') as f:
67
+ return f.read()
68
+
69
+ @staticmethod
70
+ def _extract_from_txt(file_path: str) -> str:
71
+ """ไปŽTXTๆๅ–ๆ–‡ๆœฌ"""
72
+ with open(file_path, 'r', encoding='utf-8') as f:
73
+ return f.read()
74
+
75
+ @classmethod
76
+ def extract_from_multiple(cls, file_paths: List[str]) -> str:
77
+ """
78
+ ไปŽๅคšไธชๆ–‡ไปถๆๅ–ๆ–‡ๆœฌๅนถๅˆๅนถ
79
+
80
+ Args:
81
+ file_paths: ๆ–‡ไปถ่ทฏๅพ„ๅˆ—่กจ
82
+
83
+ Returns:
84
+ ๅˆๅนถๅŽ็š„ๆ–‡ๆœฌ
85
+ """
86
+ all_texts = []
87
+
88
+ for i, file_path in enumerate(file_paths, 1):
89
+ try:
90
+ text = cls.extract_text(file_path)
91
+ filename = Path(file_path).name
92
+ all_texts.append(f"=== ๆ–‡ๆกฃ {i}: {filename} ===\n{text}")
93
+ except Exception as e:
94
+ all_texts.append(f"=== ๆ–‡ๆกฃ {i}: {file_path} (ๆๅ–ๅคฑ่ดฅ: {str(e)}) ===")
95
+
96
+ return "\n\n".join(all_texts)
97
+
98
+
99
+ def split_text_into_chunks(
100
+ text: str,
101
+ chunk_size: int = 500,
102
+ overlap: int = 50
103
+ ) -> List[str]:
104
+ """
105
+ ๅฐ†ๆ–‡ๆœฌๅˆ†ๅ‰ฒๆˆๅฐๅ—
106
+
107
+ Args:
108
+ text: ๅŽŸๅง‹ๆ–‡ๆœฌ
109
+ chunk_size: ๆฏๅ—็š„ๅญ—็ฌฆๆ•ฐ
110
+ overlap: ้‡ๅ ๅญ—็ฌฆๆ•ฐ
111
+
112
+ Returns:
113
+ ๆ–‡ๆœฌๅ—ๅˆ—่กจ
114
+ """
115
+ if len(text) <= chunk_size:
116
+ return [text] if text.strip() else []
117
+
118
+ chunks = []
119
+ start = 0
120
+
121
+ while start < len(text):
122
+ end = start + chunk_size
123
+
124
+ # ๅฐ่ฏ•ๅœจๅฅๅญ่พน็•Œๅค„ๅˆ†ๅ‰ฒ
125
+ if end < len(text):
126
+ # ๆŸฅๆ‰พๆœ€่ฟ‘็š„ๅฅๅญ็ป“ๆŸ็ฌฆ
127
+ for sep in ['ใ€‚', '๏ผ', '๏ผŸ', '.\n', '!\n', '?\n', '\n\n', '. ', '! ', '? ']:
128
+ last_sep = text[start:end].rfind(sep)
129
+ if last_sep != -1 and last_sep > chunk_size * 0.3:
130
+ end = start + last_sep + len(sep)
131
+ break
132
+
133
+ chunk = text[start:end].strip()
134
+ if chunk:
135
+ chunks.append(chunk)
136
+
137
+ # ไธ‹ไธ€ไธชๅ—ไปŽ้‡ๅ ไฝ็ฝฎๅผ€ๅง‹
138
+ start = end - overlap if end < len(text) else len(text)
139
+
140
+ return chunks
141
+
backend/app/utils/llm_client.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLMๅฎขๆˆท็ซฏๅฐ่ฃ…
3
+ ็ปŸไธ€ไฝฟ็”จOpenAIๆ ผๅผ่ฐƒ็”จ
4
+ """
5
+
6
+ import json
7
+ from typing import Optional, Dict, Any, List
8
+ from openai import OpenAI
9
+
10
+ from ..config import Config
11
+
12
+
13
+ class LLMClient:
14
+ """LLMๅฎขๆˆท็ซฏ"""
15
+
16
+ def __init__(
17
+ self,
18
+ api_key: Optional[str] = None,
19
+ base_url: Optional[str] = None,
20
+ model: Optional[str] = None
21
+ ):
22
+ self.api_key = api_key or Config.LLM_API_KEY
23
+ self.base_url = base_url or Config.LLM_BASE_URL
24
+ self.model = model or Config.LLM_MODEL_NAME
25
+
26
+ if not self.api_key:
27
+ raise ValueError("LLM_API_KEY ๆœช้…็ฝฎ")
28
+
29
+ self.client = OpenAI(
30
+ api_key=self.api_key,
31
+ base_url=self.base_url
32
+ )
33
+
34
+ def chat(
35
+ self,
36
+ messages: List[Dict[str, str]],
37
+ temperature: float = 0.7,
38
+ max_tokens: int = 4096,
39
+ response_format: Optional[Dict] = None
40
+ ) -> str:
41
+ """
42
+ ๅ‘้€่Šๅคฉ่ฏทๆฑ‚
43
+
44
+ Args:
45
+ messages: ๆถˆๆฏๅˆ—่กจ
46
+ temperature: ๆธฉๅบฆๅ‚ๆ•ฐ
47
+ max_tokens: ๆœ€ๅคงtokenๆ•ฐ
48
+ response_format: ๅ“ๅบ”ๆ ผๅผ๏ผˆๅฆ‚JSONๆจกๅผ๏ผ‰
49
+
50
+ Returns:
51
+ ๆจกๅž‹ๅ“ๅบ”ๆ–‡ๆœฌ
52
+ """
53
+ kwargs = {
54
+ "model": self.model,
55
+ "messages": messages,
56
+ "temperature": temperature,
57
+ "max_tokens": max_tokens,
58
+ }
59
+
60
+ if response_format:
61
+ kwargs["response_format"] = response_format
62
+
63
+ response = self.client.chat.completions.create(**kwargs)
64
+ return response.choices[0].message.content
65
+
66
+ def chat_json(
67
+ self,
68
+ messages: List[Dict[str, str]],
69
+ temperature: float = 0.3,
70
+ max_tokens: int = 4096
71
+ ) -> Dict[str, Any]:
72
+ """
73
+ ๅ‘้€่Šๅคฉ่ฏทๆฑ‚ๅนถ่ฟ”ๅ›žJSON
74
+
75
+ Args:
76
+ messages: ๆถˆๆฏๅˆ—่กจ
77
+ temperature: ๆธฉๅบฆๅ‚ๆ•ฐ
78
+ max_tokens: ๆœ€ๅคงtokenๆ•ฐ
79
+
80
+ Returns:
81
+ ่งฃๆžๅŽ็š„JSONๅฏน่ฑก
82
+ """
83
+ response = self.chat(
84
+ messages=messages,
85
+ temperature=temperature,
86
+ max_tokens=max_tokens,
87
+ response_format={"type": "json_object"}
88
+ )
89
+
90
+ return json.loads(response)
91
+
backend/app/utils/logger.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ๆ—ฅๅฟ—้…็ฝฎๆจกๅ—
3
+ ๆไพ›็ปŸไธ€็š„ๆ—ฅๅฟ—็ฎก็†๏ผŒๅŒๆ—ถ่พ“ๅ‡บๅˆฐๆŽงๅˆถๅฐๅ’Œๆ–‡ไปถ
4
+ """
5
+
6
+ import os
7
+ import logging
8
+ from datetime import datetime
9
+ from logging.handlers import RotatingFileHandler
10
+
11
+
12
+ # ๆ—ฅๅฟ—็›ฎๅฝ•
13
+ LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'logs')
14
+
15
+
16
+ def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.Logger:
17
+ """
18
+ ่ฎพ็ฝฎๆ—ฅๅฟ—ๅ™จ
19
+
20
+ Args:
21
+ name: ๆ—ฅๅฟ—ๅ™จๅ็งฐ
22
+ level: ๆ—ฅๅฟ—็บงๅˆซ
23
+
24
+ Returns:
25
+ ้…็ฝฎๅฅฝ็š„ๆ—ฅๅฟ—ๅ™จ
26
+ """
27
+ # ็กฎไฟๆ—ฅๅฟ—็›ฎๅฝ•ๅญ˜ๅœจ
28
+ os.makedirs(LOG_DIR, exist_ok=True)
29
+
30
+ # ๅˆ›ๅปบๆ—ฅๅฟ—ๅ™จ
31
+ logger = logging.getLogger(name)
32
+ logger.setLevel(level)
33
+
34
+ # ๅฆ‚ๆžœๅทฒ็ปๆœ‰ๅค„็†ๅ™จ๏ผŒไธ้‡ๅคๆทปๅŠ 
35
+ if logger.handlers:
36
+ return logger
37
+
38
+ # ๆ—ฅๅฟ—ๆ ผๅผ
39
+ detailed_formatter = logging.Formatter(
40
+ '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s',
41
+ datefmt='%Y-%m-%d %H:%M:%S'
42
+ )
43
+
44
+ simple_formatter = logging.Formatter(
45
+ '[%(asctime)s] %(levelname)s: %(message)s',
46
+ datefmt='%H:%M:%S'
47
+ )
48
+
49
+ # 1. ๆ–‡ไปถๅค„็†ๅ™จ - ่ฏฆ็ป†ๆ—ฅๅฟ—๏ผˆๆŒ‰ๆ—ฅๆœŸๅ‘ฝๅ๏ผŒๅธฆ่ฝฎ่ฝฌ๏ผ‰
50
+ log_filename = datetime.now().strftime('%Y-%m-%d') + '.log'
51
+ file_handler = RotatingFileHandler(
52
+ os.path.join(LOG_DIR, log_filename),
53
+ maxBytes=10 * 1024 * 1024, # 10MB
54
+ backupCount=5,
55
+ encoding='utf-8'
56
+ )
57
+ file_handler.setLevel(logging.DEBUG)
58
+ file_handler.setFormatter(detailed_formatter)
59
+
60
+ # 2. ๆŽงๅˆถๅฐๅค„็†ๅ™จ - ็ฎ€ๆดๆ—ฅๅฟ—๏ผˆINFOๅŠไปฅไธŠ๏ผ‰
61
+ console_handler = logging.StreamHandler()
62
+ console_handler.setLevel(logging.INFO)
63
+ console_handler.setFormatter(simple_formatter)
64
+
65
+ # ๆทปๅŠ ๅค„็†ๅ™จ
66
+ logger.addHandler(file_handler)
67
+ logger.addHandler(console_handler)
68
+
69
+ return logger
70
+
71
+
72
+ def get_logger(name: str = 'mirofish') -> logging.Logger:
73
+ """
74
+ ่Žทๅ–ๆ—ฅๅฟ—ๅ™จ๏ผˆๅฆ‚ๆžœไธๅญ˜ๅœจๅˆ™ๅˆ›ๅปบ๏ผ‰
75
+
76
+ Args:
77
+ name: ๆ—ฅๅฟ—ๅ™จๅ็งฐ
78
+
79
+ Returns:
80
+ ๆ—ฅๅฟ—ๅ™จๅฎžไพ‹
81
+ """
82
+ logger = logging.getLogger(name)
83
+ if not logger.handlers:
84
+ return setup_logger(name)
85
+ return logger
86
+
87
+
88
+ # ๅˆ›ๅปบ้ป˜่ฎคๆ—ฅๅฟ—ๅ™จ
89
+ logger = setup_logger()
90
+
91
+
92
+ # ไพฟๆทๆ–นๆณ•
93
+ def debug(msg, *args, **kwargs):
94
+ logger.debug(msg, *args, **kwargs)
95
+
96
+ def info(msg, *args, **kwargs):
97
+ logger.info(msg, *args, **kwargs)
98
+
99
+ def warning(msg, *args, **kwargs):
100
+ logger.warning(msg, *args, **kwargs)
101
+
102
+ def error(msg, *args, **kwargs):
103
+ logger.error(msg, *args, **kwargs)
104
+
105
+ def critical(msg, *args, **kwargs):
106
+ logger.critical(msg, *args, **kwargs)
107
+
backend/requirements.txt ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Flaskๆก†ๆžถ
2
+ flask>=3.0.0
3
+ flask-cors>=4.0.0
4
+
5
+ # Zep Cloud SDK
6
+ zep-cloud>=2.0.0
7
+
8
+ # OpenAI SDK๏ผˆ็”จไบŽLLM่ฐƒ็”จ๏ผ‰
9
+ openai>=1.0.0
10
+
11
+ # PDFๅค„็†
12
+ PyMuPDF>=1.24.0
13
+
14
+ # ็Žฏๅขƒๅ˜้‡
15
+ python-dotenv>=1.0.0
16
+
17
+ # ๆ•ฐๆฎ้ชŒ่ฏ
18
+ pydantic>=2.0.0
19
+
20
+ # ๆ–‡ไปถๅค„็†
21
+ werkzeug>=3.0.0
22
+
backend/run.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MiroFish Backend ๅฏๅŠจๅ…ฅๅฃ
3
+ """
4
+
5
+ import os
6
+ import sys
7
+
8
+ # ๆทปๅŠ ้กน็›ฎๆ น็›ฎๅฝ•ๅˆฐ่ทฏๅพ„
9
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
10
+
11
+ from app import create_app
12
+ from app.config import Config
13
+
14
+
15
+ def main():
16
+ """ไธปๅ‡ฝๆ•ฐ"""
17
+ # ้ชŒ่ฏ้…็ฝฎ
18
+ errors = Config.validate()
19
+ if errors:
20
+ print("้…็ฝฎ้”™่ฏฏ:")
21
+ for err in errors:
22
+ print(f" - {err}")
23
+ print("\n่ฏทๆฃ€ๆŸฅ .env ๆ–‡ไปถไธญ็š„้…็ฝฎ")
24
+ sys.exit(1)
25
+
26
+ # ๅˆ›ๅปบๅบ”็”จ
27
+ app = create_app()
28
+
29
+ # ่Žทๅ–่ฟ่กŒ้…็ฝฎ
30
+ host = os.environ.get('FLASK_HOST', '0.0.0.0')
31
+ port = int(os.environ.get('FLASK_PORT', 5001))
32
+ debug = Config.DEBUG
33
+
34
+ print(f"""
35
+ โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
36
+ โ•‘ MiroFish Backend Server โ•‘
37
+ โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
38
+ โ•‘ Running on: http://{host}:{port}
39
+ โ•‘ Debug mode: {debug}
40
+ โ•‘
41
+ โ•‘ API Endpoints:
42
+ โ•‘ POST /api/graph/ontology/generate - ็”Ÿๆˆๆœฌไฝ“
43
+ โ•‘ POST /api/graph/build - ๆž„ๅปบๅ›พ่ฐฑ
44
+ โ•‘ GET /api/graph/task/<task_id> - ๆŸฅ่ฏขไปปๅŠก
45
+ โ•‘ GET /api/graph/tasks - ๅˆ—ๅ‡บไปปๅŠก
46
+ โ•‘ GET /api/graph/data/<graph_id> - ่Žทๅ–ๅ›พๆ•ฐๆฎ
47
+ โ•‘ DELETE /api/graph/delete/<graph_id>- ๅˆ ้™คๅ›พ่ฐฑ
48
+ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
49
+ """)
50
+
51
+ # ๅฏๅŠจๆœๅŠก
52
+ app.run(host=host, port=port, debug=debug, threaded=True)
53
+
54
+
55
+ if __name__ == '__main__':
56
+ main()
57
+