afaceuser09 commited on
Commit
2593922
·
verified ·
1 Parent(s): 6bffe1f

Create search_pipeline.py

Browse files
Files changed (1) hide show
  1. search_pipeline.py +586 -0
search_pipeline.py ADDED
@@ -0,0 +1,586 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Infinity GRM - Search / Execution Pipeline
2
+ ===========================================
3
+
4
+ Standalone tool/execution layer.
5
+
6
+ Responsibilities:
7
+ - conversation/session management
8
+ - deciding when web research is needed
9
+ - DuckDuckGo search
10
+ - asynchronous web retrieval
11
+ - page extraction
12
+ - context assembly
13
+ - calling the separate reasoning engine
14
+ - FastAPI API
15
+
16
+ The reasoning model itself lives in reasoning_engine.py.
17
+ """
18
+
19
+ import os
20
+ import re
21
+ import asyncio
22
+ import logging
23
+ from contextlib import asynccontextmanager
24
+ from urllib.parse import urlparse
25
+
26
+ import httpx
27
+ import torch
28
+ import uvicorn
29
+ from bs4 import BeautifulSoup
30
+ from trafilatura import extract
31
+ from ddgs import DDGS
32
+
33
+ from fastapi import FastAPI, HTTPException
34
+ from fastapi.middleware.cors import CORSMiddleware
35
+ from fastapi.staticfiles import StaticFiles
36
+ from pydantic import BaseModel
37
+
38
+ import reasoning_engine
39
+
40
+
41
+ logging.basicConfig(
42
+ level=logging.INFO,
43
+ format="%(asctime)s - %(levelname)s - %(message)s",
44
+ )
45
+
46
+ logger = logging.getLogger("infinitygrm.pipeline")
47
+
48
+ session_store = {}
49
+ engine_state = {}
50
+
51
+
52
+ # ---------------------------------------------------------------------
53
+ # REQUEST MODEL
54
+ # ---------------------------------------------------------------------
55
+
56
+ class ChatRequest(BaseModel):
57
+ message: str
58
+ session_id: str
59
+
60
+
61
+ # ---------------------------------------------------------------------
62
+ # SESSION
63
+ # ---------------------------------------------------------------------
64
+
65
+ def fetch_or_create_session(session_id: str) -> dict:
66
+ if session_id not in session_store:
67
+ session_store[session_id] = {
68
+ "history": [],
69
+ "last_entity": "",
70
+ "last_web_query": "",
71
+ "last_response": "",
72
+ "last_web_context": [],
73
+ }
74
+
75
+ return session_store[session_id]
76
+
77
+
78
+ def update_history(session: dict, role: str, content: str):
79
+ session["history"].append(
80
+ {
81
+ "role": role,
82
+ "content": content,
83
+ }
84
+ )
85
+
86
+ max_history = engine_state.get("max_history", 16)
87
+
88
+ while len(session["history"]) > max_history:
89
+ session["history"].pop(0)
90
+
91
+
92
+ # ---------------------------------------------------------------------
93
+ # FOLLOW-UP DETECTION
94
+ # ---------------------------------------------------------------------
95
+
96
+ def has_reference_language(query: str) -> bool:
97
+ words = set(re.findall(r"\b[\w'-]+\b", query.lower()))
98
+
99
+ return bool(
100
+ words.intersection(
101
+ {
102
+ "he", "him", "his",
103
+ "she", "her", "hers",
104
+ "they", "them", "their",
105
+ "it", "its",
106
+ "this", "that", "these", "those",
107
+ "then", "next", "after",
108
+ }
109
+ )
110
+ )
111
+
112
+
113
+ def is_followup(query: str, session: dict) -> bool:
114
+ if not session.get("last_response"):
115
+ return False
116
+
117
+ if len(query.split()) <= 4:
118
+ return True
119
+
120
+ if has_reference_language(query):
121
+ return True
122
+
123
+ # Let the reasoning engine handle semantic interpretation when
124
+ # necessary rather than forcing a brittle keyword rule.
125
+ return False
126
+
127
+
128
+ # ---------------------------------------------------------------------
129
+ # WEB SEARCH
130
+ # ---------------------------------------------------------------------
131
+
132
+ def isolated_ddg_search(query: str, max_results: int = 8) -> list:
133
+ try:
134
+ with DDGS() as ddgs:
135
+ results = ddgs.text(
136
+ query,
137
+ max_results=max_results,
138
+ )
139
+
140
+ return [
141
+ {
142
+ "title": r.get("title", ""),
143
+ "href": r.get("href", ""),
144
+ "body": r.get("body", ""),
145
+ }
146
+ for r in results
147
+ if r.get("href")
148
+ ]
149
+
150
+ except Exception as exc:
151
+ logger.error("DuckDuckGo search failed: %s", exc)
152
+ return []
153
+
154
+
155
+ async def search_web_async(query: str) -> list:
156
+ return await asyncio.to_thread(
157
+ isolated_ddg_search,
158
+ query,
159
+ )
160
+
161
+
162
+ # ---------------------------------------------------------------------
163
+ # WEB SCRAPING
164
+ # ---------------------------------------------------------------------
165
+
166
+ async def scrape_target_url(client, result: dict):
167
+ url = result["href"]
168
+
169
+ try:
170
+ response = await client.get(
171
+ url,
172
+ timeout=12.0,
173
+ follow_redirects=True,
174
+ )
175
+
176
+ if response.status_code != 200:
177
+ logger.warning(
178
+ "HTTP %s: %s",
179
+ response.status_code,
180
+ url,
181
+ )
182
+ return None
183
+
184
+ text = None
185
+
186
+ try:
187
+ text = extract(response.text)
188
+ except Exception as exc:
189
+ logger.warning(
190
+ "Trafilatura failed for %s: %s",
191
+ url,
192
+ exc,
193
+ )
194
+
195
+ if not text:
196
+ try:
197
+ soup = BeautifulSoup(
198
+ response.text,
199
+ "html.parser",
200
+ )
201
+ text = soup.get_text(
202
+ separator=" ",
203
+ strip=True,
204
+ )
205
+ except Exception as exc:
206
+ logger.warning(
207
+ "BeautifulSoup failed for %s: %s",
208
+ url,
209
+ exc,
210
+ )
211
+ return None
212
+
213
+ if not text or len(text.split()) < 20:
214
+ return None
215
+
216
+ return {
217
+ "url": url,
218
+ "title": result.get("title", ""),
219
+ "domain": urlparse(url).netloc,
220
+ "text": text[:7000],
221
+ }
222
+
223
+ except Exception as exc:
224
+ logger.warning(
225
+ "Scraping error for %s: %s",
226
+ url,
227
+ exc,
228
+ )
229
+ return None
230
+
231
+
232
+ async def build_context_pipeline(
233
+ query: str,
234
+ max_results: int = 8,
235
+ ) -> tuple[str, list]:
236
+ """Search and scrape the web independently of reasoning."""
237
+ search_results = await search_web_async(
238
+ query
239
+ )
240
+
241
+ if not search_results:
242
+ return "", []
243
+
244
+ headers = {
245
+ "User-Agent": (
246
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
247
+ "AppleWebKit/537.36 "
248
+ "(KHTML, like Gecko) "
249
+ "Chrome/122.0.0.0 Safari/537.36"
250
+ ),
251
+ "Accept": (
252
+ "text/html,application/xhtml+xml,"
253
+ "application/xml;q=0.9,image/avif,"
254
+ "image/webp,*/*;q=0.8"
255
+ ),
256
+ "Accept-Language": "en-US,en;q=0.9",
257
+ "Accept-Encoding": "gzip, deflate, br",
258
+ "Connection": "keep-alive",
259
+ }
260
+
261
+ try:
262
+ from curl_cffi.requests import AsyncSession
263
+ except ImportError:
264
+ AsyncSession = None
265
+
266
+ if AsyncSession is not None:
267
+ async with AsyncSession(
268
+ impersonate="chrome",
269
+ headers=headers,
270
+ ) as client:
271
+ tasks = [
272
+ scrape_target_url(client, result)
273
+ for result in search_results[:max_results]
274
+ ]
275
+
276
+ scraped = await asyncio.gather(
277
+ *tasks,
278
+ return_exceptions=False,
279
+ )
280
+ else:
281
+ async with httpx.AsyncClient(
282
+ headers=headers,
283
+ follow_redirects=True,
284
+ ) as client:
285
+ tasks = [
286
+ scrape_target_url(client, result)
287
+ for result in search_results[:max_results]
288
+ ]
289
+
290
+ scraped = await asyncio.gather(
291
+ *tasks,
292
+ return_exceptions=False,
293
+ )
294
+
295
+ blocks = []
296
+ sources = []
297
+
298
+ for item in scraped:
299
+ if not item:
300
+ continue
301
+
302
+ blocks.append(
303
+ "\n".join(
304
+ [
305
+ f"SOURCE: {item['url']}",
306
+ f"TITLE: {item['title']}",
307
+ f"DOMAIN: {item['domain']}",
308
+ f"CONTENT: {item['text']}",
309
+ ]
310
+ )
311
+ )
312
+
313
+ sources.append(item["url"])
314
+
315
+ return (
316
+ "\n\n---\n\n".join(blocks),
317
+ list(dict.fromkeys(sources)),
318
+ )
319
+
320
+
321
+ # ---------------------------------------------------------------------
322
+ # WEB DECISION
323
+ # ---------------------------------------------------------------------
324
+
325
+ SEARCH_TRIGGERS = {
326
+ "latest", "today", "current", "recent",
327
+ "news", "update", "price", "stock",
328
+ "founder", "ceo", "company",
329
+ "documentation", "release", "version",
330
+ "who", "what", "when", "where",
331
+ "code", "python", "javascript", "html",
332
+ }
333
+
334
+
335
+ def obvious_web_request(query: str) -> bool:
336
+ words = set(re.findall(r"\b[\w'-]+\b", query.lower()))
337
+ return bool(words.intersection(SEARCH_TRIGGERS))
338
+
339
+
340
+ # ---------------------------------------------------------------------
341
+ # MAIN EXECUTION
342
+ # ---------------------------------------------------------------------
343
+
344
+ async def process_request(
345
+ query: str,
346
+ session: dict,
347
+ ):
348
+ """
349
+ Separation boundary:
350
+
351
+ reasoning_engine.plan()
352
+ |
353
+ v
354
+ execution/search
355
+ |
356
+ v
357
+ reasoning_engine.analyze()
358
+ |
359
+ v
360
+ reasoning_engine.final_answer()
361
+ """
362
+
363
+ # ---------------------------------------------------------------
364
+ # PASS 1: REASONING / PLANNING
365
+ # ---------------------------------------------------------------
366
+
367
+ plan = await asyncio.to_thread(
368
+ reasoning_engine.plan,
369
+ query,
370
+ session["history"],
371
+ )
372
+
373
+ requires_web = bool(
374
+ plan.get("requires_web")
375
+ or obvious_web_request(query)
376
+ or is_followup(query, session)
377
+ )
378
+
379
+ search_queries = plan.get("search_queries") or [query]
380
+
381
+ # ---------------------------------------------------------------
382
+ # EXECUTION / SEARCH
383
+ # ---------------------------------------------------------------
384
+
385
+ evidence_blocks = []
386
+ sources = []
387
+
388
+ if requires_web:
389
+ # Multiple searches let the reasoning engine decompose advanced
390
+ # questions instead of forcing one broad query.
391
+ for search_query in search_queries[:4]:
392
+ search_query = str(search_query).strip()
393
+
394
+ if not search_query:
395
+ continue
396
+
397
+ context, found_sources = await build_context_pipeline(
398
+ search_query
399
+ )
400
+
401
+ if context:
402
+ evidence_blocks.append(
403
+ f"SEARCH QUERY: {search_query}\n{context}"
404
+ )
405
+
406
+ sources.extend(found_sources)
407
+
408
+ sources = list(dict.fromkeys(sources))
409
+
410
+ session["last_web_query"] = " | ".join(
411
+ str(x) for x in search_queries[:4]
412
+ )
413
+
414
+ session["last_web_context"] = [
415
+ {
416
+ "user": query,
417
+ "search": session["last_web_query"],
418
+ }
419
+ ]
420
+
421
+ evidence = "\n\n========\n\n".join(
422
+ evidence_blocks
423
+ )
424
+
425
+ # ---------------------------------------------------------------
426
+ # PASS 2: REASONING / EVIDENCE ANALYSIS
427
+ # ---------------------------------------------------------------
428
+
429
+ analysis = await asyncio.to_thread(
430
+ reasoning_engine.analyze,
431
+ query,
432
+ plan,
433
+ evidence,
434
+ session["history"],
435
+ )
436
+
437
+ # ---------------------------------------------------------------
438
+ # PASS 3: FINAL SYNTHESIS
439
+ # ---------------------------------------------------------------
440
+
441
+ answer = await asyncio.to_thread(
442
+ reasoning_engine.final_answer,
443
+ query,
444
+ plan,
445
+ analysis,
446
+ evidence,
447
+ session["history"],
448
+ )
449
+
450
+ # ---------------------------------------------------------------
451
+ # LEARNING
452
+ # ---------------------------------------------------------------
453
+
454
+ # Store the solved problem as an experience. This does not mutate
455
+ # model weights; it creates a retrieval corpus for future problems.
456
+ await asyncio.to_thread(
457
+ reasoning_engine.learn_from_result,
458
+ query,
459
+ plan,
460
+ analysis,
461
+ answer,
462
+ 1.0,
463
+ )
464
+
465
+ session["last_response"] = answer
466
+ session["last_entity"] = plan.get("task", "")
467
+
468
+ update_history(
469
+ session,
470
+ "user",
471
+ query,
472
+ )
473
+
474
+ update_history(
475
+ session,
476
+ "assistant",
477
+ answer,
478
+ )
479
+
480
+ return answer, sources
481
+
482
+
483
+ # ---------------------------------------------------------------------
484
+ # FASTAPI
485
+ # ---------------------------------------------------------------------
486
+
487
+ @asynccontextmanager
488
+ async def lifespan(app: FastAPI):
489
+ logger.info("Starting Infinity GRM execution pipeline...")
490
+
491
+ await asyncio.to_thread(
492
+ reasoning_engine.initialize
493
+ )
494
+
495
+ engine_state["max_history"] = 16
496
+
497
+ yield
498
+
499
+ engine_state.clear()
500
+ logger.info("Infinity GRM pipeline stopped.")
501
+
502
+
503
+ app = FastAPI(
504
+ title="Infinity GRM - Separated Reasoning/Search Engine",
505
+ lifespan=lifespan,
506
+ )
507
+
508
+ app.add_middleware(
509
+ CORSMiddleware,
510
+ allow_origins=["*"],
511
+ allow_credentials=True,
512
+ allow_methods=["*"],
513
+ allow_headers=["*"],
514
+ )
515
+
516
+
517
+ @app.post("/query")
518
+ async def chat_endpoint(request: ChatRequest):
519
+ try:
520
+ session = fetch_or_create_session(
521
+ request.session_id
522
+ )
523
+
524
+ answer, sources = await process_request(
525
+ request.message,
526
+ session,
527
+ )
528
+
529
+ return {
530
+ "response": answer,
531
+ "sources": sources,
532
+ }
533
+
534
+ except Exception as exc:
535
+ logger.exception(
536
+ "Critical exception in Infinity GRM pipeline"
537
+ )
538
+
539
+ raise HTTPException(
540
+ status_code=500,
541
+ detail=str(exc),
542
+ )
543
+
544
+
545
+ @app.get("/health")
546
+ async def health():
547
+ return {
548
+ "status": "healthy",
549
+ "gpu_acceleration_active": torch.cuda.is_available(),
550
+ "reasoning_engine": "separated",
551
+ "learning": "retrieval-based experience memory",
552
+ }
553
+
554
+
555
+ if not os.path.exists("static"):
556
+ os.makedirs("static")
557
+
558
+ app.mount(
559
+ "/static",
560
+ StaticFiles(directory="static"),
561
+ name="static",
562
+ )
563
+
564
+
565
+ @app.get("/")
566
+ async def root():
567
+ return {
568
+ "message": "Infinity GRM separated reasoning/search engine is active.",
569
+ "architecture": [
570
+ "reasoning_engine",
571
+ "search_execution",
572
+ "evidence_analysis",
573
+ "final_synthesis",
574
+ ],
575
+ }
576
+
577
+
578
+ if __name__ == "__main__":
579
+ port = int(os.getenv("PORT", "7860"))
580
+
581
+ uvicorn.run(
582
+ "search_pipeline:app",
583
+ host="0.0.0.0",
584
+ port=port,
585
+ reload=False,
586
+ )