afaceuser09 commited on
Commit
c7fa9e5
·
verified ·
1 Parent(s): 1cd5e9c

Update search_pipeline.py

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