muhammad1707 commited on
Commit
a2ea346
·
1 Parent(s): 7869fb3

Prepare project for Hugging Face deployment

Browse files
Files changed (6) hide show
  1. .gitignore +3 -0
  2. back/.gitignore +2 -0
  3. back/requirements.txt +3 -1
  4. back/server.py +214 -409
  5. services/api.ts +1 -1
  6. vite.config.ts +1 -1
.gitignore CHANGED
@@ -12,6 +12,9 @@ dist
12
  dist-ssr
13
  *.local
14
  .env
 
 
 
15
 
16
  # Editor directories and files
17
  .vscode/*
 
12
  dist-ssr
13
  *.local
14
  .env
15
+ back/data/uploads/
16
+ back/chroma_db/
17
+ back/chats.db
18
 
19
  # Editor directories and files
20
  .vscode/*
back/.gitignore CHANGED
@@ -16,6 +16,8 @@ vector_db.pkl
16
  # Ma'lumotlar bazalari va Vektor do'konlari
17
  chroma_db/
18
  chats.db
 
 
19
 
20
  # Caches
21
  .cache/
 
16
  # Ma'lumotlar bazalari va Vektor do'konlari
17
  chroma_db/
18
  chats.db
19
+ data/
20
+ data/uploads
21
 
22
  # Caches
23
  .cache/
back/requirements.txt CHANGED
@@ -10,4 +10,6 @@ requests
10
  google-genai
11
  python-multipart
12
  gradio
13
-
 
 
 
10
  google-genai
11
  python-multipart
12
  gradio
13
+ transformers>=4.37.0
14
+ sentence-transformers
15
+ openai
back/server.py CHANGED
@@ -1,6 +1,5 @@
1
  import os
2
- import json
3
- import pickle
4
  import numpy as np
5
  import sqlite3
6
  import uuid
@@ -8,26 +7,25 @@ from datetime import datetime
8
  from typing import List, Optional
9
  from dotenv import load_dotenv
10
  from pypdf import PdfReader
11
- from google import genai
12
- from google.genai import types
13
- from sklearn.metrics.pairwise import cosine_similarity
14
- # import chromadb - Moved to try/except block below
15
-
16
 
17
  # ==============================
18
  # 0. Sozlamalar
19
  # ==============================
20
- load_dotenv()
21
 
22
- API_KEY = os.getenv("GEMINI_API_KEY")
23
- if not API_KEY:
24
- raise RuntimeError("GEMINI_API_KEY topilmadi!")
25
 
26
- client = genai.Client(api_key=API_KEY)
27
- # Chat modeli (tez va arzon variant)
28
- GEMINI_CHAT_MODEL = "gemini-1.5-flash"
29
- # Embedding modeli
30
- GEMINI_EMBED_MODEL = "text-embedding-004"
 
 
 
 
 
 
31
 
32
  CHROMA_DIR = "./chroma_db"
33
  CHAT_DB_PATH = "./chats.db"
@@ -46,23 +44,79 @@ except Exception as e:
46
  collection = None
47
  RAG_AVAILABLE = False
48
 
 
 
49
 
50
  # ==============================
51
  # 1. PDF -> TEXT
52
  # ==============================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  def load_pdf(path: str) -> str:
54
  reader = PdfReader(path)
55
- text = ""
 
56
  for page in reader.pages:
57
  page_text = page.extract_text()
58
- if page_text:
59
- text += page_text + "\n"
60
- return text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  # ==============================
63
  # 2. TEXT -> CHUNKS
64
  # ==============================
65
- def chunk_text(text, chunk_size=300, overlap=200):
66
  chunks = []
67
  start = 0
68
  while start < len(text):
@@ -74,16 +128,34 @@ def chunk_text(text, chunk_size=300, overlap=200):
74
  # ==============================
75
  # 3. CHUNKS -> EMBEDDINGS
76
  # ==============================
77
- def embed_texts(texts):
78
- # Ollama embedding for a list of texts
79
- embeddings = []
80
- for text in texts:
81
- response = client.models.embed_content(
82
- model=GEMINI_EMBED_MODEL,
83
- contents=text
84
- )
85
- embeddings.append(response.embedding.values)
86
- return embeddings
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
 
89
  # ==============================
@@ -126,7 +198,7 @@ def delete_from_chroma(doc_id):
126
  # ==============================
127
  # Helper for RAG Tool
128
  # ==============================
129
- def find_context(query, top_k=3):
130
  if not RAG_AVAILABLE: return []
131
  try:
132
  query_embedding = embed_texts([query])[0]
@@ -156,9 +228,6 @@ def retrieve_documents(query: str) -> str:
156
  # ... (init_db, CRUD, etc - skipped for brevity in tool call logic, assuming target content matches)
157
 
158
 
159
-
160
-
161
-
162
  # ==============================
163
  # 6. CHAT & DOCUMENT DATABASE SETUP
164
  # ==============================
@@ -366,7 +435,7 @@ def get_chat_messages(chat_id: str) -> List[dict]:
366
  # 9. RAG-AWARE GENERATION
367
  # ==============================
368
 
369
- from tools import calculate_expression, get_current_weather
370
  # from google.genai.types import Tool, GenerateContentConfig, FunctionDeclaration
371
 
372
 
@@ -374,265 +443,88 @@ from tools import calculate_expression, get_current_weather
374
  # 9. RAG-AWARE GENERATION
375
  # ==============================
376
 
377
- SYSTEM_PROMPT = """You are a helpful assistant.
378
- - Answer general greetings (like 'hi', 'hello') directly and briefly.
379
- - Use `retrieve_documents` ONLY for questions about uploaded files.
380
- - Use `calculate_expression` ONLY for math.
381
- - Use `get_current_weather` ONLY for weather questions.
382
- DO NOT use tools for simple conversation.
383
- """
 
 
384
 
 
 
 
 
 
 
 
385
 
 
386
 
387
  def generate_rag_response(question: str, context_list: List[str], chat_history: List[dict]) -> str:
388
- """
389
- Generate a response using RAG with conversation history and tools.
390
- """
391
- # --- ROUTING LAYER (For 1B model stability) ---
392
- is_greeting = question.lower().strip() in ["hi", "hello", "hey", "salom", "qalay", "howdy"]
393
- is_very_short = len(question.strip()) < 10
394
-
395
- messages = [{"role": "system", "content": SYSTEM_PROMPT}]
396
- history_messages = chat_history[-10:] if len(chat_history) > 10 else chat_history
397
- for msg in history_messages:
398
- messages.append({"role": msg["role"], "content": msg["content"]})
399
- messages.append({"role": "user", "content": question})
400
-
401
- # If it's just a greeting, don't even show tools to the 1B model
402
- if is_greeting or (is_very_short and not any(char.isdigit() for char in question)):
403
- print(f"--- Routing: Simple greeting detected. Skipping tools. ---")
404
- try:
405
- response = client.models.generate_content(
406
- model=GEMINI_CHAT_MODEL,
407
- contents=question,
408
- config=types.GenerateContentConfig(
409
- system_instruction="You are a friendly assistant. Greet the user normally and briefly.",
410
- temperature=0,
411
- ),
412
- # messages=[{"role": "system", "content": "You are a friendly assistant. Greet the user normally and briefly."}, {"role": "user", "content": question}],
413
- # options={'temperature': 0}
414
- )
415
- # return response.text
416
- return (response.text or "Hello! How can I help you todayyy?").strip()
417
- except Exception as e:
418
- print(f"Gemini Routing Error: {e}")
419
- return "Hello! How can I help you today?"
420
-
421
- # --- STANDARD TOOL CALLING LAYER ---
422
- # tools =[
423
- # {
424
- # 'type': 'function',
425
- # 'function': {
426
- # 'name': 'calculate_expression',
427
- # 'description': 'Solve arithmetic math problems (e.g. 2+2).',
428
- # 'parameters': {
429
- # 'type': 'object',
430
- # 'properties': {
431
- # 'expression': {'type': 'string', 'description': 'The math expression'},
432
- # },
433
- # 'required': ['expression'],
434
- # },
435
- # },
436
- # },
437
- # {
438
- # 'type': 'function',
439
- # 'function': {
440
- # 'name': 'get_current_weather',
441
- # 'description': 'Get the current weather for a city.',
442
- # 'parameters': {
443
- # 'type': 'object',
444
- # 'properties': {
445
- # 'location': {'type': 'string', 'description': 'City name'},
446
- # },
447
- # 'required': ['location'],
448
- # },
449
- # },
450
- # },
451
- # {
452
- # 'type': 'function',
453
- # 'function': {
454
- # 'name': 'retrieve_documents',
455
- # 'description': 'Search for information in uploaded PDF documents.',
456
- # 'parameters': {
457
- # 'type': 'object',
458
- # 'properties': {
459
- # 'query': {'type': 'string', 'description': 'The search query'},
460
- # },
461
- # 'required': ['query'],
462
- # },
463
- # },
464
- # },
465
- # ]
466
-
467
- tool = types.Tool(function_declarations=[
468
- types.FunctionDeclaration(
469
- name="calculate_expression",
470
- description="Solve arithmetic math problems (e.g. 2+2).",
471
- parameters_json_schema={
472
- "type": "object",
473
- "properties": {
474
- "expression": {"type": "string", "description": "The math expression"},
475
- },
476
- "required": ["expression"],
477
- },
478
- ),
479
- types.FunctionDeclaration(
480
- name="get_current_weather",
481
- description="Get the current weather for a city.",
482
- parameters_json_schema={
483
- "type": "object",
484
- "properties": {
485
- "location": {"type": "string", "description": "City name"},
486
- },
487
- "required": ["location"],
488
- },
489
- ),
490
- types.FunctionDeclaration(
491
- name="retrieve_documents",
492
- description="Search for information in uploaded PDF documents.",
493
- parameters_json_schema={
494
- "type": "object",
495
- "properties": {
496
- "query": {"type": "string", "description": "The search query"},
497
- },
498
- "required": ["query"],
499
- },
500
- ),
501
- ])
502
 
 
 
 
503
 
504
- try:
505
- print(f"--- Sending to Ollama: {question} ---")
506
- available_functions = {
507
- "calculate_expression": calculate_expression,
508
- "get_current_weather": get_current_weather,
509
- "retrieve_documents": retrieve_documents,
510
- }
511
-
512
- # Gemini uchun promptni “system + history + user” ko‘rinishida bitta textga yig’amiz
513
- system_text = SYSTEM_PROMPT
514
- history_text = ""
515
- for msg in (chat_history[-10:] if len(chat_history) > 10 else chat_history):
516
- history_text += f"{msg['role'].upper()}: {msg['content']}\n"
517
-
518
- user_text = f"{history_text}\nUSER: {question}".strip()
519
- response = client.models.generate_content(
520
- model=GEMINI_CHAT_MODEL,
521
- contents=user_text,
522
- config=types.GenerateContentConfig(
523
- system_instruction=system_text,
524
- tools=[tool],
525
- temperature=0,
526
- automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=False),
527
- ),
528
- # messages=messages,
529
- # tools=tools,
530
- # options={'temperature': 0}
531
- )
532
 
533
- print(f"Raw Ollama response content: {response.text}")
 
534
 
535
- # 1. Native Tool Calls
536
- if response.function_calls:
537
- print(f"Native tool calls detected: {response.function_calls}")
538
- available_functions = {
539
- 'calculate_expression': calculate_expression,
540
- 'get_current_weather': get_current_weather,
541
- 'retrieve_documents': retrieve_documents,
542
- }
543
-
544
- # messages.append(response.message)
545
-
546
- tool_outputs = []
547
-
548
- for call in response.function_calls:
549
- # google-genai SDK da odatda shu ko‘rinish:
550
- func_name = getattr(call, "name", None)
551
- func_args = getattr(call, "args", None)
552
-
553
- # fallback (agar boshqa format bo‘lsa):
554
- if func_name is None and hasattr(call, "function"):
555
- func_name = getattr(call.function, "name", None)
556
- func_args = getattr(call.function, "arguments", None)
557
-
558
- if isinstance(func_args, str):
559
- try:
560
- func_args = json.loads(func_args)
561
- except Exception:
562
- func_args = {}
563
-
564
- if func_args is None:
565
- func_args = {}
566
-
567
- if func_name not in available_functions:
568
- tool_outputs.append(f"{func_name}: Unknown tool")
569
- continue
570
-
571
- try:
572
- result = available_functions[func_name](**func_args)
573
- except Exception as e:
574
- result = f"Tool error: {e}"
575
-
576
- tool_outputs.append(f"{func_name}({func_args}) => {result}")
577
-
578
- followup_prompt = (
579
- f"{user_text}\n\n"
580
- "Tool results:\n"
581
- + "\n".join(tool_outputs)
582
- + "\n\nNow answer the user using the tool results."
583
- )
584
 
585
- final_response = client.models.generate_content(
586
- model=GEMINI_CHAT_MODEL,
587
- contents=followup_prompt,
588
- config=types.GenerateContentConfig(
589
- system_instruction=system_text,
590
- temperature=0,
591
- ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  )
593
- return (final_response.text or "").strip()
594
-
595
- # 2. Hard Fallback for math/tool hallucinations
596
- content = response.text.strip() if response.text else ""
597
- hallucination_keywords = ["calculate_expression", "syntax error", "expression", "parameters"]
598
-
599
- if any(kw in content.lower() for kw in hallucination_keywords) and not any(char.isdigit() for char in question):
600
- print(f"Detected tool hallucination in text: {content[:50]}...")
601
- retry_response = client.models.generate_content(
602
- model=GEMINI_CHAT_MODEL,
603
- messages=[{"role": "system", "content": "You are a helpful assistant. Provide a natural response without mentioning tools or syntax."}, {"role": "user", "content": question}],
604
- options={'temperature': 0}
605
- )
606
- return retry_response.text
607
-
608
- return content
609
-
610
- except Exception as e:
611
- print(f"Ollama Error: {e}")
612
- return f"Error generation response: {str(e)}"
613
-
614
-
615
 
 
616
 
617
  # Legacy function - kept for backwards compatibility
618
  def ask_gemini(question, context_list):
619
- context = "\n\n".join(context_list)
620
- prompt = f"""Quyidagi context ma'lumotlaridan foydalanib savolga javob ber.
621
- Faqat context ichidagi ma'lumotni ishlat.
622
-
623
- Context:
624
- {context}
625
-
626
- Savol: {question}"""
627
-
628
- response = client.models.generate_content(
629
- # messages=[{'role': 'user', 'content': prompt}]
630
- model=GEMINI_CHAT_MODEL,
631
- contents=prompt,
632
- config=types.GenerateContentConfig(temperature=0),
633
- )
634
- return (response.text or "").strip()
635
-
636
 
637
  # ==============================
638
  # 10. MAIN PROCESS (PDF Processing)
@@ -650,7 +542,6 @@ from fastapi import FastAPI, HTTPException, UploadFile, File
650
  from fastapi.middleware.cors import CORSMiddleware
651
  from pydantic import BaseModel
652
 
653
-
654
  app = FastAPI(title="RAG Chat API", version="2.0.0")
655
  app.add_middleware(
656
  CORSMiddleware,
@@ -676,94 +567,24 @@ class SendMessageRequest(BaseModel):
676
  class UpdateChatRequest(BaseModel):
677
  title: str
678
 
679
-
680
- # ==============================
681
- # 14.5 DOCUMENT ENDPOINTS
682
- # ==============================
683
-
684
- @app.get("/documents")
685
- async def list_documents():
686
- return {"documents": get_all_documents()}
687
-
688
- @app.post("/documents")
689
- async def upload_document(file: UploadFile = File(...)):
690
- if not RAG_AVAILABLE:
691
- raise HTTPException(status_code=503, detail="RAG system unavailable (Python 3.14 incompatibility).")
692
-
693
- if not file.filename.endswith('.pdf'):
694
- raise HTTPException(status_code=400, detail="Only PDF files are allowed")
695
-
696
- # Save file temporarily
697
- os.makedirs("data/uploads", exist_ok=True)
698
- file_path = f"data/uploads/{uuid.uuid4()}_{file.filename}"
699
-
700
- try:
701
- with open(file_path, "wb") as f:
702
- content = await file.read()
703
- f.write(content)
704
-
705
- # Process PDF
706
- text = load_pdf(file_path)
707
- if not text.strip():
708
- raise HTTPException(status_code=400, detail="Could not extract text from PDF")
709
-
710
- chunks = chunk_text(text)
711
- embeddings = embed_texts(chunks)
712
-
713
- # Save Metadata
714
- doc = create_document_record(file.filename)
715
-
716
- # Save Vectors
717
- save_to_chroma(chunks, embeddings, doc["id"])
718
-
719
- # Cleanup file (optional, keeping it for now in case needed, or delete)
720
- # os.remove(file_path)
721
-
722
- return doc
723
-
724
- except Exception as e:
725
- if os.path.exists(file_path):
726
- os.remove(file_path)
727
- raise HTTPException(status_code=500, detail=f"Processing failed: {str(e)}")
728
-
729
- @app.delete("/documents/{doc_id}")
730
- async def delete_document(doc_id: str):
731
- if not RAG_AVAILABLE:
732
- # Still allow deleting from DB, just skip vector delete
733
- pass
734
-
735
- success = delete_document_record(doc_id)
736
- if not success:
737
- raise HTTPException(status_code=404, detail="Document not found")
738
-
739
- # Remove from Vector DB
740
- if RAG_AVAILABLE:
741
- delete_from_chroma(doc_id)
742
-
743
- return {"message": "Document deleted successfully"}
744
-
745
-
746
  # ==============================
747
  # 13. LEGACY ENDPOINT (backwards compatibility)
748
  # ==============================
749
 
750
  @app.post("/ask")
751
  async def ask_question(req: QuestionRequest):
752
- """Legacy endpoint - still functional for backwards compatibility"""
753
  question = req.question.strip()
754
  if not question:
755
  raise HTTPException(status_code=400, detail="Savol bo'sh bo'lishi mumkin emas")
756
 
757
- # This endpoint now relies on documents already in ChromaDB, not auto-loading data.pdf
758
  if collection.count() == 0:
759
  raise HTTPException(status_code=404, detail="No documents loaded into the system. Please upload PDFs first.")
760
 
761
- relevant_chunks = find_context(question, top_k=3)
762
- answer = ask_gemini(question, relevant_chunks)
763
 
764
  return {"answer": answer}
765
 
766
-
767
  # ==============================
768
  # 14. CHAT ENDPOINTS
769
  # ==============================
@@ -817,61 +638,51 @@ async def remove_chat(chat_id: str):
817
  async def list_documents():
818
  return {"documents": get_all_documents()}
819
 
 
820
  @app.post("/documents")
821
  async def upload_document(file: UploadFile = File(...)):
822
  if not RAG_AVAILABLE:
823
- raise HTTPException(status_code=503, detail="RAG system unavailable (Python 3.14 incompatibility).")
824
 
825
- if not file.filename.endswith('.pdf'):
826
  raise HTTPException(status_code=400, detail="Only PDF files are allowed")
827
-
828
- # Save file temporarily
829
  os.makedirs("data/uploads", exist_ok=True)
830
  file_path = f"data/uploads/{uuid.uuid4()}_{file.filename}"
831
-
832
  try:
833
  with open(file_path, "wb") as f:
834
  content = await file.read()
835
  f.write(content)
836
-
837
- # Process PDF
838
  text = load_pdf(file_path)
839
  if not text.strip():
840
  raise HTTPException(status_code=400, detail="Could not extract text from PDF")
841
-
842
  chunks = chunk_text(text)
843
  embeddings = embed_texts(chunks)
844
-
845
- # Save Metadata
846
  doc = create_document_record(file.filename)
847
-
848
- # Save Vectors
849
  save_to_chroma(chunks, embeddings, doc["id"])
850
-
851
- # Cleanup file (optional, keeping it for now in case needed, or delete)
852
- # os.remove(file_path)
853
-
854
  return doc
855
-
856
  except Exception as e:
857
  if os.path.exists(file_path):
858
  os.remove(file_path)
859
  raise HTTPException(status_code=500, detail=f"Processing failed: {str(e)}")
860
 
 
861
  @app.delete("/documents/{doc_id}")
862
  async def delete_document(doc_id: str):
863
  success = delete_document_record(doc_id)
864
  if not success:
865
  raise HTTPException(status_code=404, detail="Document not found")
866
-
867
- # Remove from Vector DB
868
  if RAG_AVAILABLE:
869
  delete_from_chroma(doc_id)
870
-
871
- return {"message": "Document deleted successfully"}
872
-
873
-
874
 
 
875
  # ==============================
876
  # 15. MESSAGE ENDPOINTS
877
  # ==============================
@@ -889,54 +700,48 @@ async def get_messages(chat_id: str):
889
 
890
  @app.post("/chats/{chat_id}/messages")
891
  async def send_message(chat_id: str, req: SendMessageRequest):
892
- """
893
- Send a message and get RAG-powered response.
894
-
895
- This endpoint:
896
- 1. Saves the user message
897
- 2. Retrieves relevant context from vector DB
898
- 3. Generates response using chat history + RAG context
899
- 4. Saves and returns the assistant response
900
- """
901
  chat = get_chat_by_id(chat_id)
902
  if not chat:
903
  raise HTTPException(status_code=404, detail="Chat not found")
904
-
905
  content = req.content.strip()
906
  if not content:
907
  raise HTTPException(status_code=400, detail="Message content cannot be empty")
908
-
909
- # NO AUTO_LOAD of data.pdf anymore. RAG uses whatever is in Chroma.
910
-
911
- # Save user message
912
  user_message = add_message(chat_id, "user", content)
913
-
914
- # Update chat title if this is the first message
915
  messages = get_chat_messages(chat_id)
916
- if len(messages) == 1: # Only the user message we just added
917
- # Generate title from first message (truncate if too long)
918
  title = content[:50] + "..." if len(content) > 50 else content
919
  update_chat_title(chat_id, title)
920
-
921
- # Get relevant context from RAG (Legacy/Fallback)
922
- # The model will now use retrieve_documents tool if needed
923
- relevant_chunks = []
924
-
925
- # Get chat history (excluding the message we just added for cleaner history)
926
  chat_history = messages[:-1] if len(messages) > 1 else []
927
-
928
- # Generate RAG-powered response
929
- response_text = generate_rag_response(content, relevant_chunks, chat_history)
930
-
931
- # Save assistant message
 
 
 
 
 
 
 
 
 
 
 
932
  assistant_message = add_message(chat_id, "assistant", response_text)
933
-
934
  return {
935
  "user_message": user_message,
936
  "assistant_message": assistant_message
937
  }
938
 
939
-
940
  # ==============================
941
  # 16. SERVER STARTUP
942
  # ==============================
@@ -977,4 +782,4 @@ if __name__ == "__main__":
977
  print(f"Failed to auto-load data.pdf: {e}")
978
 
979
  import uvicorn
980
- uvicorn.run("server:app", host="0.0.0.0", port=4000, reload=True)
 
1
  import os
2
+ from openai import OpenAI
 
3
  import numpy as np
4
  import sqlite3
5
  import uuid
 
7
  from typing import List, Optional
8
  from dotenv import load_dotenv
9
  from pypdf import PdfReader
10
+ from pathlib import Path
11
+ import re
 
 
 
12
 
13
  # ==============================
14
  # 0. Sozlamalar
15
  # ==============================
 
16
 
 
 
 
17
 
18
+ env_path = Path(__file__).resolve().parent / ".env"
19
+ load_dotenv(env_path, override=True)
20
+
21
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
22
+ if not OPENAI_API_KEY:
23
+ raise RuntimeError("OPENAI_API_KEY topilmadi")
24
+
25
+ openai_client = OpenAI(api_key=OPENAI_API_KEY)
26
+
27
+ OPENAI_EMBED_MODEL = "text-embedding-3-small"
28
+ OPENAI_CHAT_MODEL = "gpt-4o-mini"
29
 
30
  CHROMA_DIR = "./chroma_db"
31
  CHAT_DB_PATH = "./chats.db"
 
44
  collection = None
45
  RAG_AVAILABLE = False
46
 
47
+ print("RAG_AVAILABLE:", RAG_AVAILABLE)
48
+ print("COLLECTION COUNT:", collection.count() if collection else 0)
49
 
50
  # ==============================
51
  # 1. PDF -> TEXT
52
  # ==============================
53
+ # def load_pdf(path: str) -> str:
54
+ # reader = PdfReader(path)
55
+ # text = ""
56
+ # for page in reader.pages:
57
+ # page_text = page.extract_text()
58
+ # if page_text:
59
+ # text += page_text + "\n"
60
+ # return text
61
+
62
+ def fix_spaced_text(line: str) -> str:
63
+ if re.fullmatch(r'(?:[A-Za-z]\s+){3,}[A-Za-z]?', line.strip()):
64
+ return line.replace(" ", "")
65
+ return line
66
+
67
  def load_pdf(path: str) -> str:
68
  reader = PdfReader(path)
69
+ lines = []
70
+
71
  for page in reader.pages:
72
  page_text = page.extract_text()
73
+ if not page_text:
74
+ continue
75
+
76
+ for line in page_text.splitlines():
77
+ cleaned = fix_spaced_text(line)
78
+ if cleaned:
79
+ lines.append(cleaned)
80
+
81
+ text = "\n".join(lines)
82
+
83
+ text = re.sub(r'(?<=\w)\s*@\s*(?=\w)', '@', text)
84
+ text = re.sub(r'(?<=\w)\s*\.\s*(?=\w)', '.', text)
85
+ text = re.sub(r'\n{3,}', '\n\n', text)
86
+ text = re.sub(r'[ \t]{2,}', ' ', text)
87
+
88
+ return text.strip()
89
+
90
+ def extract_email_from_context(context_list: List[str]) -> Optional[str]:
91
+ text = "\n".join(context_list)
92
+
93
+ text = re.sub(r'(?<=\w)\s*@\s*(?=\w)', '@', text)
94
+ text = re.sub(r'(?<=\w)\s*\.\s*(?=\w)', '.', text)
95
+ text = re.sub(r'(?<=\w)\s+(?=\w@)', '', text)
96
+ text = re.sub(r'(?<=@)\s+(?=\w)', '', text)
97
+
98
+ match = re.search(r'[\w\.-]+@[\w\.-]+\.\w+', text)
99
+ return match.group(0) if match else None
100
+
101
+
102
+ def extract_project_lines(context_list: List[str]) -> List[str]:
103
+ text = "\n".join(context_list)
104
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
105
+
106
+ keywords = ["project", "cs core", "trusty", "mily", "corporate solutions"]
107
+ result = []
108
+
109
+ for line in lines:
110
+ low = line.lower()
111
+ if any(k in low for k in keywords):
112
+ result.append(line)
113
+
114
+ return result[:8]
115
 
116
  # ==============================
117
  # 2. TEXT -> CHUNKS
118
  # ==============================
119
+ def chunk_text(text, chunk_size=500, overlap=100):
120
  chunks = []
121
  start = 0
122
  while start < len(text):
 
128
  # ==============================
129
  # 3. CHUNKS -> EMBEDDINGS
130
  # ==============================
131
+ # def embed_texts(texts):
132
+ # # Ollama embedding for a list of texts
133
+ # embeddings = []
134
+ # for text in texts:
135
+ # response = client.models.embed_content(
136
+ # model=GEMINI_EMBED_MODEL,
137
+ # contents=text
138
+ # )
139
+ # embeddings.append(response.embeddings[0].values)
140
+ # return embeddings
141
+
142
+ # qwen
143
+ # def embed_texts(texts):
144
+ # embeddings = embedder.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
145
+ # return embeddings.tolist()
146
+
147
+ OPENAI_EMBED_MODEL = "text-embedding-3-small"
148
+
149
+ def embed_texts(texts: List[str]) -> List[List[float]]:
150
+ cleaned = [t.strip() for t in texts if t and t.strip()]
151
+ if not cleaned:
152
+ return []
153
+
154
+ response = openai_client.embeddings.create(
155
+ model=OPENAI_EMBED_MODEL,
156
+ input=cleaned
157
+ )
158
+ return [item.embedding for item in response.data]
159
 
160
 
161
  # ==============================
 
198
  # ==============================
199
  # Helper for RAG Tool
200
  # ==============================
201
+ def find_context(query, top_k=4):
202
  if not RAG_AVAILABLE: return []
203
  try:
204
  query_embedding = embed_texts([query])[0]
 
228
  # ... (init_db, CRUD, etc - skipped for brevity in tool call logic, assuming target content matches)
229
 
230
 
 
 
 
231
  # ==============================
232
  # 6. CHAT & DOCUMENT DATABASE SETUP
233
  # ==============================
 
435
  # 9. RAG-AWARE GENERATION
436
  # ==============================
437
 
438
+ # from tools import calculate_expression, get_current_weather
439
  # from google.genai.types import Tool, GenerateContentConfig, FunctionDeclaration
440
 
441
 
 
443
  # 9. RAG-AWARE GENERATION
444
  # ==============================
445
 
446
+ SYSTEM_PROMPT = (
447
+ "You are a document-grounded assistant. "
448
+ "Answer the user's question using only the provided document context and relevant chat history. "
449
+ "Do not guess, do not invent facts, and do not add information that is not supported by the document context. "
450
+ "If the answer is not clearly available in the provided context, say exactly: "
451
+ "'The exact answer is not clearly available in the document.' "
452
+ "When the document contains the answer, provide a complete and accurate response with all relevant details found in the context. "
453
+ "Preserve important names, numbers, dates, email addresses, links, titles, and technical terms exactly as they appear in the document whenever possible."
454
+ )
455
 
456
+ # SYSTEM_PROMPT = """You are a helpful assistant.
457
+ # - Answer general greetings (like 'hi', 'hello') directly and briefly.
458
+ # - Use `retrieve_documents` ONLY for questions about uploaded files.
459
+ # - Use `calculate_expression` ONLY for math.
460
+ # - Use `get_current_weather` ONLY for weather questions.
461
+ # DO NOT use tools for simple conversation.
462
+ # """
463
 
464
+ OPENAI_CHAT_MODEL = "gpt-4o-mini"
465
 
466
  def generate_rag_response(question: str, context_list: List[str], chat_history: List[dict]) -> str:
467
+ # context_text = "\n\n---\n\n".join(context_list[:2]) if context_list else "No relevant context found."
468
+ context_text = "\n\n---\n\n".join(context_list) if context_list else "No relevant context found."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
 
470
+ history_text = ""
471
+ for msg in (chat_history[-3:] if len(chat_history) > 3 else chat_history):
472
+ history_text += f"{msg['role'].upper()}: {msg['content']}\n"
473
 
474
+ prompt = f"""Document context:
475
+ {context_text}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
+ Chat history:
478
+ {history_text}
479
 
480
+ User question:
481
+ {question}
482
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
 
484
+ response = openai_client.responses.create(
485
+ model=OPENAI_CHAT_MODEL,
486
+ input=[
487
+ {
488
+ "role": "system",
489
+ "content": [
490
+ {
491
+ "type": "input_text",
492
+ "text": (
493
+ "You are a document-grounded assistant. "
494
+ "Answer ONLY from the provided document context. "
495
+ "Do not guess. Do not rewrite names, emails, project names, companies, locations, or technologies. "
496
+ "If the exact answer is not clearly available in the document, say exactly: "
497
+ "'The exact answer is not clearly available in the document.' "
498
+ "Keep the answer short and factual."
499
+ ),
500
+ # "text": (
501
+ # "You are a document-grounded assistant. "
502
+ # "Answer ONLY from the provided document context. "
503
+ # "Do not guess. Do not rewrite names, emails, project names, companies, locations, or technologies. "
504
+ # "If the exact answer is not clearly available in the document, say exactly: "
505
+ # "'The exact answer is not clearly available in the document.' "
506
+ # "Keep the answer short and factual."
507
+ # ),
508
+ }
509
+ ],
510
+ },
511
+ {
512
+ "role": "user",
513
+ "content": [
514
+ {
515
+ "type": "input_text",
516
+ "text": prompt,
517
+ }
518
+ ],
519
+ },
520
+ ],
521
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
 
523
+ return (response.output_text or "").strip() or "The exact answer is not clearly available in the document."
524
 
525
  # Legacy function - kept for backwards compatibility
526
  def ask_gemini(question, context_list):
527
+ return generate_rag_response(question, context_list, [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
 
529
  # ==============================
530
  # 10. MAIN PROCESS (PDF Processing)
 
542
  from fastapi.middleware.cors import CORSMiddleware
543
  from pydantic import BaseModel
544
 
 
545
  app = FastAPI(title="RAG Chat API", version="2.0.0")
546
  app.add_middleware(
547
  CORSMiddleware,
 
567
  class UpdateChatRequest(BaseModel):
568
  title: str
569
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  # ==============================
571
  # 13. LEGACY ENDPOINT (backwards compatibility)
572
  # ==============================
573
 
574
  @app.post("/ask")
575
  async def ask_question(req: QuestionRequest):
 
576
  question = req.question.strip()
577
  if not question:
578
  raise HTTPException(status_code=400, detail="Savol bo'sh bo'lishi mumkin emas")
579
 
 
580
  if collection.count() == 0:
581
  raise HTTPException(status_code=404, detail="No documents loaded into the system. Please upload PDFs first.")
582
 
583
+ relevant_chunks = find_context(question, top_k=4)
584
+ answer = generate_rag_response(question, relevant_chunks, [])
585
 
586
  return {"answer": answer}
587
 
 
588
  # ==============================
589
  # 14. CHAT ENDPOINTS
590
  # ==============================
 
638
  async def list_documents():
639
  return {"documents": get_all_documents()}
640
 
641
+
642
  @app.post("/documents")
643
  async def upload_document(file: UploadFile = File(...)):
644
  if not RAG_AVAILABLE:
645
+ raise HTTPException(status_code=503, detail="RAG system unavailable.")
646
 
647
+ if not file.filename.endswith(".pdf"):
648
  raise HTTPException(status_code=400, detail="Only PDF files are allowed")
649
+
 
650
  os.makedirs("data/uploads", exist_ok=True)
651
  file_path = f"data/uploads/{uuid.uuid4()}_{file.filename}"
652
+
653
  try:
654
  with open(file_path, "wb") as f:
655
  content = await file.read()
656
  f.write(content)
657
+
 
658
  text = load_pdf(file_path)
659
  if not text.strip():
660
  raise HTTPException(status_code=400, detail="Could not extract text from PDF")
661
+
662
  chunks = chunk_text(text)
663
  embeddings = embed_texts(chunks)
664
+
 
665
  doc = create_document_record(file.filename)
 
 
666
  save_to_chroma(chunks, embeddings, doc["id"])
667
+
 
 
 
668
  return doc
669
+
670
  except Exception as e:
671
  if os.path.exists(file_path):
672
  os.remove(file_path)
673
  raise HTTPException(status_code=500, detail=f"Processing failed: {str(e)}")
674
 
675
+
676
  @app.delete("/documents/{doc_id}")
677
  async def delete_document(doc_id: str):
678
  success = delete_document_record(doc_id)
679
  if not success:
680
  raise HTTPException(status_code=404, detail="Document not found")
681
+
 
682
  if RAG_AVAILABLE:
683
  delete_from_chroma(doc_id)
 
 
 
 
684
 
685
+ return {"message": "Document deleted successfully"}
686
  # ==============================
687
  # 15. MESSAGE ENDPOINTS
688
  # ==============================
 
700
 
701
  @app.post("/chats/{chat_id}/messages")
702
  async def send_message(chat_id: str, req: SendMessageRequest):
 
 
 
 
 
 
 
 
 
703
  chat = get_chat_by_id(chat_id)
704
  if not chat:
705
  raise HTTPException(status_code=404, detail="Chat not found")
706
+
707
  content = req.content.strip()
708
  if not content:
709
  raise HTTPException(status_code=400, detail="Message content cannot be empty")
710
+
 
 
 
711
  user_message = add_message(chat_id, "user", content)
712
+
 
713
  messages = get_chat_messages(chat_id)
714
+ if len(messages) == 1:
 
715
  title = content[:50] + "..." if len(content) > 50 else content
716
  update_chat_title(chat_id, title)
717
+
718
+ relevant_chunks = find_context(content, top_k=4)
719
+ print("RELEVANT CHUNKS:", relevant_chunks)
720
+
 
 
721
  chat_history = messages[:-1] if len(messages) > 1 else []
722
+ lower_content = content.lower()
723
+
724
+ if "email" in lower_content or "e-mail" in lower_content or "gmail" in lower_content:
725
+ email = extract_email_from_context(relevant_chunks)
726
+ response_text = email if email else "The exact answer is not clearly available in the document."
727
+
728
+ elif "project" in lower_content:
729
+ project_lines = extract_project_lines(relevant_chunks)
730
+ if project_lines:
731
+ response_text = "\n".join(project_lines)
732
+ else:
733
+ response_text = generate_rag_response(content, relevant_chunks, chat_history)
734
+
735
+ else:
736
+ response_text = generate_rag_response(content, relevant_chunks, chat_history)
737
+
738
  assistant_message = add_message(chat_id, "assistant", response_text)
739
+
740
  return {
741
  "user_message": user_message,
742
  "assistant_message": assistant_message
743
  }
744
 
 
745
  # ==============================
746
  # 16. SERVER STARTUP
747
  # ==============================
 
782
  print(f"Failed to auto-load data.pdf: {e}")
783
 
784
  import uvicorn
785
+ uvicorn.run("server:app", host="0.0.0.0", port=4000)
services/api.ts CHANGED
@@ -1,7 +1,7 @@
1
  // Chat API Service
2
  import { Chat, Message, Document } from '../types';
3
 
4
- const API_BASE = 'http://localhost:4000';
5
 
6
  // Create a new chat
7
  export async function createChat(title?: string): Promise<Chat> {
 
1
  // Chat API Service
2
  import { Chat, Message, Document } from '../types';
3
 
4
+ const API_BASE = 'http://localhost:8000';
5
 
6
  // Create a new chat
7
  export async function createChat(title?: string): Promise<Chat> {
vite.config.ts CHANGED
@@ -6,7 +6,7 @@ export default defineConfig(({ mode }) => {
6
  const env = loadEnv(mode, '.', '');
7
  return {
8
  server: {
9
- port: 3000,
10
  host: '0.0.0.0',
11
  },
12
  plugins: [react()],
 
6
  const env = loadEnv(mode, '.', '');
7
  return {
8
  server: {
9
+ port: 4000,
10
  host: '0.0.0.0',
11
  },
12
  plugins: [react()],