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.
@@ -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
@@ -0,0 +1,7 @@
1
+ everos==1.0.2
2
+ everalgo-rank>=0.3.0
3
+ lancedb>=0.30.0
4
+ httpx>=0.27.0
5
+ fastapi>=0.104.0
6
+ uvicorn>=0.30.0
7
+ pydantic>=2.0