engine7 7.1.23 → 7.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +15 -5
- package/dist/engine-startup.mjs +786 -311
- package/dist/main.mjs +787 -312
- package/package.json +3 -1
- package/src/memory/everos/python/__pycache__/agentic_search.cpython-314.pyc +0 -0
- package/src/memory/everos/python/__pycache__/agentic_server.cpython-314.pyc +0 -0
- package/src/memory/everos/python/agentic_search.py +980 -0
- package/src/memory/everos/python/agentic_server.py +383 -0
- package/src/memory/everos/python/fcntl_compat.py +23 -0
- package/src/memory/everos/python/requirements.txt +7 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Agentic Search Server — FastAPI wrapper around the 3-mode search pipeline.
|
|
4
|
+
|
|
5
|
+
Modes:
|
|
6
|
+
hybrid — hybrid + rerank (~2s)
|
|
7
|
+
hybrid_agentic — hybrid + rerank + LLM sufficiency + optional multi_query (~10s)
|
|
8
|
+
agentic — full cluster-scoped pipeline (~40s)
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
uvicorn agentic_server:app --host 127.0.0.1 --port 8101
|
|
12
|
+
|
|
13
|
+
Or directly:
|
|
14
|
+
python agentic_server.py --port 8101
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from typing import Any, Literal
|
|
23
|
+
|
|
24
|
+
from fastapi import FastAPI
|
|
25
|
+
from pydantic import BaseModel
|
|
26
|
+
|
|
27
|
+
# Import all search logic from agentic_search.py
|
|
28
|
+
from agentic_search import (
|
|
29
|
+
Episode,
|
|
30
|
+
AgenticResult,
|
|
31
|
+
hybrid_search,
|
|
32
|
+
rerank,
|
|
33
|
+
rrf_fusion,
|
|
34
|
+
DB,
|
|
35
|
+
cluster_scoped_search,
|
|
36
|
+
check_sufficiency,
|
|
37
|
+
generate_multi_queries,
|
|
38
|
+
generate_refined_query,
|
|
39
|
+
format_docs,
|
|
40
|
+
dedup_by_id,
|
|
41
|
+
# Hyperparams
|
|
42
|
+
ROUND1_TOP_N,
|
|
43
|
+
ROUND1_RERANK_TOP_N,
|
|
44
|
+
ROUND2_CAP,
|
|
45
|
+
CLUSTER_BASE_CANDIDATES,
|
|
46
|
+
MULTI_QUERY_COUNT,
|
|
47
|
+
HYBRID_RRF_K,
|
|
48
|
+
REFINEMENT_STRATEGY,
|
|
49
|
+
)
|
|
50
|
+
import asyncio
|
|
51
|
+
|
|
52
|
+
app = FastAPI(title="Agentic Search Server", version="1.0.0")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── Request/Response models ───────────────────────────────────────────
|
|
56
|
+
class SearchRequest(BaseModel):
|
|
57
|
+
query: str
|
|
58
|
+
user_id: str = "xiaomei"
|
|
59
|
+
app_id: str | None = None
|
|
60
|
+
project_id: str | None = None
|
|
61
|
+
mode: Literal["hybrid", "hybrid_agentic", "agentic"] = "hybrid_agentic"
|
|
62
|
+
top_k: int = 5
|
|
63
|
+
strategy: Literal["multi_query", "refined_query"] = "multi_query"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class EpisodeItem(BaseModel):
|
|
67
|
+
id: str
|
|
68
|
+
subject: str
|
|
69
|
+
summary: str
|
|
70
|
+
episode: str
|
|
71
|
+
timestamp: str
|
|
72
|
+
score: float
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SearchResponse(BaseModel):
|
|
76
|
+
episodes: list[EpisodeItem]
|
|
77
|
+
mode: str
|
|
78
|
+
method: str
|
|
79
|
+
is_sufficient: bool = False
|
|
80
|
+
multi_queries: list[str] = []
|
|
81
|
+
reasoning: str = ""
|
|
82
|
+
timing: dict[str, float] = {}
|
|
83
|
+
total_time: float = 0.0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── Helpers ───────────────────────────────────────────────────────────
|
|
87
|
+
def episode_to_item(ep: Episode) -> EpisodeItem:
|
|
88
|
+
return EpisodeItem(
|
|
89
|
+
id=ep.id,
|
|
90
|
+
subject=ep.subject[:200],
|
|
91
|
+
summary=ep.summary[:500],
|
|
92
|
+
episode=ep.episode[:1000],
|
|
93
|
+
timestamp=ep.timestamp[:30],
|
|
94
|
+
score=ep.score,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def result_to_response(result: AgenticResult) -> SearchResponse:
|
|
99
|
+
return SearchResponse(
|
|
100
|
+
episodes=[episode_to_item(ep) for ep in result.episodes],
|
|
101
|
+
mode=result.method,
|
|
102
|
+
method=result.method,
|
|
103
|
+
is_sufficient=result.is_sufficient,
|
|
104
|
+
multi_queries=result.multi_queries,
|
|
105
|
+
reasoning=result.reasoning,
|
|
106
|
+
timing=result.timing,
|
|
107
|
+
total_time=result.timing.get("total", 0.0),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── Endpoints ─────────────────────────────────────────────────────────
|
|
112
|
+
@app.get("/health")
|
|
113
|
+
def health():
|
|
114
|
+
return {"status": "ok"}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.post("/api/v1/search", response_model=SearchResponse)
|
|
118
|
+
async def search(req: SearchRequest):
|
|
119
|
+
"""Unified search endpoint — 3 modes."""
|
|
120
|
+
t_total = time.time()
|
|
121
|
+
|
|
122
|
+
uid = req.user_id
|
|
123
|
+
aid = req.app_id or uid
|
|
124
|
+
pid = req.project_id or "default"
|
|
125
|
+
|
|
126
|
+
if req.mode == "hybrid":
|
|
127
|
+
# Mode 1: hybrid + rerank only
|
|
128
|
+
eps, _ = hybrid_search(req.query, top_k=ROUND1_TOP_N, user_id=uid, app_id=aid, project_id=pid)
|
|
129
|
+
reranked, _ = rerank(req.query, eps)
|
|
130
|
+
reranked = reranked[: req.top_k]
|
|
131
|
+
result = AgenticResult(
|
|
132
|
+
episodes=reranked,
|
|
133
|
+
method="hybrid+rerank",
|
|
134
|
+
timing={"total": time.time() - t_total},
|
|
135
|
+
)
|
|
136
|
+
return result_to_response(result)
|
|
137
|
+
|
|
138
|
+
elif req.mode == "hybrid_agentic":
|
|
139
|
+
# Mode 2: hybrid + rerank + sufficiency + optional multi_query
|
|
140
|
+
result = await _hybrid_agentic(req, uid, aid, pid)
|
|
141
|
+
return result_to_response(result)
|
|
142
|
+
|
|
143
|
+
elif req.mode == "agentic":
|
|
144
|
+
# Mode 3: full cluster-scoped agentic
|
|
145
|
+
result = await _full_agentic(req, uid, aid, pid)
|
|
146
|
+
return result_to_response(result)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── Mode 2: hybrid_agentic ────────────────────────────────────────────
|
|
150
|
+
async def _hybrid_agentic(req: SearchRequest, uid: str, aid: str, pid: str) -> AgenticResult:
|
|
151
|
+
timing = {}
|
|
152
|
+
t_total = time.time()
|
|
153
|
+
|
|
154
|
+
# Round 1: hybrid + rerank
|
|
155
|
+
t0 = time.time()
|
|
156
|
+
eps, _ = hybrid_search(req.query, top_k=ROUND1_TOP_N, user_id=uid, app_id=aid, project_id=pid)
|
|
157
|
+
if not eps:
|
|
158
|
+
return AgenticResult(episodes=[], method="hybrid_agentic (empty)", timing={"total": time.time() - t_total})
|
|
159
|
+
reranked, _ = rerank(req.query, eps)
|
|
160
|
+
reranked = reranked[:ROUND1_RERANK_TOP_N]
|
|
161
|
+
timing["hybrid_rerank"] = time.time() - t0
|
|
162
|
+
|
|
163
|
+
# Sufficiency
|
|
164
|
+
t0 = time.time()
|
|
165
|
+
try:
|
|
166
|
+
sufficiency = check_sufficiency(req.query, reranked)
|
|
167
|
+
except Exception:
|
|
168
|
+
timing["sufficiency"] = time.time() - t0
|
|
169
|
+
return AgenticResult(
|
|
170
|
+
episodes=reranked[: req.top_k],
|
|
171
|
+
method="hybrid_agentic (sufficiency_error)",
|
|
172
|
+
timing={**timing, "total": time.time() - t_total},
|
|
173
|
+
)
|
|
174
|
+
timing["sufficiency"] = time.time() - t0
|
|
175
|
+
|
|
176
|
+
if sufficiency.get("is_sufficient", True):
|
|
177
|
+
return AgenticResult(
|
|
178
|
+
episodes=reranked[: req.top_k],
|
|
179
|
+
method="hybrid_agentic (sufficient_r1)",
|
|
180
|
+
is_sufficient=True,
|
|
181
|
+
reasoning=sufficiency.get("reasoning", ""),
|
|
182
|
+
timing={**timing, "total": time.time() - t_total},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Round 2
|
|
186
|
+
missing = sufficiency.get("missing_information", [])
|
|
187
|
+
key_info = sufficiency.get("key_information_found", [])
|
|
188
|
+
|
|
189
|
+
if req.strategy == "refined_query":
|
|
190
|
+
t0 = time.time()
|
|
191
|
+
try:
|
|
192
|
+
refined = generate_refined_query(req.query, reranked, missing)
|
|
193
|
+
except Exception:
|
|
194
|
+
refined = req.query
|
|
195
|
+
timing["refined_query_gen"] = time.time() - t0
|
|
196
|
+
|
|
197
|
+
t0 = time.time()
|
|
198
|
+
r2_eps, _ = hybrid_search(refined, top_k=ROUND1_TOP_N, user_id=uid, app_id=aid, project_id=pid)
|
|
199
|
+
timing["round2_search"] = time.time() - t0
|
|
200
|
+
|
|
201
|
+
seen = {ep.id for ep in reranked}
|
|
202
|
+
r2_unique = [ep for ep in r2_eps if ep.id not in seen]
|
|
203
|
+
keep = max(0, ROUND2_CAP - len(reranked))
|
|
204
|
+
r2_unique = r2_unique[:keep]
|
|
205
|
+
merged = list(reranked) + r2_unique
|
|
206
|
+
|
|
207
|
+
t0 = time.time()
|
|
208
|
+
final, _ = rerank(req.query, merged)
|
|
209
|
+
final = final[: req.top_k]
|
|
210
|
+
timing["final_rerank"] = time.time() - t0
|
|
211
|
+
|
|
212
|
+
return AgenticResult(
|
|
213
|
+
episodes=final,
|
|
214
|
+
method="hybrid_agentic (refined_query)",
|
|
215
|
+
multi_queries=[refined],
|
|
216
|
+
reasoning=sufficiency.get("reasoning", ""),
|
|
217
|
+
timing={**timing, "total": time.time() - t_total},
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# multi_query path
|
|
221
|
+
t0 = time.time()
|
|
222
|
+
try:
|
|
223
|
+
multi_queries = generate_multi_queries(req.query, reranked, missing, key_info)
|
|
224
|
+
except Exception:
|
|
225
|
+
multi_queries = []
|
|
226
|
+
timing["multi_query_gen"] = time.time() - t0
|
|
227
|
+
|
|
228
|
+
if not multi_queries:
|
|
229
|
+
return AgenticResult(
|
|
230
|
+
episodes=reranked[: req.top_k],
|
|
231
|
+
method="hybrid_agentic (multi_query_failed)",
|
|
232
|
+
reasoning=sufficiency.get("reasoning", ""),
|
|
233
|
+
timing={**timing, "total": time.time() - t_total},
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
t0 = time.time()
|
|
237
|
+
round2_lists = []
|
|
238
|
+
for mq in multi_queries:
|
|
239
|
+
r2_eps, _ = hybrid_search(mq, top_k=ROUND1_TOP_N, user_id=uid, app_id=aid, project_id=pid)
|
|
240
|
+
round2_lists.append(r2_eps)
|
|
241
|
+
timing["round2_search"] = time.time() - t0
|
|
242
|
+
|
|
243
|
+
if len(round2_lists) == 1:
|
|
244
|
+
fused = round2_lists[0]
|
|
245
|
+
else:
|
|
246
|
+
fused = rrf_fusion(*round2_lists, k=HYBRID_RRF_K)
|
|
247
|
+
|
|
248
|
+
seen = {ep.id for ep in reranked}
|
|
249
|
+
r2_unique = [ep for ep in fused if ep.id not in seen]
|
|
250
|
+
keep = max(0, ROUND2_CAP - len(reranked))
|
|
251
|
+
r2_unique = r2_unique[:keep]
|
|
252
|
+
merged = list(reranked) + r2_unique
|
|
253
|
+
|
|
254
|
+
t0 = time.time()
|
|
255
|
+
final, _ = rerank(req.query, merged)
|
|
256
|
+
final = final[: req.top_k]
|
|
257
|
+
timing["final_rerank"] = time.time() - t0
|
|
258
|
+
|
|
259
|
+
return AgenticResult(
|
|
260
|
+
episodes=final,
|
|
261
|
+
method="hybrid_agentic (multi_round)",
|
|
262
|
+
multi_queries=multi_queries,
|
|
263
|
+
reasoning=sufficiency.get("reasoning", ""),
|
|
264
|
+
timing={**timing, "total": time.time() - t_total},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ── Mode 3: full agentic (cluster-scoped) ─────────────────────────────
|
|
269
|
+
async def _full_agentic(req: SearchRequest, uid: str, aid: str, pid: str) -> AgenticResult:
|
|
270
|
+
timing = {}
|
|
271
|
+
t_total = time.time()
|
|
272
|
+
|
|
273
|
+
db = DB()
|
|
274
|
+
|
|
275
|
+
t0 = time.time()
|
|
276
|
+
clusters = db.get_clusters()
|
|
277
|
+
all_episodes = db.get_all_episodes()
|
|
278
|
+
timing["load_corpus"] = time.time() - t0
|
|
279
|
+
|
|
280
|
+
t0 = time.time()
|
|
281
|
+
round1 = cluster_scoped_search(req.query, all_episodes, clusters)
|
|
282
|
+
|
|
283
|
+
if not round1:
|
|
284
|
+
fb_eps, _ = hybrid_search(req.query, top_k=req.top_k * 2, user_id=uid, app_id=aid, project_id=pid)
|
|
285
|
+
fb_reranked, rr_time = rerank(req.query, fb_eps)
|
|
286
|
+
fb_reranked = fb_reranked[: req.top_k]
|
|
287
|
+
timing["fallback_hybrid"] = time.time() - t0
|
|
288
|
+
return AgenticResult(
|
|
289
|
+
episodes=fb_reranked,
|
|
290
|
+
method="fallback_hybrid",
|
|
291
|
+
timing={**timing, "total": time.time() - t_total},
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
timing["cluster_scoped"] = time.time() - t0
|
|
295
|
+
|
|
296
|
+
t0 = time.time()
|
|
297
|
+
reranked, _ = rerank(req.query, round1)
|
|
298
|
+
reranked = reranked[:ROUND1_RERANK_TOP_N]
|
|
299
|
+
timing["round1_rerank"] = time.time() - t0
|
|
300
|
+
|
|
301
|
+
t0 = time.time()
|
|
302
|
+
try:
|
|
303
|
+
sufficiency = check_sufficiency(req.query, reranked)
|
|
304
|
+
except Exception:
|
|
305
|
+
timing["sufficiency"] = time.time() - t0
|
|
306
|
+
return AgenticResult(
|
|
307
|
+
episodes=reranked[: req.top_k],
|
|
308
|
+
method="agentic (sufficiency_error)",
|
|
309
|
+
timing={**timing, "total": time.time() - t_total},
|
|
310
|
+
)
|
|
311
|
+
timing["sufficiency"] = time.time() - t0
|
|
312
|
+
|
|
313
|
+
if sufficiency.get("is_sufficient", True):
|
|
314
|
+
return AgenticResult(
|
|
315
|
+
episodes=reranked[: req.top_k],
|
|
316
|
+
method="agentic (sufficient_r1)",
|
|
317
|
+
is_sufficient=True,
|
|
318
|
+
reasoning=sufficiency.get("reasoning", ""),
|
|
319
|
+
timing={**timing, "total": time.time() - t_total},
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Round 2 (same as hybrid_agentic but base_retrieve is hybrid_full)
|
|
323
|
+
missing = sufficiency.get("missing_information", [])
|
|
324
|
+
key_info = sufficiency.get("key_information_found", [])
|
|
325
|
+
|
|
326
|
+
t0 = time.time()
|
|
327
|
+
try:
|
|
328
|
+
multi_queries = generate_multi_queries(req.query, reranked, missing, key_info)
|
|
329
|
+
except Exception:
|
|
330
|
+
multi_queries = []
|
|
331
|
+
timing["multi_query_gen"] = time.time() - t0
|
|
332
|
+
|
|
333
|
+
if not multi_queries:
|
|
334
|
+
return AgenticResult(
|
|
335
|
+
episodes=reranked[: req.top_k],
|
|
336
|
+
method="agentic (multi_query_failed)",
|
|
337
|
+
reasoning=sufficiency.get("reasoning", ""),
|
|
338
|
+
timing={**timing, "total": time.time() - t_total},
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
t0 = time.time()
|
|
342
|
+
round2_lists = []
|
|
343
|
+
for mq in multi_queries:
|
|
344
|
+
r2_eps, _ = hybrid_search(mq, top_k=ROUND1_TOP_N, user_id=uid, app_id=aid, project_id=pid)
|
|
345
|
+
round2_lists.append(r2_eps)
|
|
346
|
+
timing["round2_search"] = time.time() - t0
|
|
347
|
+
|
|
348
|
+
if len(round2_lists) == 1:
|
|
349
|
+
fused = round2_lists[0]
|
|
350
|
+
else:
|
|
351
|
+
fused = rrf_fusion(*round2_lists, k=HYBRID_RRF_K)
|
|
352
|
+
|
|
353
|
+
seen = {ep.id for ep in reranked}
|
|
354
|
+
r2_unique = [ep for ep in fused if ep.id not in seen]
|
|
355
|
+
keep = max(0, ROUND2_CAP - len(reranked))
|
|
356
|
+
r2_unique = r2_unique[:keep]
|
|
357
|
+
merged = list(reranked) + r2_unique
|
|
358
|
+
|
|
359
|
+
t0 = time.time()
|
|
360
|
+
final, _ = rerank(req.query, merged)
|
|
361
|
+
final = final[: req.top_k]
|
|
362
|
+
timing["final_rerank"] = time.time() - t0
|
|
363
|
+
|
|
364
|
+
return AgenticResult(
|
|
365
|
+
episodes=final,
|
|
366
|
+
method="agentic (multi_round)",
|
|
367
|
+
multi_queries=multi_queries,
|
|
368
|
+
reasoning=sufficiency.get("reasoning", ""),
|
|
369
|
+
timing={**timing, "total": time.time() - t_total},
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ── CLI ───────────────────────────────────────────────────────────────
|
|
374
|
+
if __name__ == "__main__":
|
|
375
|
+
parser = argparse.ArgumentParser(description="Agentic Search Server")
|
|
376
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
377
|
+
parser.add_argument("--port", type=int, default=8101)
|
|
378
|
+
args = parser.parse_args()
|
|
379
|
+
|
|
380
|
+
import uvicorn
|
|
381
|
+
|
|
382
|
+
print(f"Starting Agentic Search Server on {args.host}:{args.port}")
|
|
383
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""fcntl compatibility shim for Windows.
|
|
2
|
+
|
|
3
|
+
Drop this into the EverOS venv's site-packages so `import fcntl`
|
|
4
|
+
doesn't crash on Windows. The real fcntl module is POSIX-only.
|
|
5
|
+
|
|
6
|
+
On macOS/Linux this file is never imported (Python finds the real fcntl first).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Constants (values don't matter — all functions are no-ops)
|
|
10
|
+
LOCK_EX = 2
|
|
11
|
+
LOCK_NB = 4
|
|
12
|
+
LOCK_UN = 8
|
|
13
|
+
LOCK_SH = 1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def flock(fd, operation):
|
|
17
|
+
"""No-op on Windows. Single-process mode doesn't need file locking."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def lockf(fd, operation, length=0, start=0, whence=0):
|
|
22
|
+
"""No-op on Windows."""
|
|
23
|
+
pass
|