sadidft commited on
Commit
1b2f5ea
·
verified ·
1 Parent(s): 12306e8

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +723 -0
main.py ADDED
@@ -0,0 +1,723 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cogni-Engine v1 — Main Entry Point
3
+ FastAPI server with OpenAI-compatible API.
4
+ Manages startup, background threads (thinker + keep-alive), and shutdown.
5
+
6
+ Endpoints:
7
+ POST /v1/chat/completions — Chat (OpenAI-compatible)
8
+ GET /v1/status — Brain status & intelligence score
9
+ POST /v1/data/upload — Upload JSONL data
10
+ GET /v1/health — Health check / keep-alive
11
+ GET /v1/graph/stats — Detailed graph statistics
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import time
17
+ import signal
18
+ import asyncio
19
+ import threading
20
+ import traceback
21
+ from typing import Optional, List
22
+ from contextlib import asynccontextmanager
23
+
24
+ import uvicorn
25
+ import httpx
26
+ from fastapi import FastAPI, Request, HTTPException, Header, UploadFile, File
27
+ from fastapi.responses import JSONResponse
28
+ from fastapi.middleware.cors import CORSMiddleware
29
+
30
+ import config
31
+ from memory import Memory
32
+ from knowledge import KnowledgeGraph
33
+ from thinker import Thinker
34
+ from brain import Brain
35
+
36
+
37
+ # ═══════════════════════════════════════════════════════════
38
+ # GLOBAL STATE
39
+ # ═══════════════════════════════════════════════════════════
40
+
41
+ brain: Optional[Brain] = None
42
+ startup_time: float = 0
43
+ _keep_alive_task: Optional[asyncio.Task] = None
44
+ _cleanup_task: Optional[asyncio.Task] = None
45
+ _ready = False
46
+
47
+
48
+ # ═══════════════════════════════════════════════════════════
49
+ # STARTUP & SHUTDOWN
50
+ # ═══════════════════════════════════════════════════════════
51
+
52
+ def initialize_engine() -> Brain:
53
+ """
54
+ Initialize the entire Cogni-Engine:
55
+ 1. Validate config
56
+ 2. Connect to TiDB
57
+ 3. Load knowledge graph from DB
58
+ 4. Start thinker thread
59
+ """
60
+ global startup_time
61
+ startup_time = time.time()
62
+
63
+ print("=" * 55)
64
+ print(" COGNI-ENGINE v1 — Starting Up")
65
+ print("=" * 55)
66
+
67
+ # ── Step 1: Config ──
68
+ config.print_config_summary()
69
+ config_valid = config.validate_config()
70
+ if not config_valid:
71
+ print("[INIT] Config validation has errors. Continuing with warnings...")
72
+
73
+ # ── Step 2: Memory (TiDB) ──
74
+ print("\n[INIT] Initializing memory layer...")
75
+ memory = Memory()
76
+ db_connected = memory.initialize()
77
+
78
+ if db_connected:
79
+ print("[INIT] Database connected successfully.")
80
+ else:
81
+ print("[INIT] Database not connected. Running in memory-only mode.")
82
+ print("[INIT] WARNING: All data will be lost on restart!")
83
+
84
+ # ── Step 3: Knowledge Graph ──
85
+ print("\n[INIT] Initializing knowledge graph...")
86
+ graph = KnowledgeGraph(memory)
87
+ graph.load_from_memory()
88
+
89
+ stats = graph.get_stats()
90
+ score = graph.get_intelligence_score()
91
+ print(f"[INIT] Graph loaded: {stats['total_nodes']} nodes, "
92
+ f"{stats['total_edges']} edges, score={score:.2f}")
93
+
94
+ # ── Step 4: Thinker ──
95
+ print("\n[INIT] Initializing thinker...")
96
+ thinker = Thinker(graph)
97
+
98
+ # ── Step 5: Brain ──
99
+ print("\n[INIT] Initializing brain...")
100
+ engine = Brain(graph, thinker)
101
+
102
+ # ── Step 6: Start thinker ──
103
+ thinker.start()
104
+
105
+ # ── Step 7: Ensure data directory ──
106
+ os.makedirs(config.DATA_DIR, exist_ok=True)
107
+ readme_path = os.path.join(config.DATA_DIR, "README.md")
108
+ if not os.path.exists(readme_path):
109
+ _create_data_readme(readme_path)
110
+
111
+ elapsed = time.time() - startup_time
112
+ print(f"\n[INIT] Cogni-Engine ready in {elapsed:.1f}s")
113
+ print("=" * 55)
114
+
115
+ return engine
116
+
117
+
118
+ def _create_data_readme(path: str):
119
+ """Create README.md in data directory with format guide."""
120
+ content = """# Cogni-Engine Data Directory
121
+
122
+ Place your `.jsonl` files here. Each file will be automatically
123
+ detected and ingested by the thinking engine.
124
+
125
+ ## Format
126
+
127
+ One JSON object per line. Required fields: `type` and `content`.
128
+
129
+ ```json
130
+ {"type":"fact","content":"Earth orbits the Sun in 365.25 days","tags":["astronomy"]}
131
+ {"type":"definition","term":"API","content":"Application Programming Interface"}
132
+ {"type":"relation","from":"Python","to":"Django","relation":"has_framework"}
133
+ ```
134
+
135
+ ## Supported Types
136
+
137
+ fact, definition, explanation, description, property, statistic,
138
+ measurement, term, abbreviation, jargon, slang, idiom, synonym,
139
+ antonym, quote, rule, example, analogy, opinion, paragraph,
140
+ relation, cause_effect, comparison, hierarchy, composition,
141
+ dependency, contradiction, timeline, process, procedure, event,
142
+ history, change, qa, custom_*
143
+
144
+ ## Optional Fields
145
+
146
+ tags, source, confidence, language, domain, related, metadata
147
+
148
+ See full documentation for details on each type.
149
+ """
150
+ try:
151
+ with open(path, 'w', encoding='utf-8') as f:
152
+ f.write(content)
153
+ except Exception:
154
+ pass
155
+
156
+
157
+ # ═══════════════════════════════════════════════════════════
158
+ # AUTHENTICATION
159
+ # ═══════════════════════════════════════════════════════════
160
+
161
+ def verify_api_key(authorization: Optional[str]) -> bool:
162
+ """Verify Bearer token against configured API key."""
163
+ if not authorization:
164
+ return False
165
+
166
+ parts = authorization.split(" ", 1)
167
+ if len(parts) != 2 or parts[0].lower() != "bearer":
168
+ return False
169
+
170
+ token = parts[1].strip()
171
+ return token == config.API_KEY
172
+
173
+
174
+ def require_auth(authorization: Optional[str] = Header(None, alias="Authorization")):
175
+ """Dependency that enforces authentication."""
176
+ if not verify_api_key(authorization):
177
+ raise HTTPException(
178
+ status_code=401,
179
+ detail="Invalid or missing API key. Use 'Authorization: Bearer <key>'"
180
+ )
181
+
182
+
183
+ # ═══════════════════════════════════════════════════════════
184
+ # KEEP-ALIVE & BACKGROUND TASKS
185
+ # ═══════════════════════════════════════════════════════════
186
+
187
+ async def keep_alive_loop():
188
+ """Self-ping to prevent HF Space from sleeping."""
189
+ if not config.KEEP_ALIVE_ENABLED:
190
+ return
191
+
192
+ base_url = f"http://localhost:{config.PORT}"
193
+ interval = config.KEEP_ALIVE_INTERVAL
194
+
195
+ # Wait for server to be ready
196
+ await asyncio.sleep(10)
197
+
198
+ async with httpx.AsyncClient() as client:
199
+ while True:
200
+ try:
201
+ response = await client.get(
202
+ f"{base_url}/v1/health",
203
+ timeout=10
204
+ )
205
+ if config.LOG_THINKING_DETAILS:
206
+ print(f"[KEEP-ALIVE] Ping OK: {response.status_code}")
207
+ except Exception as e:
208
+ print(f"[KEEP-ALIVE] Ping failed: {e}")
209
+
210
+ await asyncio.sleep(interval)
211
+
212
+
213
+ async def cleanup_loop():
214
+ """Periodic cleanup of expired sessions and buffer flush."""
215
+ while True:
216
+ try:
217
+ await asyncio.sleep(config.SESSION_CLEANUP_INTERVAL)
218
+ if brain:
219
+ brain.cleanup()
220
+ except Exception as e:
221
+ print(f"[CLEANUP] Error: {e}")
222
+ await asyncio.sleep(60)
223
+
224
+
225
+ # ═══════════════════════════════════════════════════════════
226
+ # FASTAPI APP
227
+ # ═══════════════════════════════════════════════════════════
228
+
229
+ @asynccontextmanager
230
+ async def lifespan(app: FastAPI):
231
+ """Manage startup and shutdown lifecycle."""
232
+ global brain, _keep_alive_task, _cleanup_task, _ready
233
+
234
+ # ── Startup ──
235
+ try:
236
+ brain = initialize_engine()
237
+ _ready = True
238
+
239
+ # Start background tasks
240
+ _keep_alive_task = asyncio.create_task(keep_alive_loop())
241
+ _cleanup_task = asyncio.create_task(cleanup_loop())
242
+
243
+ except Exception as e:
244
+ print(f"[FATAL] Startup failed: {e}")
245
+ traceback.print_exc()
246
+ _ready = False
247
+
248
+ yield
249
+
250
+ # ── Shutdown ──
251
+ print("\n[SHUTDOWN] Shutting down Cogni-Engine...")
252
+
253
+ if _keep_alive_task:
254
+ _keep_alive_task.cancel()
255
+ if _cleanup_task:
256
+ _cleanup_task.cancel()
257
+
258
+ if brain:
259
+ brain.shutdown()
260
+
261
+ print("[SHUTDOWN] Complete.")
262
+
263
+
264
+ app = FastAPI(
265
+ title="Cogni-Engine v1",
266
+ description="Self-Evolving Knowledge AI",
267
+ version="1.0.0",
268
+ lifespan=lifespan
269
+ )
270
+
271
+ # CORS — allow all origins for API access
272
+ app.add_middleware(
273
+ CORSMiddleware,
274
+ allow_origins=["*"],
275
+ allow_credentials=True,
276
+ allow_methods=["*"],
277
+ allow_headers=["*"],
278
+ )
279
+
280
+
281
+ # ═══════════════════════════════════════════════════════════
282
+ # API ENDPOINTS
283
+ # ═══════════════════════════════════════════════════════════
284
+
285
+ # ───────────────────────────────────────────────────
286
+ # POST /v1/chat/completions — OpenAI Compatible
287
+ # ─────────────────────────────────────────���─────────
288
+
289
+ @app.post("/v1/chat/completions")
290
+ async def chat_completions(
291
+ request: Request,
292
+ authorization: Optional[str] = Header(None, alias="Authorization")
293
+ ):
294
+ """
295
+ OpenAI-compatible chat completions endpoint.
296
+ Compatible with LibreChat, Open WebUI, ChatBox, etc.
297
+ """
298
+ # Auth
299
+ if not verify_api_key(authorization):
300
+ raise HTTPException(status_code=401, detail="Invalid API key")
301
+
302
+ if not _ready or not brain:
303
+ raise HTTPException(status_code=503, detail="Engine not ready")
304
+
305
+ # Parse request body
306
+ try:
307
+ body = await request.json()
308
+ except Exception:
309
+ raise HTTPException(status_code=400, detail="Invalid JSON body")
310
+
311
+ messages = body.get("messages", [])
312
+ if not messages:
313
+ raise HTTPException(status_code=400, detail="'messages' field required")
314
+
315
+ temperature = body.get("temperature", config.DEFAULT_TEMPERATURE)
316
+ temperature = max(0.0, min(1.0, float(temperature)))
317
+
318
+ # Extract session ID from request (custom field) or generate
319
+ session_id = body.get("session_id", None)
320
+
321
+ # Process through brain
322
+ try:
323
+ result = brain.process_message(
324
+ messages=messages,
325
+ session_id=session_id,
326
+ temperature=temperature
327
+ )
328
+ except Exception as e:
329
+ print(f"[API] Processing error: {e}")
330
+ traceback.print_exc()
331
+ raise HTTPException(status_code=500, detail="Internal processing error")
332
+
333
+ # Format as OpenAI-compatible response
334
+ response_id = f"cogni-{config.generate_session_id()}"
335
+
336
+ return JSONResponse(content={
337
+ "id": response_id,
338
+ "object": "chat.completion",
339
+ "created": int(time.time()),
340
+ "model": "cogni-engine-v1",
341
+ "choices": [
342
+ {
343
+ "index": 0,
344
+ "message": {
345
+ "role": "assistant",
346
+ "content": result["response"]
347
+ },
348
+ "finish_reason": "stop"
349
+ }
350
+ ],
351
+ "usage": {
352
+ "prompt_tokens": sum(len(m.get("content", "").split()) for m in messages),
353
+ "completion_tokens": len(result["response"].split()),
354
+ "total_tokens": (
355
+ sum(len(m.get("content", "").split()) for m in messages) +
356
+ len(result["response"].split())
357
+ ),
358
+ # Cogni-specific metadata
359
+ "cogni_metadata": {
360
+ "confidence": result["confidence"],
361
+ "reasoning_depth": result["reasoning_depth"],
362
+ "nodes_traversed": result["nodes_traversed"],
363
+ "chains_used": result["chains_used"],
364
+ "thinking_cycles": result["thinking_cycles"],
365
+ "processing_time_ms": result["processing_time_ms"],
366
+ "session_id": result["session_id"]
367
+ }
368
+ }
369
+ })
370
+
371
+
372
+ # ───────────────────────────────────────────────────
373
+ # GET /v1/status — Brain Status
374
+ # ───────────────────────────────────────────────────
375
+
376
+ @app.get("/v1/status")
377
+ async def get_status(
378
+ authorization: Optional[str] = Header(None, alias="Authorization")
379
+ ):
380
+ """Get comprehensive brain status and intelligence metrics."""
381
+ if not verify_api_key(authorization):
382
+ raise HTTPException(status_code=401, detail="Invalid API key")
383
+
384
+ if not _ready or not brain:
385
+ return JSONResponse(content={"alive": False, "ready": False})
386
+
387
+ status = brain.get_status()
388
+
389
+ # Add uptime
390
+ uptime_seconds = time.time() - startup_time
391
+ status["uptime"] = utils.format_duration(uptime_seconds)
392
+ status["uptime_seconds"] = round(uptime_seconds, 0)
393
+ status["started_at"] = time.strftime(
394
+ "%Y-%m-%dT%H:%M:%SZ", time.gmtime(startup_time)
395
+ )
396
+
397
+ return JSONResponse(content=status)
398
+
399
+
400
+ # ───────────────────────────────────────────────────
401
+ # POST /v1/data/upload — Upload JSONL Data
402
+ # ───────────────────────────────────────────────────
403
+
404
+ @app.post("/v1/data/upload")
405
+ async def upload_data(
406
+ file: UploadFile = File(...),
407
+ authorization: Optional[str] = Header(None, alias="Authorization")
408
+ ):
409
+ """
410
+ Upload a JSONL data file.
411
+ File will be saved to /data/ and auto-ingested by thinker.
412
+ """
413
+ if not verify_api_key(authorization):
414
+ raise HTTPException(status_code=401, detail="Invalid API key")
415
+
416
+ if not _ready:
417
+ raise HTTPException(status_code=503, detail="Engine not ready")
418
+
419
+ # Validate file extension
420
+ filename = file.filename or "upload.jsonl"
421
+ if not any(filename.endswith(ext) for ext in config.SUPPORTED_DATA_EXTENSIONS):
422
+ raise HTTPException(
423
+ status_code=400,
424
+ detail=f"Only {config.SUPPORTED_DATA_EXTENSIONS} files are supported"
425
+ )
426
+
427
+ # Read content
428
+ try:
429
+ content = await file.read()
430
+ content_str = content.decode("utf-8")
431
+ except Exception as e:
432
+ raise HTTPException(status_code=400, detail=f"Failed to read file: {e}")
433
+
434
+ # Validate size
435
+ size_mb = len(content) / (1024 * 1024)
436
+ if size_mb > config.MAX_REQUEST_SIZE_MB:
437
+ raise HTTPException(
438
+ status_code=413,
439
+ detail=f"File too large: {size_mb:.1f}MB (max {config.MAX_REQUEST_SIZE_MB}MB)"
440
+ )
441
+
442
+ # Validate JSONL format (check first few lines)
443
+ lines = content_str.strip().split('\n')
444
+ valid_lines = 0
445
+ errors = []
446
+
447
+ for i, line in enumerate(lines[:10]):
448
+ line = line.strip()
449
+ if not line:
450
+ continue
451
+ try:
452
+ import json
453
+ entry = json.loads(line)
454
+ if "type" not in entry or "content" not in entry:
455
+ errors.append(f"Line {i+1}: missing 'type' or 'content' field")
456
+ else:
457
+ valid_lines += 1
458
+ except json.JSONDecodeError as e:
459
+ errors.append(f"Line {i+1}: invalid JSON — {e}")
460
+
461
+ if valid_lines == 0:
462
+ raise HTTPException(
463
+ status_code=400,
464
+ detail={
465
+ "error": "No valid JSONL entries found",
466
+ "details": errors[:5]
467
+ }
468
+ )
469
+
470
+ # Save to data directory
471
+ safe_filename = "".join(
472
+ c if c.isalnum() or c in "._-" else "_"
473
+ for c in filename
474
+ )
475
+ # Add timestamp to prevent overwrites
476
+ ts = int(time.time())
477
+ if not safe_filename.startswith(f"{ts}_"):
478
+ safe_filename = f"{ts}_{safe_filename}"
479
+
480
+ save_path = os.path.join(config.DATA_DIR, safe_filename)
481
+
482
+ try:
483
+ with open(save_path, 'w', encoding='utf-8') as f:
484
+ f.write(content_str)
485
+ except Exception as e:
486
+ raise HTTPException(status_code=500, detail=f"Failed to save file: {e}")
487
+
488
+ return JSONResponse(content={
489
+ "status": "uploaded",
490
+ "filename": safe_filename,
491
+ "total_lines": len(lines),
492
+ "valid_lines_sampled": valid_lines,
493
+ "validation_errors": errors[:5] if errors else [],
494
+ "size_mb": round(size_mb, 2),
495
+ "message": (
496
+ f"File saved. Thinker will auto-ingest on next INGEST cycle "
497
+ f"(within ~{config.THINKING_INTERVAL_SLOW}s)."
498
+ )
499
+ })
500
+
501
+
502
+ # ───────────────────────────────────────────────────
503
+ # GET /v1/health — Health Check
504
+ # ───────────────────────────────────────────────────
505
+
506
+ @app.get("/v1/health")
507
+ async def health_check():
508
+ """
509
+ Health check endpoint.
510
+ Used by keep-alive self-ping and external monitoring.
511
+ No authentication required.
512
+ """
513
+ if not _ready:
514
+ return JSONResponse(
515
+ status_code=503,
516
+ content={
517
+ "status": "starting",
518
+ "timestamp": utils.timestamp_now()
519
+ }
520
+ )
521
+
522
+ uptime = time.time() - startup_time
523
+
524
+ status_data = {
525
+ "status": "healthy",
526
+ "timestamp": utils.timestamp_now(),
527
+ "uptime": utils.format_duration(uptime)
528
+ }
529
+
530
+ if brain:
531
+ status_data["thinking_cycles"] = brain.thinker.total_cycles
532
+ status_data["thinker_phase"] = brain.thinker.current_phase
533
+ status_data["thinker_running"] = brain.thinker.is_running
534
+
535
+ return JSONResponse(content=status_data)
536
+
537
+
538
+ # ───────────────────────────────────────────────────
539
+ # GET /v1/graph/stats — Detailed Graph Statistics
540
+ # ───────────────────────────────────────────────────
541
+
542
+ @app.get("/v1/graph/stats")
543
+ async def graph_stats(
544
+ authorization: Optional[str] = Header(None, alias="Authorization")
545
+ ):
546
+ """Get detailed knowledge graph statistics."""
547
+ if not verify_api_key(authorization):
548
+ raise HTTPException(status_code=401, detail="Invalid API key")
549
+
550
+ if not _ready or not brain:
551
+ raise HTTPException(status_code=503, detail="Engine not ready")
552
+
553
+ graph = brain.graph
554
+ stats = graph.get_stats()
555
+ intelligence = graph.get_intelligence_score()
556
+ db_stats = graph.memory.get_db_stats()
557
+ thinker_metrics = brain.thinker.metrics
558
+
559
+ # Node type distribution
560
+ type_counts = {}
561
+ for node in graph.nodes.values():
562
+ type_counts[node.type] = type_counts.get(node.type, 0) + 1
563
+
564
+ # Edge relation distribution
565
+ relation_counts = {}
566
+ for edge in graph.edges.values():
567
+ relation_counts[edge.relation] = relation_counts.get(edge.relation, 0) + 1
568
+
569
+ # Source distribution
570
+ source_counts = {"data": 0, "inferred": 0, "user_chat": 0}
571
+ for node in graph.nodes.values():
572
+ source_counts[node.source] = source_counts.get(node.source, 0) + 1
573
+
574
+ # Top weighted nodes
575
+ top_nodes = sorted(
576
+ graph.nodes.values(),
577
+ key=lambda n: n.weight * n.connections,
578
+ reverse=True
579
+ )[:20]
580
+
581
+ return JSONResponse(content={
582
+ "intelligence_score": round(intelligence, 2),
583
+
584
+ "overview": stats,
585
+
586
+ "node_types": type_counts,
587
+ "edge_relations": relation_counts,
588
+ "node_sources": source_counts,
589
+
590
+ "top_nodes": [
591
+ {
592
+ "id": n.id,
593
+ "content": utils.truncate_text(n.content, 100),
594
+ "type": n.type,
595
+ "weight": round(n.weight, 3),
596
+ "connections": n.connections
597
+ }
598
+ for n in top_nodes
599
+ ],
600
+
601
+ "thinker_metrics": thinker_metrics,
602
+ "database": db_stats,
603
+
604
+ "uptime_seconds": round(time.time() - startup_time, 0)
605
+ })
606
+
607
+
608
+ # ───────────────────────────────────────────────────
609
+ # GET /v1/models — Model List (OpenAI Compat)
610
+ # ───────────────────────────────────────────────────
611
+
612
+ @app.get("/v1/models")
613
+ async def list_models(
614
+ authorization: Optional[str] = Header(None, alias="Authorization")
615
+ ):
616
+ """
617
+ List available models.
618
+ OpenAI-compatible endpoint required by some clients.
619
+ """
620
+ if not verify_api_key(authorization):
621
+ raise HTTPException(status_code=401, detail="Invalid API key")
622
+
623
+ intelligence = 0.0
624
+ if brain:
625
+ intelligence = brain.graph.get_intelligence_score()
626
+
627
+ return JSONResponse(content={
628
+ "object": "list",
629
+ "data": [
630
+ {
631
+ "id": "cogni-engine-v1",
632
+ "object": "model",
633
+ "created": int(startup_time),
634
+ "owned_by": "cogni-engine",
635
+ "permission": [],
636
+ "root": "cogni-engine-v1",
637
+ "parent": None,
638
+ "meta": {
639
+ "type": "self-evolving-knowledge-ai",
640
+ "intelligence_score": round(intelligence, 2)
641
+ }
642
+ }
643
+ ]
644
+ })
645
+
646
+
647
+ # ───────────────────────────────────────────────────
648
+ # Root endpoint
649
+ # ───────────────────────────────────────────────────
650
+
651
+ @app.get("/")
652
+ async def root():
653
+ """Root endpoint with basic info."""
654
+ uptime = time.time() - startup_time if startup_time else 0
655
+
656
+ info = {
657
+ "name": "Cogni-Engine v1",
658
+ "description": "Self-Evolving Knowledge AI",
659
+ "status": "running" if _ready else "starting",
660
+ "uptime": utils.format_duration(uptime),
661
+ "api_docs": "/docs",
662
+ "endpoints": {
663
+ "chat": "POST /v1/chat/completions",
664
+ "status": "GET /v1/status",
665
+ "upload": "POST /v1/data/upload",
666
+ "health": "GET /v1/health",
667
+ "stats": "GET /v1/graph/stats",
668
+ "models": "GET /v1/models"
669
+ }
670
+ }
671
+
672
+ if brain:
673
+ info["intelligence_score"] = round(
674
+ brain.graph.get_intelligence_score(), 2
675
+ )
676
+ info["thinking_cycles"] = brain.thinker.total_cycles
677
+ info["total_nodes"] = brain.graph.get_stats()["total_nodes"]
678
+
679
+ return JSONResponse(content=info)
680
+
681
+
682
+ # ═══════════════════════════════════════════════════════════
683
+ # SIGNAL HANDLING
684
+ # ═══════════════════════════════════════════════════════════
685
+
686
+ def handle_shutdown_signal(signum, frame):
687
+ """Handle graceful shutdown on SIGTERM/SIGINT."""
688
+ print(f"\n[SIGNAL] Received signal {signum}. Initiating shutdown...")
689
+ if brain:
690
+ brain.shutdown()
691
+ sys.exit(0)
692
+
693
+
694
+ # ═══════════════════════════════════════════════════════════
695
+ # IMPORTS NEEDED BY THIS FILE
696
+ # ═══════════════════════════════════════════════════════════
697
+
698
+ import utils # noqa: E402 — needed for utility functions used in endpoints
699
+
700
+
701
+ # ═══════════════════════════════════════════════════════════
702
+ # ENTRY POINT
703
+ # ══════��════════════════════════════════════════════════════
704
+
705
+ if __name__ == "__main__":
706
+ # Register signal handlers
707
+ signal.signal(signal.SIGTERM, handle_shutdown_signal)
708
+ signal.signal(signal.SIGINT, handle_shutdown_signal)
709
+
710
+ print(f"\n[MAIN] Starting Cogni-Engine on port {config.PORT}...")
711
+ print(f"[MAIN] API Key: {'SET' if os.environ.get('API_KEY') else 'AUTO-GENERATED'}")
712
+ print(f"[MAIN] API Docs: http://localhost:{config.PORT}/docs\n")
713
+
714
+ uvicorn.run(
715
+ app,
716
+ host="0.0.0.0",
717
+ port=config.PORT,
718
+ log_level=config.LOG_LEVEL.lower(),
719
+ access_log=config.LOG_API_REQUESTS,
720
+ timeout_keep_alive=65,
721
+ # Single worker — thinker thread runs in-process
722
+ workers=1
723
+ )