MRMAQ commited on
Commit
ce2466e
·
1 Parent(s): d27e50d

initial commit

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Jupyter Notebook checkpoints
7
+ .ipynb_checkpoints/
8
+
9
+ # Environment variables
10
+ .env
11
+ .env.*
12
+
13
+ # Virtual environments
14
+ env/
15
+ .venv/
16
+ venv/
17
+ ENV/
18
+
19
+ # VS Code settings
20
+ .vscode/
21
+
22
+ # Data files
23
+ *.xlsx
24
+ *.csv
25
+ *.tsv
26
+
27
+ # OS files
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Python egg files
32
+ *.egg
33
+ *.egg-info/
34
+ dist/
35
+ build/
36
+
37
+ # Logs
38
+ *.log
39
+
40
+ # Misc
41
+ *.bak
42
+ *.swp
43
+ *.swo
44
+
45
+ # Ignore model files and large downloads
46
+ *.bin
47
+ *.h5
48
+ *.ckpt
49
+ *.pt
50
+ *.pth
51
+
52
+ # Ignore outputs
53
+ *.out
54
+ *.tmp
55
+
56
+ # Ignore Python and Jupyter temp files
57
+ *.tmp
58
+ *.temp
59
+ *~
60
+ *.bak
61
+ *.swp
62
+ *.swo
63
+ *.pyc
64
+ *.pyo
65
+ *$py.class
66
+ *.python
67
+ # Jupyter Notebook temp files
68
+ **/tempCodeRunnerFile.*
69
+
70
+ # CONFIG.CFG
71
+ *.cfg
72
+
73
+ *assembly_ai.py
74
+ *stt_routes.py
75
+ *stt.py
76
+
77
+ *test.ipynb
78
+ *chat1.html
79
+ *chat2.html
80
+ *chat3.html
81
+ *chat4.html
82
+ *chat5.html
83
+ *chat6.html
84
+ *chat7.html
85
+ *chat8.html
86
+ *test.html
87
+ *assembly_ai_transcriber_without_mic.py
88
+ *assembly_ai_transcriber_old.py
89
+ *ws_transcriber_old.py
90
+ *transcribe_old.py
91
+ *ws_speak.py
92
+ *endpoint_tester.ipynb
93
+ *stt_end.py
94
+ *speak_old.py
95
+ *chat_old.py
96
+ *unilever_logo1.png
97
+
98
+ *.png
99
+ *config_old.py
100
+
101
+ *ws_transcriber.py
102
+ *speak.py
103
+ *transcribe.py
Dockerfile ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim-bookworm
2
+
3
+ # # Set up a new user named "user" with user ID 1000
4
+ # RUN useradd -m -u 1000 user
5
+
6
+ # # Switch to the "user" user
7
+ # USER user
8
+
9
+ LABEL maintainer="Maqbool Ahmed <[email protected]>"
10
+
11
+ ENV DEBIAN_FRONTEND=noninteractive \
12
+ PYTHONUNBUFFERED=1 \
13
+ PYTHONDONTWRITEBYTECODE=1 \
14
+ PYTHONWARNINGS="ignore:Unverified HTTPS request"
15
+
16
+
17
+ # Install dependencies for PyAudio
18
+ RUN apt-get update && apt-get install -y \
19
+ gcc \
20
+ libasound2-dev \
21
+ portaudio19-dev \
22
+ libportaudio2 \
23
+ libportaudiocpp0 \
24
+ && rm -rf /var/lib/apt/lists/*
25
+
26
+ # Upgrade pip
27
+ RUN python3 -m pip install --upgrade pip==24.3.1
28
+
29
+ # Create workdir
30
+ WORKDIR /app
31
+
32
+ # Copy requirements first (for caching)
33
+ COPY requirements.txt .
34
+
35
+ # Install dependencies (uvicorn[standard] includes websockets)
36
+ RUN pip install --no-cache-dir -r requirements.txt \
37
+ && pip install --no-cache-dir "uvicorn[standard]"
38
+
39
+ # Copy app code and assets
40
+ COPY app ./app
41
+ # COPY *.png ./
42
+
43
+ # Expose Hugging Face Spaces port (must be 7860)
44
+ EXPOSE 7860
45
+
46
+ # Start FastAPI with WebSocket support
47
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--ws", "websockets"]
app/api/chat.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
3
+ from app.models.chat import ChatRequest
4
+ from app.services.chat_service import (
5
+ get_embedding_async,
6
+ query_pinecone_async,
7
+ query_groq_stream_async,
8
+ pinecone_query_maker,
9
+ )
10
+ import json
11
+ import uuid
12
+
13
+ router = APIRouter()
14
+
15
+ # In-memory session store (for dev only)
16
+ chat_sessions = {}
17
+
18
+
19
+ @router.get("/")
20
+ def serve_chat_html():
21
+ """Serve chat.html from static directory."""
22
+ return FileResponse("app/static/chat.html")
23
+
24
+
25
+ @router.post("/stream/start")
26
+ async def start_chat_session(payload: ChatRequest):
27
+ """Initialize a new chat session."""
28
+ session_id = str(uuid.uuid4())
29
+ chat_sessions[session_id] = {
30
+ "user_query": payload.user_query,
31
+ "chat_state": payload.chat_state,
32
+ "memory_state": payload.memory_state,
33
+ }
34
+ return {"session_id": session_id}
35
+
36
+
37
+ @router.get("/stream")
38
+ async def chat_stream(session_id: str):
39
+ """Stream AI response for a session using SSE."""
40
+ session = chat_sessions.pop(session_id, None)
41
+ if not session:
42
+ return JSONResponse({"error": "Invalid session_id"}, status_code=400)
43
+
44
+ user_query = session["user_query"]
45
+ chat_state = session["chat_state"]
46
+ memory_state = session["memory_state"]
47
+
48
+ async def event_stream():
49
+ partial_response = ""
50
+ # Build chat history from memory_state
51
+
52
+ history_str = "\n".join(memory_state)
53
+ if len(memory_state) > 0:
54
+ history = f"{memory_state[-1]}"
55
+ else:
56
+ history = f"{memory_state}"
57
+
58
+ pinecone_query = pinecone_query_maker(user_query, history)
59
+
60
+ # Async embedding
61
+ embedding = await get_embedding_async(pinecone_query)
62
+
63
+ # Async Pinecone query
64
+ relevant_chunks = await query_pinecone_async(embedding)
65
+
66
+ # Sort all chunks in descending order by score
67
+ sorted_chunks = sorted(relevant_chunks, key=lambda x: x["score"], reverse=True)
68
+
69
+ # Select the top 6 scoring chunks
70
+ top_chunks = sorted_chunks[:6]
71
+
72
+ # Build context from top chunks
73
+ context = "\n".join(
74
+ f"{chunk['score']}\n"
75
+ f"{chunk['metadata'].get('source', '')}\n"
76
+ f"{chunk['metadata'].get('link', '')}\n"
77
+ f"{chunk['metadata'].get('text', '')}"
78
+ for chunk in top_chunks
79
+ )
80
+
81
+ prompt = f"""
82
+ You are a professional assistant for helping user to understand ISO data.
83
+
84
+ 🎯 Use *only* the information from the provided document context to respond.
85
+
86
+ 🚫 DO NOT:
87
+ - Invent, assume, or infer beyond what's explicitly stated in the context.
88
+ - Generate or modify any links—only use links already present in the **context**.
89
+ - Reference external sources or extrapolate outside the provided material.
90
+
91
+ 📌 Note:
92
+ If the user replies with "no", "nah", "not", "nope", or "nopes", respond only with:
93
+ **"Anything else I can help you with?"**
94
+
95
+ ---
96
+
97
+ 📄 **Context**:
98
+ {context}
99
+
100
+ ---
101
+
102
+ 🧠 **Response Guidelines**:
103
+ - ✅ Format all outputs in **Markdown** for readability.
104
+ - ✅ Convert any tables into well-structured paragraphs for smoother narrative flow.
105
+ - ✅ Conclude every response with the following clearly labeled sections:
106
+
107
+ ### Follow-Up Question Suggestions:
108
+ *Would you like to ask any of these follow-up questions based on the above?*
109
+
110
+ - ✅ If the context contains no relevant information, respond with:
111
+ *"No relevant information available in the provided documents."*
112
+
113
+ - ✅ Never fabricate sources, assumptions, or external references.
114
+ """
115
+
116
+ # Async LLM streaming with full context
117
+ async for chunk in query_groq_stream_async(pinecone_query, prompt):
118
+ # async for chunk in query_groq_stream_async(user_prompt, prompt):
119
+ partial_response += chunk
120
+ data = {
121
+ "partial_response": partial_response,
122
+ "chat_state": chat_state + [(user_query, partial_response)],
123
+ "memory_state": memory_state,
124
+ }
125
+ yield f"data: {json.dumps(data)}\n\n"
126
+
127
+ # Final state update
128
+ memory_state.append(f"User: {user_query}")
129
+ memory_state.append(f"{partial_response}")
130
+ chat_state.append((user_query, partial_response))
131
+
132
+ yield f"data: {json.dumps({'final': True, 'chat_state': chat_state, 'memory_state': memory_state})}\n\n"
133
+
134
+ return StreamingResponse(event_stream(), media_type="text/event-stream")
135
+
136
+
137
+ @router.post("/clear")
138
+ async def clear_chat():
139
+ """Clear session state (frontend reset)."""
140
+ return JSONResponse(content={"chat_state": [], "memory_state": [], "input_box": ""})
app/core/clients.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pinecone import Pinecone
2
+ from openai import OpenAI
3
+ from groq import Groq
4
+ from cartesia import Cartesia
5
+ # from app.core.config import PINECONE_API, NVIDIA_API, GROQ_API_KEY, CARTESIA_API_KEY
6
+ from app.core.config import PINECONE_API, NVIDIA_API, GROQ_API_KEY
7
+
8
+ # from app.core.config import PINECONE_API, NVIDIA_API
9
+
10
+ # Pinecone client
11
+ pc = Pinecone(api_key=PINECONE_API)
12
+ index = pc.Index("isocert")
13
+
14
+ # NVIDIA embedding client
15
+ embedding_client = OpenAI(
16
+ api_key=NVIDIA_API,
17
+ base_url="https://integrate.api.nvidia.com/v1",
18
+ )
19
+
20
+ # Groq client
21
+ groq_client = Groq(api_key=GROQ_API_KEY)
22
+
23
+ # # Cartesia client
24
+ # cartesia_client = Cartesia(api_key=CARTESIA_API_KEY)
25
+
26
+ # # tts_test
27
+ # cartesia_client = Cartesia(api_key=)
app/core/config.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ # Load variables from .env file (if present)
5
+ load_dotenv()
6
+
7
+ # Application mode
8
+ APPLICATION_TYPE = os.getenv("APPLICATION_TYPE", "STANDALONE")
9
+ print(f"APPLICATION_TYPE = {APPLICATION_TYPE}")
10
+
11
+ # === PINECONE ===
12
+ PINECONE_API = os.getenv("PINECONE_API")
13
+ PINECONE_ENV = os.getenv("PINECONE_ENV")
14
+ PINECONE_NAMESPACE = os.getenv("PINECONE_NAMESPACE", "").split(",") if os.getenv("PINECONE_NAMESPACE") else [
15
+ "iso_certificates",
16
+ ]
17
+
18
+ # === GROQ ===
19
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY") # instead of TEST_API
20
+
21
+ LLM_MODEL = os.getenv("LLM_MODEL")
22
+
23
+ # === NVIDIA ===
24
+ NVIDIA_API = os.getenv("NVIDEA_EMBEDDING_API")
25
+
26
+ # # === ASSEMBLY ===
27
+ # ASSEMBLY_API_KEY = os.getenv("ASSEMBLY_API_KEY")
28
+
29
+ # # === CARTESIA ===
30
+ # CARTESIA_API_KEY = os.getenv("CARTESIA_API_KEY")
app/main.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+ # from app.api import chat, transcribe, speak
5
+ from app.api import chat
6
+
7
+ app = FastAPI(title="AI Assistant for ISO Documents")
8
+
9
+ # CORS
10
+ app.add_middleware(
11
+ CORSMiddleware,
12
+ allow_origins=["*"], # Change to specific frontend domain in prod
13
+ allow_credentials=True,
14
+ allow_methods=["*"],
15
+ allow_headers=["*"],
16
+ )
17
+
18
+ # Static files
19
+ app.mount("/static", StaticFiles(directory="app/static"), name="static")
20
+
21
+ # Routers
22
+ app.include_router(chat.router, prefix="/chat", tags=["Chat"])
23
+
24
+ # app.include_router(transcribe.router, prefix="/stt")
25
+
26
+ # app.include_router(speak.router, prefix="/tts")
27
+
28
+ print("✅ FastAPI backend for AI Assistant is ready.")
29
+
30
+ # print("🌐 Visit http://127.0.0.1:8000 to open chat UI.")
31
+ # print("📄 API docs available at http://127.0.0.1:8000/docs")
32
+
33
+ print("🌐 Visit https://inveros-tech-iso-rag.hf.space/chat to open chat UI.")
34
+ print("📄 API docs available at https://inveros-tech-iso-rag.hf.space/docs")
app/models/chat.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Literal, Optional
3
+ from datetime import datetime
4
+
5
+
6
+ class ChatRequest(BaseModel):
7
+ user_query: str
8
+ chat_state: list
9
+ memory_state: list
10
+
11
+
12
+ # class STTResponse(BaseModel):
13
+ # type: Literal["partial", "final", "error", "closed"]
14
+ # text: Optional[str] = None
15
+ # timestamp: str = datetime.utcnow().isoformat()
16
+ # code: Optional[int] = None
17
+ # reason: Optional[str] = None
18
+
19
+
20
+ class STTResponse(BaseModel):
21
+ type: Literal["partial", "final", "error", "closed"]
22
+ text: Optional[str] = None
23
+ timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
24
+ code: Optional[int] = None
25
+ reason: Optional[str] = None
app/services/chat_service.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi.concurrency import run_in_threadpool
2
+
3
+ # from app.core.clients import embedding_client, index, groq_client
4
+ from openai import OpenAI
5
+ from app.core.clients import index
6
+ from groq import Groq
7
+ from app.core.config import LLM_MODEL, PINECONE_NAMESPACE, NVIDIA_API, GROQ_API_KEY
8
+ import logging
9
+
10
+
11
+ # ------------------------
12
+ # NVIDIA Embedding API
13
+ # ------------------------
14
+ def _get_embedding(text="None"):
15
+ """Blocking call to get NVIDIA embedding for a text string."""
16
+ # NVIDIA embedding client
17
+ embedding_client = OpenAI(
18
+ api_key=NVIDIA_API,
19
+ base_url="https://integrate.api.nvidia.com/v1",
20
+ )
21
+
22
+ response = embedding_client.embeddings.create(
23
+ input=text,
24
+ model="nvidia/nv-embed-v1",
25
+ encoding_format="float",
26
+ extra_body={"input_type": "query", "truncate": "NONE"},
27
+ )
28
+ return response.data[0].embedding
29
+
30
+
31
+ async def get_embedding_async(text="None"):
32
+ """Async wrapper for embedding call."""
33
+ return await run_in_threadpool(_get_embedding, text)
34
+
35
+
36
+ # ------------------------
37
+ # Pinecone Query
38
+ # ------------------------
39
+ def _query_pinecone(embedding):
40
+ """Blocking Pinecone query."""
41
+ result = index.query_namespaces(
42
+ vector=embedding,
43
+ namespaces=PINECONE_NAMESPACE,
44
+ metric="cosine",
45
+ top_k=35,
46
+ include_metadata=True,
47
+ )
48
+ return result["matches"]
49
+
50
+
51
+ async def query_pinecone_async(embedding):
52
+ """Async wrapper for Pinecone query."""
53
+ return await run_in_threadpool(_query_pinecone, embedding)
54
+
55
+
56
+ # Instantiate client once and reuse across calls
57
+ client = Groq(api_key=GROQ_API_KEY)
58
+
59
+
60
+ def groq_chunk_cleaner(chunk: str) -> str:
61
+ """
62
+ Cleans a text chunk using Groq's model by stripping formatting,
63
+ sources, and boilerplate sections.
64
+ Preserves the exact user wording and phrasing of the main content.
65
+ """
66
+ try:
67
+ completion = client.chat.completions.create(
68
+ model="llama-3.1-8b-instant",
69
+ messages=[
70
+ {
71
+ "role": "system",
72
+ "content": (
73
+ "You are a Text Cleaning Assistant.\n"
74
+ "Your ONLY job is to return the exact same text as provided by the user, "
75
+ "but cleaned of unwanted elements.\n\n"
76
+ "Strict rules:\n"
77
+ "- DO NOT rephrase, paraphrase, or summarize the wording.\n"
78
+ "- Preserve all original sentences, casing, punctuation, and wording of the main text.\n"
79
+ "- Remove all formatting (Markdown, HTML, LaTeX, bullet points, headers, etc.).\n"
80
+ "- Remove links, URLs, and citations.\n"
81
+ "- Remove boilerplate sections such as:\n"
82
+ " * 'Sources:' and everything after it.\n"
83
+ " * 'Follow-up Question Suggestions:' and everything after it.\n"
84
+ "- Output only the cleaned plain text content with no extra commentary."
85
+ ),
86
+ },
87
+ {
88
+ "role": "user",
89
+ "content": chunk,
90
+ },
91
+ ],
92
+ temperature=0.1, # strictly deterministic
93
+ )
94
+
95
+ return completion.choices[0].message.content.strip()
96
+
97
+ except Exception as e:
98
+ logging.error("Groq text cleaning failed: %s", e)
99
+ return "[Error] Unable to process the request at the moment."
100
+
101
+
102
+ def pinecone_query_maker(user_query, history):
103
+ """
104
+ Generates an optimized prompt from Groq using prior history and current input.
105
+ If the input is unrelated, history is ignored. Follow-up logic is handled externally.
106
+ """
107
+ client = Groq(api_key=GROQ_API_KEY)
108
+ try:
109
+ chat_completion = client.chat.completions.create(
110
+ messages=[
111
+ {
112
+ "role": "system",
113
+ "content": (
114
+ "You are a prompt optimization engine. Your task is to generate a clean, context-aware prompt "
115
+ "for querying a vector database or a language model.\n\n"
116
+ "You are given:\n"
117
+ "- Prior conversation history (which may or may not be relevant)\n"
118
+ "- A new user input (current query)\n\n"
119
+ "Instructions:\n"
120
+ "- If the user input is clearly related to the conversation history, merge the relevant context "
121
+ "to enrich and clarify the prompt.\n"
122
+ "- If the new input is unrelated or self-contained, do **not** incorporate history—just enhance the "
123
+ "standalone query for precision and clarity.\n"
124
+ "- If user input contains 'no', 'nah', 'not', 'nope', or 'nopes', respond only with: 'no'.\n"
125
+ "- If user input contains 'yes', 'yup', 'yo', or 'y', analyze the prior question or topic in the Conversation History and generate a meaningful follow-up prompt based on it.\n\n"
126
+ "Return only the final optimized user prompt as plain text—no extra commentary, headers, or formatting."
127
+ ),
128
+ },
129
+ {
130
+ "role": "user",
131
+ "content": (
132
+ f"Conversation History:\n{history.strip()}\n\n"
133
+ f"User Input:\n{user_query.strip()}"
134
+ ),
135
+ },
136
+ ],
137
+ model=LLM_MODEL,
138
+ temperature=0.4,
139
+ stream=False,
140
+ )
141
+
142
+ return chat_completion.choices[0].message.content.strip()
143
+
144
+ except Exception as e:
145
+ print("Groq streaming failed:", e)
146
+ return "[Error] Unable to process the request at the moment."
147
+
148
+
149
+ def _query_groq_stream(user_input, relevant_context):
150
+ """Blocking Groq streaming call."""
151
+ groq_client = Groq(api_key=GROQ_API_KEY)
152
+ try:
153
+ chat_completion = groq_client.chat.completions.create(
154
+ messages=[
155
+ {"role": "system", "content": f"{relevant_context}"},
156
+ {"role": "user", "content": f"{user_input}"},
157
+ ],
158
+ model=LLM_MODEL,
159
+ temperature=0.3,
160
+ stream=True,
161
+ )
162
+
163
+ for chunk in chat_completion:
164
+ content = chunk.choices[0].delta.content or ""
165
+ yield content
166
+
167
+ except Exception as e:
168
+ print("Groq streaming failed:", e)
169
+ yield "[Error] Unable to process the request at the moment."
170
+
171
+
172
+ async def query_groq_stream_async(user_input, relevant_context):
173
+ """Async wrapper for Groq streaming."""
174
+ generator = _query_groq_stream(user_input, relevant_context)
175
+ for chunk in generator:
176
+ yield chunk
app/static/chat.html ADDED
@@ -0,0 +1,947 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>ISO Certificate Query System</title>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
11
+ <style>
12
+ :root {
13
+ --primary-color: #0068B5;
14
+ --secondary-color: #00A8E2;
15
+ --accent-color: #FFCB05;
16
+ --text-color: #333;
17
+ --bg-color: #f9f9f9;
18
+ --message-user-bg: #e3f2fd;
19
+ --message-assistant-bg: #fff;
20
+ --border-color: #e0e0e0;
21
+ --shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
22
+ --success-color: #4CAF50;
23
+ --warning-color: #FF9800;
24
+ --error-color: #F44336;
25
+ }
26
+
27
+ .dark-mode {
28
+ --primary-color: #2C92D5;
29
+ --secondary-color: #4AB7E6;
30
+ --accent-color: #FFD54F;
31
+ --text-color: #f0f0f0;
32
+ --bg-color: #1a1a1a;
33
+ --message-user-bg: #2A3C4F;
34
+ --message-assistant-bg: #2D2D2D;
35
+ --border-color: #444;
36
+ --shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
37
+ }
38
+
39
+ * {
40
+ box-sizing: border-box;
41
+ margin: 0;
42
+ padding: 0;
43
+ }
44
+
45
+ body {
46
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
47
+ background-color: var(--bg-color);
48
+ color: var(--text-color);
49
+ line-height: 1.6;
50
+ transition: background-color 0.3s, color 0.3s;
51
+ display: flex;
52
+ flex-direction: column;
53
+ height: 100vh;
54
+ overflow: hidden;
55
+ }
56
+
57
+ .header {
58
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
59
+ color: white;
60
+ padding: 1rem;
61
+ display: flex;
62
+ justify-content: space-between;
63
+ align-items: center;
64
+ box-shadow: var(--shadow);
65
+ z-index: 10;
66
+ }
67
+
68
+ .logo {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 0.5rem;
72
+ font-weight: bold;
73
+ font-size: 1.2rem;
74
+ }
75
+
76
+ .logo i {
77
+ font-size: 1.5rem;
78
+ }
79
+
80
+ .controls {
81
+ display: flex;
82
+ gap: 0.5rem;
83
+ }
84
+
85
+ .btn {
86
+ background: rgba(255, 255, 255, 0.2);
87
+ border: none;
88
+ border-radius: 50%;
89
+ width: 2.5rem;
90
+ height: 2.5rem;
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ cursor: pointer;
95
+ color: white;
96
+ transition: background 0.2s;
97
+ }
98
+
99
+ .btn:hover {
100
+ background: rgba(255, 255, 255, 0.3);
101
+ }
102
+
103
+ .main {
104
+ display: flex;
105
+ flex: 1;
106
+ overflow: hidden;
107
+ }
108
+
109
+ .sidebar {
110
+ width: 300px;
111
+ background-color: var(--bg-color);
112
+ border-right: 1px solid var(--border-color);
113
+ padding: 1rem;
114
+ display: flex;
115
+ flex-direction: column;
116
+ transition: transform 0.3s;
117
+ }
118
+
119
+ .sidebar.hidden {
120
+ transform: translateX(-100%);
121
+ }
122
+
123
+ .connection-status {
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 0.5rem;
127
+ margin-bottom: 1rem;
128
+ padding: 0.5rem;
129
+ border-radius: 4px;
130
+ background-color: rgba(0, 0, 0, 0.05);
131
+ }
132
+
133
+ .connection-status i {
134
+ font-size: 0.7rem;
135
+ }
136
+
137
+ .chat-container {
138
+ flex: 1;
139
+ display: flex;
140
+ flex-direction: column;
141
+ overflow: hidden;
142
+ }
143
+
144
+ .chat-box {
145
+ flex: 1;
146
+ padding: 1rem;
147
+ overflow-y: auto;
148
+ display: flex;
149
+ flex-direction: column;
150
+ gap: 1rem;
151
+ }
152
+
153
+ .message {
154
+ max-width: 80%;
155
+ padding: 0.75rem 1rem;
156
+ border-radius: 1rem;
157
+ position: relative;
158
+ animation: fadeIn 0.3s;
159
+ box-shadow: var(--shadow);
160
+ }
161
+
162
+ @keyframes fadeIn {
163
+ from {
164
+ opacity: 0;
165
+ transform: translateY(10px);
166
+ }
167
+
168
+ to {
169
+ opacity: 1;
170
+ transform: translateY(0);
171
+ }
172
+ }
173
+
174
+ .message.user {
175
+ align-self: flex-end;
176
+ background-color: var(--message-user-bg);
177
+ border-bottom-right-radius: 0.25rem;
178
+ }
179
+
180
+ .message.assistant {
181
+ align-self: flex-start;
182
+ background-color: var(--message-assistant-bg);
183
+ border-bottom-left-radius: 0.25rem;
184
+ }
185
+
186
+ .message-time {
187
+ font-size: 0.7rem;
188
+ opacity: 0.7;
189
+ margin-top: 0.5rem;
190
+ }
191
+
192
+ .message-actions {
193
+ position: absolute;
194
+ top: 0.5rem;
195
+ right: 0.5rem;
196
+ display: none;
197
+ gap: 0.25rem;
198
+ }
199
+
200
+ .message:hover .message-actions {
201
+ display: flex;
202
+ }
203
+
204
+ .message-action {
205
+ background: rgba(0, 0, 0, 0.1);
206
+ border: none;
207
+ border-radius: 50%;
208
+ width: 1.5rem;
209
+ height: 1.5rem;
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ cursor: pointer;
214
+ font-size: 0.7rem;
215
+ color: inherit;
216
+ }
217
+
218
+ .message-action:hover {
219
+ background: rgba(0, 0, 0, 0.2);
220
+ }
221
+
222
+ .typing-indicator {
223
+ display: flex;
224
+ align-items: center;
225
+ gap: 0.5rem;
226
+ padding: 0.75rem 1rem;
227
+ background-color: var(--message-assistant-bg);
228
+ border-radius: 1rem;
229
+ align-self: flex-start;
230
+ margin-bottom: 1rem;
231
+ box-shadow: var(--shadow);
232
+ }
233
+
234
+ .typing-dot {
235
+ width: 8px;
236
+ height: 8px;
237
+ background-color: var(--text-color);
238
+ border-radius: 50%;
239
+ opacity: 0.6;
240
+ animation: typing-dot 1.4s infinite ease-in-out both;
241
+ }
242
+
243
+ .typing-dot:nth-child(1) {
244
+ animation-delay: -0.32s;
245
+ }
246
+
247
+ .typing-dot:nth-child(2) {
248
+ animation-delay: -0.16s;
249
+ }
250
+
251
+ @keyframes typing-dot {
252
+
253
+ 0%,
254
+ 80%,
255
+ 100% {
256
+ transform: scale(0.8);
257
+ }
258
+
259
+ 40% {
260
+ transform: scale(1);
261
+ }
262
+ }
263
+
264
+ .input-area {
265
+ padding: 1rem;
266
+ border-top: 1px solid var(--border-color);
267
+ display: flex;
268
+ gap: 0.5rem;
269
+ background-color: var(--bg-color);
270
+ z-index: 5;
271
+ }
272
+
273
+ .input-wrapper {
274
+ flex: 1;
275
+ position: relative;
276
+ display: flex;
277
+ align-items: flex-end;
278
+ }
279
+
280
+ #user-input {
281
+ flex: 1;
282
+ border: 1px solid var(--border-color);
283
+ border-radius: 1.5rem;
284
+ padding: 0.75rem 3.5rem 0.75rem 1rem;
285
+ resize: none;
286
+ min-height: 3rem;
287
+ max-height: 10rem;
288
+ background-color: var(--bg-color);
289
+ color: var(--text-color);
290
+ font-family: inherit;
291
+ box-shadow: var(--shadow);
292
+ }
293
+
294
+ #user-input:focus {
295
+ outline: none;
296
+ border-color: var(--primary-color);
297
+ }
298
+
299
+ .input-buttons {
300
+ position: absolute;
301
+ right: 0.5rem;
302
+ bottom: 0.5rem;
303
+ display: flex;
304
+ gap: 0.25rem;
305
+ }
306
+
307
+ .input-btn {
308
+ background: none;
309
+ border: none;
310
+ width: 2rem;
311
+ height: 2rem;
312
+ border-radius: 50%;
313
+ display: flex;
314
+ align-items: center;
315
+ justify-content: center;
316
+ cursor: pointer;
317
+ color: var(--text-color);
318
+ }
319
+
320
+ .input-btn:hover {
321
+ background: rgba(0, 0, 0, 0.05);
322
+ }
323
+
324
+ .input-btn.primary {
325
+ background-color: var(--primary-color);
326
+ color: white;
327
+ }
328
+
329
+ .input-btn.primary:hover {
330
+ opacity: 0.9;
331
+ }
332
+
333
+ .settings-panel {
334
+ position: fixed;
335
+ top: 0;
336
+ right: 0;
337
+ bottom: 0;
338
+ width: 300px;
339
+ background-color: var(--bg-color);
340
+ border-left: 1px solid var(--border-color);
341
+ padding: 1.5rem;
342
+ box-shadow: var(--shadow);
343
+ transform: translateX(100%);
344
+ transition: transform 0.3s;
345
+ z-index: 100;
346
+ overflow-y: auto;
347
+ }
348
+
349
+ .settings-panel.open {
350
+ transform: translateX(0);
351
+ }
352
+
353
+ .settings-header {
354
+ display: flex;
355
+ justify-content: space-between;
356
+ align-items: center;
357
+ margin-bottom: 1.5rem;
358
+ padding-bottom: 0.5rem;
359
+ border-bottom: 1px solid var(--border-color);
360
+ }
361
+
362
+ .settings-close {
363
+ background: none;
364
+ border: none;
365
+ cursor: pointer;
366
+ color: var(--text-color);
367
+ font-size: 1.2rem;
368
+ }
369
+
370
+ .settings-group {
371
+ margin-bottom: 1.5rem;
372
+ }
373
+
374
+ .settings-group h3 {
375
+ margin-bottom: 0.75rem;
376
+ font-size: 1rem;
377
+ }
378
+
379
+ .form-group {
380
+ margin-bottom: 1rem;
381
+ }
382
+
383
+ .form-group label {
384
+ display: block;
385
+ margin-bottom: 0.25rem;
386
+ font-size: 0.9rem;
387
+ }
388
+
389
+ .form-group input[type="text"],
390
+ .form-group input[type="number"] {
391
+ width: 100%;
392
+ padding: 0.5rem;
393
+ border: 1px solid var(--border-color);
394
+ border-radius: 4px;
395
+ background-color: var(--bg-color);
396
+ color: var(--text-color);
397
+ }
398
+
399
+ .settings-footer {
400
+ margin-top: 2rem;
401
+ display: flex;
402
+ justify-content: flex-end;
403
+ }
404
+
405
+ #save-settings {
406
+ background: var(--primary-color);
407
+ color: white;
408
+ border: none;
409
+ border-radius: 4px;
410
+ padding: 0.5rem 1rem;
411
+ cursor: pointer;
412
+ }
413
+
414
+ #save-settings:hover {
415
+ opacity: 0.9;
416
+ }
417
+
418
+ .toast {
419
+ position: fixed;
420
+ bottom: 1rem;
421
+ left: 50%;
422
+ transform: translateX(-50%);
423
+ padding: 0.75rem 1.5rem;
424
+ border-radius: 2rem;
425
+ background: var(--text-color);
426
+ color: var(--bg-color);
427
+ box-shadow: var(--shadow);
428
+ display: none;
429
+ z-index: 1000;
430
+ animation: toast-in 0.3s, toast-out 0.3s 2.7s forwards;
431
+ }
432
+
433
+ @keyframes toast-in {
434
+ from {
435
+ opacity: 0;
436
+ transform: translate(-50%, 100%);
437
+ }
438
+
439
+ to {
440
+ opacity: 1;
441
+ transform: translate(-50%, 0);
442
+ }
443
+ }
444
+
445
+ @keyframes toast-out {
446
+ from {
447
+ opacity: 1;
448
+ transform: translate(-50%, 0);
449
+ }
450
+
451
+ to {
452
+ opacity: 0;
453
+ transform: translate(-50%, 100%);
454
+ }
455
+ }
456
+
457
+ .toast.success {
458
+ background: var(--success-color);
459
+ color: white;
460
+ }
461
+
462
+ .toast.warning {
463
+ background: var(--warning-color);
464
+ color: white;
465
+ }
466
+
467
+ .toast.error {
468
+ background: var(--error-color);
469
+ color: white;
470
+ }
471
+
472
+ .quick-actions {
473
+ display: flex;
474
+ flex-wrap: wrap;
475
+ gap: 0.5rem;
476
+ margin-bottom: 1rem;
477
+ }
478
+
479
+ .quick-action {
480
+ background: var(--primary-color);
481
+ color: white;
482
+ border: none;
483
+ border-radius: 1rem;
484
+ padding: 0.5rem 1rem;
485
+ font-size: 0.8rem;
486
+ cursor: pointer;
487
+ transition: background 0.2s;
488
+ }
489
+
490
+ .quick-action:hover {
491
+ background: var(--secondary-color);
492
+ }
493
+
494
+ .certificate-info {
495
+ background-color: var(--message-assistant-bg);
496
+ border-radius: 0.5rem;
497
+ padding: 1rem;
498
+ margin-bottom: 1rem;
499
+ box-shadow: var(--shadow);
500
+ }
501
+
502
+ .certificate-info h3 {
503
+ margin-bottom: 0.5rem;
504
+ color: var(--primary-color);
505
+ }
506
+
507
+ .certificate-info ul {
508
+ padding-left: 1.5rem;
509
+ }
510
+
511
+ .certificate-info li {
512
+ margin-bottom: 0.25rem;
513
+ }
514
+
515
+ @media (max-width: 768px) {
516
+ .sidebar {
517
+ position: absolute;
518
+ left: 0;
519
+ top: 0;
520
+ bottom: 0;
521
+ z-index: 20;
522
+ box-shadow: var(--shadow);
523
+ }
524
+
525
+ .message {
526
+ max-width: 90%;
527
+ }
528
+
529
+ .settings-panel {
530
+ width: 100%;
531
+ }
532
+ }
533
+ </style>
534
+ </head>
535
+
536
+ <body>
537
+ <div class="header">
538
+ <div class="logo">
539
+ <i class="fas fa-certificate"></i>
540
+ <span>ISO Certificate Query</span>
541
+ </div>
542
+ <div class="controls">
543
+ <button id="theme-toggle" class="btn"><i class="fas fa-moon"></i></button>
544
+ <button id="settings-toggle" class="btn"><i class="fas fa-cog"></i></button>
545
+ </div>
546
+ </div>
547
+
548
+ <div class="main">
549
+ <div class="sidebar">
550
+ <div class="connection-status" id="connection-status">
551
+ <i class="fas fa-circle"></i>
552
+ <span>Connecting...</span>
553
+ </div>
554
+
555
+
556
+ <button id="clear" class="btn"
557
+ style="background: var(--error-color); color: white; border-radius: 4px; width: auto; padding: 0.5rem 1rem; margin-top: auto;">
558
+ <i class="fas fa-trash"></i> Clear Chat
559
+ </button>
560
+ </div>
561
+
562
+ <div class="chat-container">
563
+ <div class="chat-box" id="chat-box">
564
+ <div class="message assistant">
565
+ <h3>Welcome to the ISO Certificate Query System</h3>
566
+ <p>I can help you find information about ISO certifications.</p>
567
+ <div class="message-time">Just now</div>
568
+ </div>
569
+ </div>
570
+
571
+ <div class="input-area">
572
+ <div class="input-wrapper">
573
+ <textarea id="user-input" placeholder="Ask about ISO certificates (e.g., 'Show me all ISO 9001 certificates')..." rows="1"></textarea>
574
+ <div class="input-buttons">
575
+ <button id="send" class="input-btn primary">
576
+ <i class="fas fa-paper-plane"></i>
577
+ </button>
578
+ </div>
579
+ </div>
580
+ </div>
581
+ </div>
582
+ </div>
583
+
584
+ <div class="settings-panel" id="settings-panel">
585
+ <div class="settings-header">
586
+ <h2>Settings</h2>
587
+ <button class="settings-close" id="close-settings">
588
+ <i class="fas fa-times"></i>
589
+ </button>
590
+ </div>
591
+ <div class="settings-group">
592
+ <h3>Connection</h3>
593
+ <div class="form-group">
594
+ <label for="api-url">API URL</label>
595
+ <input type="text" id="api-url" placeholder="https://inveros-tech-iso-rag.hf.space">
596
+ </div>
597
+ </div>
598
+ <div class="settings-footer">
599
+ <button id="save-settings">Save Settings</button>
600
+ </div>
601
+ </div>
602
+
603
+ <div class="toast" id="toast"></div>
604
+ <script>
605
+ // Configuration
606
+ const config = {
607
+ // apiUrl: localStorage.getItem('apiUrl') || "http://127.0.0.1:8000",
608
+ apiUrl: localStorage.getItem('apiUrl') || "https://inveros-tech-iso-rag.hf.space",
609
+ maxChatHistory: 10,
610
+ typingIndicatorDelay: 300,
611
+ reconnectDelay: 5000
612
+ };
613
+
614
+ // Global variables
615
+ let chatState = [];
616
+ let memoryState = [];
617
+ let typingIndicatorTimeout = null;
618
+ let currentAssistantMessageDiv = null;
619
+ let eventSource = null;
620
+ let reconnectAttempts = 0;
621
+ let maxReconnectAttempts = 3;
622
+
623
+ // DOM elements
624
+ const chatBox = document.getElementById("chat-box");
625
+ const userInput = document.getElementById("user-input");
626
+ const sendBtn = document.getElementById("send");
627
+ const clearBtn = document.getElementById("clear");
628
+ const themeToggle = document.getElementById("theme-toggle");
629
+ const settingsToggle = document.getElementById("settings-toggle");
630
+ const settingsPanel = document.getElementById("settings-panel");
631
+ const saveSettingsBtn = document.getElementById("save-settings");
632
+ const closeSettingsBtn = document.getElementById("close-settings");
633
+ const apiUrlInput = document.getElementById("api-url");
634
+ const toast = document.getElementById("toast");
635
+ const connectionStatus = document.getElementById("connection-status");
636
+ const quickActions = document.querySelectorAll('.quick-action');
637
+
638
+ // Initialize UI
639
+ function initializeUI() {
640
+ apiUrlInput.value = config.apiUrl;
641
+
642
+ // Set theme based on preference
643
+ if (localStorage.getItem('darkMode') === 'true') {
644
+ document.body.classList.add('dark-mode');
645
+ themeToggle.innerHTML = '<i class="fas fa-sun"></i>';
646
+ }
647
+
648
+ // Auto-resize textarea
649
+ userInput.addEventListener('input', function () {
650
+ this.style.height = 'auto';
651
+ this.style.height = (this.scrollHeight) + 'px';
652
+ });
653
+ }
654
+
655
+ // Append message to chat with sanitization
656
+ function appendMessage(role, text, timestamp = new Date()) {
657
+ const message = document.createElement("div");
658
+ message.className = `message ${role}`;
659
+
660
+ // Generate unique ID for the message
661
+ const messageId = 'msg-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
662
+ message.setAttribute('data-message-id', messageId);
663
+
664
+ // Sanitize and parse markdown
665
+ const cleanText = DOMPurify.sanitize(marked.parse(text));
666
+
667
+ message.innerHTML = `
668
+ ${cleanText}
669
+ <div class="message-time">${timestamp.toLocaleTimeString()}</div>
670
+ <div class="message-actions">
671
+ <button class="message-action" title="Copy"><i class="fas fa-copy"></i></button>
672
+ <button class="message-action" title="Delete"><i class="fas fa-trash"></i></button>
673
+ </div>
674
+ `;
675
+
676
+ // Add event listeners to action buttons
677
+ const copyBtn = message.querySelector('.message-action:first-child');
678
+ const deleteBtn = message.querySelector('.message-action:last-child');
679
+
680
+ copyBtn.addEventListener('click', () => {
681
+ navigator.clipboard.writeText(text);
682
+ showToast('Message copied to clipboard', 'success');
683
+ });
684
+
685
+ deleteBtn.addEventListener('click', () => {
686
+ message.remove();
687
+ showToast('Message deleted', 'info');
688
+ });
689
+
690
+ chatBox.appendChild(message);
691
+ chatBox.scrollTop = chatBox.scrollHeight;
692
+
693
+ return message;
694
+ }
695
+
696
+ // Show typing indicator
697
+ function showTypingIndicator() {
698
+ if (document.querySelector('.typing-indicator')) return;
699
+
700
+ const typingDiv = document.createElement('div');
701
+ typingDiv.className = 'typing-indicator';
702
+ typingDiv.innerHTML = `
703
+ <div class="typing-dot"></div>
704
+ <div class="typing-dot"></div>
705
+ <div class="typing-dot"></div>
706
+ <span>Searching certificates...</span>
707
+ `;
708
+ chatBox.appendChild(typingDiv);
709
+ chatBox.scrollTop = chatBox.scrollHeight;
710
+ }
711
+
712
+ // Hide typing indicator
713
+ function hideTypingIndicator() {
714
+ const typingIndicator = document.querySelector('.typing-indicator');
715
+ if (typingIndicator) {
716
+ typingIndicator.remove();
717
+ }
718
+ }
719
+
720
+ // Show toast notification
721
+ function showToast(message, type = 'info', duration = 3000) {
722
+ toast.textContent = message;
723
+ toast.className = `toast ${type}`;
724
+ toast.style.display = 'block';
725
+
726
+ setTimeout(() => {
727
+ toast.style.display = 'none';
728
+ }, duration);
729
+ }
730
+
731
+ // Show error message
732
+ function showError(message) {
733
+ showToast(message, 'error');
734
+ }
735
+
736
+ // Update connection status
737
+ function updateConnectionStatus(connected) {
738
+ if (connected) {
739
+ connectionStatus.innerHTML = '<i class="fas fa-circle" style="color: #2ecc71;"></i> Connected';
740
+ connectionStatus.style.color = '#2ecc71';
741
+ } else {
742
+ connectionStatus.innerHTML = '<i class="fas fa-circle" style="color: #e74c3c;"></i> Disconnected';
743
+ connectionStatus.style.color = '#e74c3c';
744
+ }
745
+ }
746
+
747
+ // Send query to backend with auto-reconnect
748
+ function sendQuery(query = null) {
749
+ const queryText = query || userInput.value.trim();
750
+ if (!queryText) return;
751
+
752
+ // If coming from text input, clear the input
753
+ if (!query) {
754
+ appendMessage("user", queryText);
755
+ userInput.value = "";
756
+ userInput.style.height = 'auto';
757
+ } else {
758
+ appendMessage("user", queryText);
759
+ }
760
+
761
+ // Show typing indicator after a short delay
762
+ if (typingIndicatorTimeout) {
763
+ clearTimeout(typingIndicatorTimeout);
764
+ }
765
+ typingIndicatorTimeout = setTimeout(() => {
766
+ showTypingIndicator();
767
+ }, config.typingIndicatorDelay);
768
+
769
+ // Close any existing event source
770
+ if (eventSource) {
771
+ eventSource.close();
772
+ }
773
+
774
+ fetch(`${config.apiUrl}/chat/stream/start`, {
775
+ method: "POST",
776
+ headers: { "Content-Type": "application/json" },
777
+ body: JSON.stringify({
778
+ user_query: queryText,
779
+ chat_state: chatState,
780
+ memory_state: memoryState
781
+ })
782
+ })
783
+ .then(res => {
784
+ if (!res.ok) {
785
+ throw new Error(`HTTP error! status: ${res.status}`);
786
+ }
787
+ return res.json();
788
+ })
789
+ .then(data => {
790
+ updateConnectionStatus(true);
791
+ reconnectAttempts = 0;
792
+
793
+ const sessionId = data.session_id;
794
+ eventSource = new EventSource(`${config.apiUrl}/chat/stream?session_id=${sessionId}`);
795
+
796
+ eventSource.onmessage = (event) => {
797
+ hideTypingIndicator();
798
+ const data = JSON.parse(event.data);
799
+
800
+ if (data.final) {
801
+ chatState = data.chat_state;
802
+ memoryState = data.memory_state;
803
+ eventSource.close();
804
+ currentAssistantMessageDiv = null;
805
+ } else {
806
+ const assistantMessage = DOMPurify.sanitize(marked.parse(data.partial_response));
807
+
808
+ if (!currentAssistantMessageDiv) {
809
+ currentAssistantMessageDiv = document.createElement("div");
810
+ currentAssistantMessageDiv.className = "message assistant";
811
+ currentAssistantMessageDiv.innerHTML = assistantMessage;
812
+ chatBox.appendChild(currentAssistantMessageDiv);
813
+ } else {
814
+ currentAssistantMessageDiv.innerHTML = assistantMessage;
815
+ }
816
+ chatBox.scrollTop = chatBox.scrollHeight;
817
+ }
818
+ };
819
+
820
+ eventSource.onerror = (err) => {
821
+ console.error("SSE error", err);
822
+ hideTypingIndicator();
823
+
824
+ if (reconnectAttempts < maxReconnectAttempts) {
825
+ reconnectAttempts++;
826
+ showToast(`Connection lost. Reconnecting (${reconnectAttempts}/${maxReconnectAttempts})...`, 'warning');
827
+ setTimeout(() => sendQuery(queryText), config.reconnectDelay);
828
+ } else {
829
+ appendMessage("assistant", "[Error] Connection closed.");
830
+ updateConnectionStatus(false);
831
+ eventSource.close();
832
+ currentAssistantMessageDiv = null;
833
+ }
834
+ };
835
+ })
836
+ .catch(err => {
837
+ console.error("Failed to send query", err);
838
+ hideTypingIndicator();
839
+ appendMessage("assistant", "[Error] Unable to send request.");
840
+ updateConnectionStatus(false);
841
+
842
+ if (reconnectAttempts < maxReconnectAttempts) {
843
+ reconnectAttempts++;
844
+ showToast(`Connection error. Retrying (${reconnectAttempts}/${maxReconnectAttempts})...`, 'warning');
845
+ setTimeout(() => sendQuery(queryText), config.reconnectDelay);
846
+ }
847
+ });
848
+ }
849
+
850
+ // Clear chat history
851
+ function clearChat() {
852
+ fetch(`${config.apiUrl}/chat/clear`, { method: "POST" })
853
+ .then(res => res.json())
854
+ .then(data => {
855
+ chatState = data.chat_state;
856
+ memoryState = data.memory_state;
857
+ chatBox.innerHTML = "";
858
+ userInput.value = "";
859
+ showToast("Chat cleared", 'success');
860
+ })
861
+ .catch(() => {
862
+ appendMessage("assistant", "[Error] Unable to clear chat.");
863
+ showToast("Failed to clear chat", 'error');
864
+ });
865
+ }
866
+
867
+ // Initialize event listeners
868
+ function initializeEventListeners() {
869
+ // Send button and input
870
+ sendBtn.addEventListener('click', () => sendQuery());
871
+ userInput.addEventListener('keypress', (e) => {
872
+ if (e.key === 'Enter' && !e.shiftKey) {
873
+ e.preventDefault();
874
+ sendQuery();
875
+ }
876
+ });
877
+
878
+ // Quick action buttons
879
+ quickActions.forEach(button => {
880
+ button.addEventListener('click', () => {
881
+ const query = button.getAttribute('data-query');
882
+ userInput.value = query;
883
+ sendQuery();
884
+ });
885
+ });
886
+
887
+ // Clear chat
888
+ clearBtn.addEventListener('click', clearChat);
889
+
890
+ // Theme toggle
891
+ themeToggle.addEventListener('click', () => {
892
+ document.body.classList.toggle('dark-mode');
893
+ const isDark = document.body.classList.contains('dark-mode');
894
+ themeToggle.innerHTML = isDark ? '<i class="fas fa-sun"></i>' : '<i class="fas fa-moon"></i>';
895
+ localStorage.setItem('darkMode', isDark);
896
+ });
897
+
898
+ // Settings panel
899
+ settingsToggle.addEventListener('click', () => {
900
+ settingsPanel.classList.toggle('open');
901
+ });
902
+
903
+ closeSettingsBtn.addEventListener('click', () => {
904
+ settingsPanel.classList.remove('open');
905
+ });
906
+
907
+ // Save settings
908
+ saveSettingsBtn.addEventListener('click', () => {
909
+ config.apiUrl = apiUrlInput.value.trim();
910
+ localStorage.setItem('apiUrl', config.apiUrl);
911
+ settingsPanel.classList.remove('open');
912
+ showToast('Settings saved', 'success');
913
+ });
914
+
915
+ // Close settings when clicking outside
916
+ document.addEventListener('click', (e) => {
917
+ if (!settingsPanel.contains(e.target) && e.target !== settingsToggle) {
918
+ settingsPanel.classList.remove('open');
919
+ }
920
+ });
921
+
922
+ // Cleanup on page unload
923
+ window.addEventListener('beforeunload', () => {
924
+ if (eventSource) {
925
+ eventSource.close();
926
+ }
927
+ });
928
+ }
929
+
930
+ // Initialize the app
931
+ function initializeApp() {
932
+ initializeUI();
933
+ initializeEventListeners();
934
+ updateConnectionStatus(false);
935
+
936
+ // Check initial connection
937
+ fetch(`${config.apiUrl}/chat/`)
938
+ .then(() => updateConnectionStatus(true))
939
+ .catch(() => updateConnectionStatus(false));
940
+ }
941
+
942
+ // Start the app
943
+ initializeApp();
944
+ </script>
945
+ </body>
946
+
947
+ </html>
requirements.txt ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ====================
2
+ # Core Data Libraries
3
+ # ====================
4
+ pandas>=2.0.0
5
+ numpy>=1.24.0 # Required for math and signal processing
6
+
7
+ # ============================
8
+ # File and Directory Handling
9
+ # ============================
10
+ glob2>=0.7
11
+ openpyxl # Excel processing
12
+ pypdf # PDF processing
13
+
14
+ # ======================
15
+ # Environment Variables
16
+ # ======================
17
+ python-dotenv>=1.0.0
18
+ dotenv # Redundant but sometimes required for legacy support
19
+
20
+ # ================================
21
+ # Machine Learning & NLP Tooling
22
+ # ================================
23
+ transformers>=4.41.1
24
+ # torch>=2.2.0 # Optional but usually required with transformers
25
+
26
+ # ====================
27
+ # LangChain Ecosystem
28
+ # ====================
29
+ langchain>=0.1.16
30
+ langchain-core>=0.1.40
31
+ langchain-community>=0.0.32
32
+ langchain-text-splitters>=0.0.1
33
+
34
+ # ==================
35
+ # OpenAI Integration
36
+ # ==================
37
+ openai>=1.30.1
38
+
39
+ # ================
40
+ # Vector Databases
41
+ # ================
42
+ pinecone
43
+ pinecone[grpc]
44
+
45
+ # ============
46
+ # Audio Stack
47
+ # ============
48
+ # portaudio==19.6.0
49
+ sounddevice
50
+ soundfile
51
+ assemblyai[extras]
52
+ SpeechRecognition
53
+ PyWavelets==1.8.0 # For signal processing
54
+ matplotlib # For audio visualization
55
+ # pipwin # For Windows-based audio driver installs
56
+ # pyaudio-wheels
57
+ # pyaudio>=0.2.14
58
+ # pyaudio==0.2.11
59
+
60
+ # ========================
61
+ # Web Framework & Runtime
62
+ # ========================
63
+ fastapi
64
+ uvicorn
65
+
66
+ # =======================
67
+ # WebSocket Communication
68
+ # =======================
69
+ websocket-client
70
+ # websocket
71
+
72
+ # ==================
73
+ # Optional Dev Tools
74
+ # ==================
75
+ # ipykernel>=6.29.0
76
+ # jupyter>=1.0.0
77
+
78
+ # ===================
79
+ # UI Framework (TBD)
80
+ # ===================
81
+ # gradio>=4.28.0
82
+ # gradio
83
+
84
+ # ===========
85
+ # Hardware AI
86
+ # ===========
87
+ groq
88
+
89
+ # ============
90
+ # Experimental
91
+ # ============
92
+ # rnnoise-cli
93
+
94
+
95
+ cartesia
96
+ ffmpeg