Martechsol commited on
Commit
392a1db
·
1 Parent(s): bd428d3

Refine RAG pipeline for comprehensive leave retrieval, improve system prompt, and fix Gradio HTML rendering

Browse files
.gitignore CHANGED
@@ -4,4 +4,6 @@ __pycache__/
4
  .mypy_cache/
5
  .venv/
6
  venv/
7
- # No longer ignoring docs or index for this full backup push
 
 
 
4
  .mypy_cache/
5
  .venv/
6
  venv/
7
+ # Ignoring data directory to prevent pushing binary index files
8
+ data/index/
9
+ data/sessions/
app/core/config.py CHANGED
@@ -31,7 +31,7 @@ class Settings(BaseSettings):
31
  chunk_size_tokens: int = 350 # Reduced for TPM safety
32
  chunk_overlap_tokens: int = 80
33
  top_k: int = Field(default=20, alias="TOP_K")
34
- max_context_chunks: int = 5 # Reduced to 5 to stay within 6000 TPM limit
35
  request_timeout_s: float = 20.0
36
  cors_allow_origins: str = Field(default="*", alias="CORS_ALLOW_ORIGINS")
37
  api_key: str = Field(default="", alias="API_KEY")
 
31
  chunk_size_tokens: int = 350 # Reduced for TPM safety
32
  chunk_overlap_tokens: int = 80
33
  top_k: int = Field(default=20, alias="TOP_K")
34
+ max_context_chunks: int = 10 # Increased to 10 to ensure all list items (like leaves) are captured
35
  request_timeout_s: float = 20.0
36
  cors_allow_origins: str = Field(default="*", alias="CORS_ALLOW_ORIGINS")
37
  api_key: str = Field(default="", alias="API_KEY")
app/services/llm.py CHANGED
@@ -8,38 +8,76 @@ import re
8
  # ═══════════════════════════════════════════════════════════════════════
9
  SYSTEM_PROMPT = """You are the Martechsol HR Assistant — intelligent, precise, and formal.
10
 
11
- INTELLIGENCE PROTOCOL (follow in order):
12
-
13
- 1. UNDERSTAND INTENT FIRST
14
- Identify exactly what the user is asking. Apply workplace common sense:
15
- • "timing" or "timings" → office working hours (not salary schedule)
16
- • "leaves" or "paid leaves" list ALL leave types with name + count
17
- Always choose the most natural, day-to-day workplace interpretation.
18
-
19
- 2. SCOPE DISCIPLINE Answer ONLY what was asked.
20
- Never mention policies, procedures, approval processes, or consequences unless user explicitly asks.
21
- "How many leaves?" = names + counts only. Nothing more.
22
- • "What are timings?" = office hours only. Nothing more.
23
-
24
- 3. COMPLETE LISTING When user asks for a category (leaves, benefits, allowances):
25
- List EVERY item with its key detail (name + number) — one per line.
26
-
27
- 4. WORD ECONOMY Keep responses ≤35 words. Always write complete, grammatically correct sentences. Never cut a sentence mid-way.
28
-
29
- 5. ZERO HALLUCINATION Every fact must come from Expert Data only.
30
- If the information is not available, say exactly: "I don't have that information."
31
-
32
- 6. SOURCE SILENCE — NEVER say "document", "handbook", "manual", "policy", "data", or reference any source. Just answer directly and naturally.
33
-
34
- HTML FORMATTING RULES:
35
- Use <b>bold</b> for all key terms, names, numbers, and dates
36
- Use <br> to separate each item or point
37
- • No long paragraphs — one clean line per point
38
-
39
- TONE: Formal, warm, and professional."""
40
-
41
-
42
- def _build_context(chunks: List[Dict[str, str]], max_words: int = 1000) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  """Combines retrieved chunks into a clean context string, capped at max_words.
44
  Prevents TPM spikes when chunks are unexpectedly large."""
45
  if not chunks:
@@ -82,15 +120,25 @@ class LLMService:
82
 
83
  async def generate_multi_queries(self, query: str, history: List[Dict[str, str]]) -> List[str]:
84
  """Generates multiple search queries to capture broader context from the document."""
85
- prompt = f"""You are an HR search optimizer for a company employee handbook. The user asked: "{query}"
86
-
87
- Generate 3 search queries to find ALL relevant information. Priorities:
88
- - For "timing/timings" Query 1 MUST focus on office hours/working hours/workday schedule
89
- - For "leaves/leave/paid leaves" Cover ALL types: casual leave, sick leave, annual leave, maternity leave, paternity leave, hajj leave, study leave
90
- - For "salary/pay/compensation" Cover salary structure, payroll, increments, deductions
91
- - Use HR synonyms and related terminology
92
-
93
- Output ONLY 3 queries, one per line. No numbers, no bullets, no extra text.
 
 
 
 
 
 
 
 
 
 
94
 
95
  Queries:"""
96
 
 
8
  # ═══════════════════════════════════════════════════════════════════════
9
  SYSTEM_PROMPT = """You are the Martechsol HR Assistant — intelligent, precise, and formal.
10
 
11
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12
+ STEP 1 — UNDERSTAND THE INTENT
13
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14
+ Read the question carefully. Identify the SINGLE core topic being asked. Apply intelligent defaults:
15
+ • "timing" / "timings" (no context) → office working hours ONLY — not payment or any other timing
16
+ • "leaves" / "leave" (no context) leave names + day counts ONLY — NOT leave policies or eligibility
17
+ "paid leaves" / "all leaves" enumerate EVERY leave type with its name and count
18
+ • "salary" / "pay" (no context) → salary structure or amount — NOT payment date unless explicitly asked
19
+ "benefits" / "perks" / "allowances" list EVERY benefit with its name and value
20
+ "terminate" / "termination" resignation/termination procedure NOT general policies
21
+ If a question has an obvious workplace context, always default to the most common interpretation.
22
+
23
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
24
+ STEP 2STRICT SCOPE DISCIPLINE
25
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
26
+ Answer ONLY what was asked. NEVER expand into:
27
+ Policies, approval processes, eligibility rules, or consequences unless user asks for policy/process
28
+ • Related topics the user did not mention
29
+ Broad overviews when a specific fact was requested
30
+ Context that wasn't in the question
31
+
32
+ Scope examples:
33
+ "How many sick leaves?" → ONE number. Not the sick leave policy.
34
+ "What are office timings?" → ONE sentence with time range. Not break times or exceptions.
35
+ "What are paid leaves?" → Complete list of all paid leave types + counts. Not rules for taking them.
36
+ "Tell me about maternity leave" Count + brief key fact. Not full policies unless asked.
37
+
38
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
39
+ STEP 3 CHOOSE THE RIGHT FORMAT
40
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
41
+
42
+ FORMAT A SINGLE FACT (default for most questions)
43
+ When: One direct answer needed (timings, a specific leave count, a single number/date)
44
+ Rule: ONE complete sentence. Maximum 25–30 words. Never cut mid-sentence.
45
+ Example: Office hours are <b>9:00 AM to 6:00 PM</b>, Monday to Saturday.
46
+
47
+ FORMAT B — EXHAUSTIVE LIST
48
+ When: User asks for ALL items in a category ("all leaves", "all benefits", "list all X", "paid leaves")
49
+ Rule:
50
+ • Include EVERY single item found — omitting even one is FORBIDDEN
51
+ • One item per line: <b>Item Name:</b> value<br>
52
+ • No intro sentence, no closing sentence, no extra commentary
53
+ • Continue until ALL items are listed
54
+ Example:
55
+ <b>Casual Leave:</b> 10 days<br>
56
+ <b>Sick Leave:</b> 10 days<br>
57
+ <b>Annual Leave:</b> 14 days<br>
58
+ <b>Maternity Leave:</b> 90 days<br>
59
+ <b>Paternity Leave:</b> 3 days<br>
60
+ <b>Hajj Leave:</b> 30 days<br>
61
+ ...(list every item — do NOT stop early)
62
+
63
+ FORMAT C — BRIEF EXPLANATION
64
+ When: User asks HOW something works, or asks for a process/procedure
65
+ Rule: Maximum 3 bullet points. Each bullet = one complete, factual sentence. No filler words.
66
+
67
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
68
+ STRICT QUALITY RULES
69
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
70
+ ✓ ZERO hallucination — every fact must exist in Expert Data only. No guessing.
71
+ ✓ If not in Expert Data → reply exactly: "I don't have that information."
72
+ ✓ Never cut a sentence mid-way — always complete every sentence fully
73
+ ✓ NEVER mention: "document", "handbook", "manual", "policy file", or any source reference
74
+ ✓ Use <b>bold</b> for names, numbers, dates, leave types, and all key terms
75
+ ✓ Use <br> between list items for clean vertical spacing
76
+ ✓ Tone: formal, warm, and professional — never robotic, never chatty
77
+ ✓ Do NOT add greetings, closings, or "Is there anything else?" type phrases"""
78
+
79
+
80
+ def _build_context(chunks: List[Dict[str, str]], max_words: int = 1500) -> str:
81
  """Combines retrieved chunks into a clean context string, capped at max_words.
82
  Prevents TPM spikes when chunks are unexpectedly large."""
83
  if not chunks:
 
120
 
121
  async def generate_multi_queries(self, query: str, history: List[Dict[str, str]]) -> List[str]:
122
  """Generates multiple search queries to capture broader context from the document."""
123
+ prompt = f"""You are an intelligent HR search optimizer. A user asked: "{query}"
124
+
125
+ STEP 1 UNDERSTAND THE INTENT:
126
+ What is the user ACTUALLY asking about? Apply workplace common sense:
127
+ - "timing" / "timings" (no qualifier) = office working hours, workday schedule
128
+ - "leaves" / "paid leaves" / "all leaves" = all leave types available to employees
129
+ - "salary" / "pay" = salary structure or amount
130
+ - "benefits" / "allowances" = employee benefits and perks
131
+ - "terminate" / "resign" = termination or resignation process
132
+
133
+ STEP 2 — GENERATE 3 TARGETED SEARCH QUERIES:
134
+ Write 3 diverse search queries to retrieve ALL relevant information from an employee handbook.
135
+ For the identified intent, cover synonyms, related terms, and sub-categories:
136
+ - For office timings → search: working hours, office schedule, workday timing, shift hours
137
+ - For leaves → cover ALL types: casual leave, sick leave, annual leave, maternity leave, paternity leave, hajj leave, study leave, earned leave, bereavement leave, unauthorized leave
138
+ - For salary → cover: salary structure, payroll, compensation, increments, deductions
139
+ - For benefits → cover: allowances, perks, medical, bonuses, reimbursements
140
+
141
+ Output ONLY 3 queries, one per line. No numbers, no bullets, no explanations.
142
 
143
  Queries:"""
144
 
app/ui_gradio.py CHANGED
@@ -126,71 +126,66 @@ async def chat_fn(message: str, chat_history: List[Dict[str, str]], session_id:
126
  logger.error(f"Unexpected error: {str(e)}\n{traceback.format_exc()}")
127
  reply = f"⚠️ Oops! Something went wrong."
128
 
129
- # ── Humanistic letter-by-letter typing with occasional typos ──────
130
- displayed = ""
131
-
132
- # We'll process word by word to decide on typos, but type letter by letter
133
- words = reply.split(" ")
134
- for i, word in enumerate(words):
135
- separator = " " if i > 0 else ""
136
-
137
- # Decide if this word gets a typo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  do_typo = (
139
- i > 0
140
- and len(word) > 4
141
- and random.random() < TYPO_CHANCE
142
- and word.isalpha()
143
  )
144
 
145
  if do_typo:
146
- # Pick a random position to make a mistake
147
- typo_pos = random.randint(2, len(word) - 1)
148
-
149
- # 1. Type the word correctly up to the typo position
150
- for char in (separator + word[:typo_pos]):
151
- displayed += char
152
- chat_history[-1] = {"role": "assistant", "content": displayed}
153
- yield gr.update(value="", interactive=False), chat_history
154
- await asyncio.sleep(random.uniform(0.03, 0.07))
155
-
156
- # 2. Type the wrong character
157
- wrong_char = _nearby_char(word[typo_pos])
158
  displayed += wrong_char
159
  chat_history[-1] = {"role": "assistant", "content": displayed}
160
  yield gr.update(value="", interactive=False), chat_history
161
- await asyncio.sleep(random.uniform(0.15, 0.3)) # Pause after error
162
-
163
- # 3. Realize and Backspace
164
- await asyncio.sleep(random.uniform(0.2, 0.4))
165
  displayed = displayed[:-1]
166
  chat_history[-1] = {"role": "assistant", "content": displayed}
167
  yield gr.update(value="", interactive=False), chat_history
168
  await asyncio.sleep(random.uniform(0.1, 0.2))
169
-
170
- # 4. Type the correct character and the rest of the word
171
- for char in word[typo_pos:]:
172
- displayed += char
173
- chat_history[-1] = {"role": "assistant", "content": displayed}
174
- yield gr.update(value="", interactive=False), chat_history
175
- await asyncio.sleep(random.uniform(0.04, 0.08))
 
 
 
 
 
 
176
  else:
177
- # Type the whole word (including separator) letter by letter
178
- for char in (separator + word):
179
- displayed += char
180
- chat_history[-1] = {"role": "assistant", "content": displayed}
181
- yield gr.update(value="", interactive=False), chat_history
182
-
183
- # Faster typing for letters, slight pause for space
184
- if char == " ":
185
- await asyncio.sleep(random.uniform(0.08, 0.15))
186
- else:
187
- await asyncio.sleep(random.uniform(0.02, 0.06))
188
-
189
- # Punctuation pauses (at the end of words)
190
- if word and word[-1] in ".!?":
191
- await asyncio.sleep(random.uniform(0.4, 0.8))
192
- elif word and word[-1] in ",:;":
193
- await asyncio.sleep(random.uniform(0.2, 0.4))
194
 
195
  # ── Fire-and-forget: persist to session store (never blocks the UI) ───
196
  # We only save successful responses, not error messages
@@ -245,8 +240,12 @@ with gr.Blocks(title="Martechsol Assistant", theme=gr.themes.Soft(), css=custom_
245
  gr.Markdown("# Martechsol Assistant")
246
  gr.Markdown("Welcome! How can I help you today?")
247
 
248
- chatbot = gr.Chatbot(show_label=False, elem_id="chatbot-window")
249
-
 
 
 
 
250
  with gr.Row():
251
  msg = gr.Textbox(
252
  placeholder="Type your question here...",
 
126
  logger.error(f"Unexpected error: {str(e)}\n{traceback.format_exc()}")
127
  reply = f"⚠️ Oops! Something went wrong."
128
 
129
+ # ── Humanistic letter-by-letter typing HTML-safe ────────────────────
130
+ displayed = "" # what is shown in the chat bubble
131
+ buffer = "" # invisible accumulator for mid-HTML-tag characters
132
+ in_tag = False # True while we are inside a < > sequence
133
+
134
+ for char in reply:
135
+ if char == '<':
136
+ in_tag = True
137
+ buffer += char
138
+ continue
139
+
140
+ if in_tag:
141
+ buffer += char
142
+ if char == '>':
143
+ # Tag is now complete — flush it to the display all at once
144
+ in_tag = False
145
+ displayed += buffer
146
+ buffer = ""
147
+ chat_history[-1] = {"role": "assistant", "content": displayed}
148
+ yield gr.update(value="", interactive=False), chat_history
149
+ # Small pause after a <br> to mimic line-break pacing
150
+ if displayed.endswith('<br>') or displayed.endswith('<br/>'):
151
+ await asyncio.sleep(random.uniform(0.1, 0.2))
152
+ continue
153
+
154
+ # ── Normal character (not inside a tag) ───────────────────────────
155
+ # Decide typo chance only for plain alphabetic words
156
  do_typo = (
157
+ char.isalpha()
158
+ and len(displayed) > 8 # skip the very beginning
159
+ and random.random() < TYPO_CHANCE
 
160
  )
161
 
162
  if do_typo:
163
+ wrong_char = _nearby_char(char)
 
 
 
 
 
 
 
 
 
 
 
164
  displayed += wrong_char
165
  chat_history[-1] = {"role": "assistant", "content": displayed}
166
  yield gr.update(value="", interactive=False), chat_history
167
+ await asyncio.sleep(random.uniform(0.15, 0.3))
168
+
169
+ # Backspace the wrong character
 
170
  displayed = displayed[:-1]
171
  chat_history[-1] = {"role": "assistant", "content": displayed}
172
  yield gr.update(value="", interactive=False), chat_history
173
  await asyncio.sleep(random.uniform(0.1, 0.2))
174
+
175
+ # Type the correct character
176
+ displayed += char
177
+ chat_history[-1] = {"role": "assistant", "content": displayed}
178
+ yield gr.update(value="", interactive=False), chat_history
179
+
180
+ # Pacing: punctuation pauses, space micro-pause, normal char speed
181
+ if char in '.!?':
182
+ await asyncio.sleep(random.uniform(0.35, 0.7))
183
+ elif char in ',:;':
184
+ await asyncio.sleep(random.uniform(0.15, 0.3))
185
+ elif char == ' ':
186
+ await asyncio.sleep(random.uniform(0.06, 0.13))
187
  else:
188
+ await asyncio.sleep(random.uniform(0.02, 0.06))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
  # ── Fire-and-forget: persist to session store (never blocks the UI) ───
191
  # We only save successful responses, not error messages
 
240
  gr.Markdown("# Martechsol Assistant")
241
  gr.Markdown("Welcome! How can I help you today?")
242
 
243
+ chatbot = gr.Chatbot(
244
+ show_label=False,
245
+ elem_id="chatbot-window",
246
+ type="messages",
247
+ render_markdown=True,
248
+ )
249
  with gr.Row():
250
  msg = gr.Textbox(
251
  placeholder="Type your question here...",
data/index/.gitkeep DELETED
File without changes
data/sessions/.gitkeep DELETED
File without changes
get_models.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import httpx
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ async def main():
9
+ api_key = os.getenv("GROQ_API_KEY")
10
+ if not api_key:
11
+ print("No API Key")
12
+ return
13
+ url = "https://api.groq.com/openai/v1/models"
14
+ headers = {"Authorization": f"Bearer {api_key}"}
15
+ async with httpx.AsyncClient() as client:
16
+ resp = await client.get(url, headers=headers)
17
+ if resp.status_code == 200:
18
+ models = resp.json().get("data", [])
19
+ for m in models:
20
+ if "deepseek" in m["id"].lower() or "qwen" in m["id"].lower():
21
+ print(m["id"])
22
+ else:
23
+ print(resp.status_code, resp.text)
24
+
25
+ asyncio.run(main())
test_groq.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import httpx
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ async def main():
9
+ api_key = os.getenv("GROQ_API_KEY")
10
+ url = "https://api.groq.com/openai/v1/chat/completions"
11
+ headers = {
12
+ "Authorization": f"Bearer {api_key}",
13
+ "Content-Type": "application/json"
14
+ }
15
+ payload = {
16
+ "model": "deepseek-r1-distill-llama-70b",
17
+ "temperature": 0.0,
18
+ "max_tokens": 1200,
19
+ "messages": [{"role": "system", "content": "hello"}, {"role": "user", "content": "test"}]
20
+ }
21
+ async with httpx.AsyncClient() as client:
22
+ resp = await client.post(url, headers=headers, json=payload)
23
+ print(resp.status_code)
24
+ print(resp.text)
25
+
26
+ asyncio.run(main())