risu1012 commited on
Commit
b82ddfa
·
1 Parent(s): 0081036

feat: complete BAMS syllabus database ingestion & local LLM config

Browse files
Files changed (6) hide show
  1. Dockerfile +9 -0
  2. app/main.py +58 -28
  3. app/services/engine.py +141 -7
  4. app/services/llm.py +1 -0
  5. parallel_10_results.json +132 -0
  6. start.sh +9 -16
Dockerfile CHANGED
@@ -6,8 +6,17 @@ RUN apt-get update && apt-get install -y \
6
  curl \
7
  git \
8
  build-essential \
 
9
  && rm -rf /var/lib/apt/lists/*
10
 
 
 
 
 
 
 
 
 
11
  WORKDIR /code
12
 
13
  # Copy requirements and install python packages
 
6
  curl \
7
  git \
8
  build-essential \
9
+ cmake \
10
  && rm -rf /var/lib/apt/lists/*
11
 
12
+ # Compile native llama-server binary
13
+ RUN git clone --depth 1 https://github.com/ggml-org/llama.cpp /opt/llama.cpp \
14
+ && cmake -B /opt/llama.cpp/build -S /opt/llama.cpp \
15
+ -DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=OFF \
16
+ && cmake --build /opt/llama.cpp/build --config Release -j "$(nproc)" --target llama-server \
17
+ && install -m 0755 /opt/llama.cpp/build/bin/llama-server /usr/local/bin/llama-server \
18
+ && rm -rf /opt/llama.cpp
19
+
20
  WORKDIR /code
21
 
22
  # Copy requirements and install python packages
app/main.py CHANGED
@@ -11,6 +11,7 @@ if not hasattr(torch.utils._pytree, "register_constant"):
11
  torch.utils._pytree.register_constant = lambda cls: cls
12
 
13
  import json
 
14
  import logging
15
  from contextlib import asynccontextmanager
16
  from typing import AsyncGenerator
@@ -185,40 +186,69 @@ async def _stream_and_persist(
185
  """
186
  Internal generator that streams engine events to SSE AND persists the
187
  final assistant message to the database once the stream completes.
 
188
  """
189
  from app.schemas import Citation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  full_content: list[str] = []
191
  citations_data: list[dict] = []
192
  is_grounded = False
193
 
194
- async for event in engine.stream_answer(domain, query, history):
195
- event_type = event.get("type")
196
-
197
- if event_type == "status":
198
- yield {"data": json.dumps(event)}
199
-
200
- elif event_type == "citations":
201
- citations_data = event.get("citations", [])
202
- is_grounded = event.get("is_grounded", False)
203
- yield {"data": json.dumps(event)}
204
-
205
- elif event_type == "delta":
206
- full_content.append(event.get("content", ""))
207
- yield {"data": json.dumps(event)}
208
-
209
- elif event_type == "done":
210
- # Persist the complete assistant message before emitting done.
211
- # Apply citation expansion so MongoDB stores human-readable text
212
- # (the client already received the expanded tokens via the delta events).
213
- content_str = "".join(full_content)
214
- domain_config = engine._load_domain_config(domain)
215
- content_str = engine.make_citations_readable(content_str, domain_config)
216
- citations = [Citation(**c) for c in citations_data]
217
- session_svc.add_assistant_message(db, session_id, content_str, citations, is_grounded)
218
- yield {"data": json.dumps(event)}
219
-
220
- elif event_type == "error":
221
- yield {"data": json.dumps(event)}
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
 
224
  # ── Helpers ────────────────────────────────────────────────────────────────────
 
11
  torch.utils._pytree.register_constant = lambda cls: cls
12
 
13
  import json
14
+ import asyncio
15
  import logging
16
  from contextlib import asynccontextmanager
17
  from typing import AsyncGenerator
 
186
  """
187
  Internal generator that streams engine events to SSE AND persists the
188
  final assistant message to the database once the stream completes.
189
+ Sends keep-alive heartbeats if the stream is idle (e.g. waiting for llama-server).
190
  """
191
  from app.schemas import Citation
192
+
193
+ queue = asyncio.Queue()
194
+ done_sentinel = object()
195
+
196
+ async def producer():
197
+ try:
198
+ async for event in engine.stream_answer(domain, query, history):
199
+ await queue.put(event)
200
+ except Exception as e:
201
+ logger.exception("Error in stream_answer producer:")
202
+ await queue.put({"type": "error", "error": str(e)})
203
+ finally:
204
+ await queue.put(done_sentinel)
205
+
206
+ producer_task = asyncio.create_task(producer())
207
+
208
  full_content: list[str] = []
209
  citations_data: list[dict] = []
210
  is_grounded = False
211
 
212
+ try:
213
+ while True:
214
+ try:
215
+ # 5.0 second timeout to send heartbeats during queuing or prompt evaluation
216
+ event = await asyncio.wait_for(queue.get(), timeout=5.0)
217
+ if event is done_sentinel:
218
+ break
219
+
220
+ event_type = event.get("type")
221
+ if event_type == "status":
222
+ yield {"data": json.dumps(event)}
223
+ elif event_type == "citations":
224
+ citations_data = event.get("citations", [])
225
+ is_grounded = event.get("is_grounded", False)
226
+ yield {"data": json.dumps(event)}
227
+ elif event_type == "delta":
228
+ full_content.append(event.get("content", ""))
229
+ yield {"data": json.dumps(event)}
230
+ elif event_type == "done":
231
+ # Persist the complete assistant message before emitting done.
232
+ content_str = "".join(full_content)
233
+ if content_str:
234
+ domain_config = engine._load_domain_config(domain)
235
+ content_str = engine.make_citations_readable(content_str, domain_config)
236
+ citations = [Citation(**c) for c in citations_data]
237
+ session_svc.add_assistant_message(db, session_id, content_str, citations, is_grounded)
238
+ yield {"data": json.dumps(event)}
239
+ elif event_type == "error":
240
+ yield {"data": json.dumps(event)}
241
+
242
+ except asyncio.TimeoutError:
243
+ # Keep proxy connections alive
244
+ yield {"data": json.dumps({"type": "heartbeat"})}
245
+ finally:
246
+ if not producer_task.done():
247
+ producer_task.cancel()
248
+ try:
249
+ await producer_task
250
+ except asyncio.CancelledError:
251
+ pass
252
 
253
 
254
  # ── Helpers ────────────────────────────────────────────────────────────────────
app/services/engine.py CHANGED
@@ -287,6 +287,23 @@ def make_citations_readable(text: str, domain_config: dict) -> str:
287
  return text
288
 
289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  def _build_prompt(
291
  domain_config: dict,
292
  query: str,
@@ -487,6 +504,25 @@ async def stream_answer(
487
  yield {"type": "done"}
488
  return
489
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  # Query condensation DISABLED on CPU deployment (saves a full LLM round trip).
491
  # ── Step 2: Tier 1 — Pre-emptive grounding gate ──────────────────────────────
492
  chunks = await asyncio.to_thread(
@@ -654,7 +690,8 @@ async def stream_answer(
654
 
655
  # Build prompt (history is trimmed + deduplicated inside _build_prompt)
656
  trimmed_history = history[-(history_turns * 2):]
657
- messages = _build_prompt(config, query, chunks, trimmed_history)
 
658
 
659
  # ── Step 4: Tier 2 — Stream buffer for OUT_OF_SYLLABUS detection ────────────
660
  # We do NOT emit citations before we know the model is grounded.
@@ -673,7 +710,7 @@ async def stream_answer(
673
  if grounded:
674
  yield_data = {
675
  "type": "citations",
676
- "citations": [c.to_citation().model_dump() for c in chunks],
677
  "is_grounded": True,
678
  }
679
  else:
@@ -695,6 +732,7 @@ async def stream_answer(
695
  max_tokens=llm_cfg.get("max_tokens"),
696
  )
697
 
 
698
  tokens_yielded = 0
699
  async for token in raw_stream:
700
  tokens_yielded += 1
@@ -728,16 +766,18 @@ async def stream_answer(
728
  citation_emitted = True
729
  yield {
730
  "type": "citations",
731
- "citations": [c.to_citation().model_dump() for c in chunks],
732
  "is_grounded": True,
733
  }
734
  # Flush buffer
735
  expanded = make_citations_readable(combined, config)
 
736
  yield {"type": "delta", "content": expanded}
737
  # Still accumulating buffer — don't yield yet
738
  else:
739
  # Post-buffer: emit tokens with citation expansion applied
740
  expanded = make_citations_readable(token, config)
 
741
  yield {"type": "delta", "content": expanded}
742
 
743
  # Edge case: stream ended while we were still buffering (very short response)
@@ -758,10 +798,11 @@ async def stream_answer(
758
  citation_emitted = True
759
  yield {
760
  "type": "citations",
761
- "citations": [c.to_citation().model_dump() for c in chunks],
762
  "is_grounded": True,
763
  }
764
  expanded = make_citations_readable(combined, config)
 
765
  yield {"type": "delta", "content": expanded}
766
 
767
  # Empty stream fallback: if we got absolutely no tokens from the LLM,
@@ -773,13 +814,13 @@ async def stream_answer(
773
  citation_emitted = True
774
  yield {
775
  "type": "citations",
776
- "citations": [c.to_citation().model_dump() for c in chunks],
777
  "is_grounded": True,
778
  }
779
 
780
- # Format fallback content from chunks
781
  context_parts = []
782
- for chunk in chunks:
783
  context_parts.append(chunk.content)
784
  context_text = "\n\n".join(context_parts).strip()
785
 
@@ -793,6 +834,28 @@ async def stream_answer(
793
  yield {"type": "delta", "content": word + " "}
794
  await asyncio.sleep(0.01)
795
  yield {"type": "delta", "content": "\n\n"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796
  async for event in _stream_with_buffer():
797
  yield event
798
 
@@ -802,4 +865,75 @@ async def stream_answer(
802
  yield {"type": "error", "error": str(e)}
803
 
804
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
805
 
 
287
  return text
288
 
289
 
290
+ def estimate_tokens(text: str) -> int:
291
+ # Heuristic for Qwen/llama token counts (~4 chars/token)
292
+ return max(1, len(text) // 4)
293
+
294
+
295
+ def condense_context(chunks: list[rag.RetrievedChunk], max_context_tokens: int = 700) -> tuple[list[rag.RetrievedChunk], int]:
296
+ selected = []
297
+ running = 0
298
+ for chunk in chunks:
299
+ cost = estimate_tokens(chunk.content)
300
+ if running + cost > max_context_tokens:
301
+ break
302
+ selected.append(chunk)
303
+ running += cost
304
+ return selected, running
305
+
306
+
307
  def _build_prompt(
308
  domain_config: dict,
309
  query: str,
 
504
  yield {"type": "done"}
505
  return
506
 
507
+ # Compute query embeddings for cache and search
508
+ embedding_fn = rag._get_embedding_fn()
509
+ query_embeddings = await asyncio.to_thread(embedding_fn, [query])
510
+
511
+ # ── Step 1.5: Semantic Cache Check ──────────────────────────────────────────
512
+ cached_val = await check_semantic_cache(query_embeddings, f"answer_cache_{domain}")
513
+ if cached_val:
514
+ logger.info("Semantic cache HIT for query: %s", query[:80])
515
+ engine_mode = "hybrid" if is_online else "fallback"
516
+ yield {"type": "status", "engine_mode": engine_mode}
517
+ yield {
518
+ "type": "citations",
519
+ "citations": cached_val["citations"],
520
+ "is_grounded": cached_val["is_grounded"]
521
+ }
522
+ yield {"type": "delta", "content": cached_val["answer"]}
523
+ yield {"type": "done"}
524
+ return
525
+
526
  # Query condensation DISABLED on CPU deployment (saves a full LLM round trip).
527
  # ── Step 2: Tier 1 — Pre-emptive grounding gate ──────────────────────────────
528
  chunks = await asyncio.to_thread(
 
690
 
691
  # Build prompt (history is trimmed + deduplicated inside _build_prompt)
692
  trimmed_history = history[-(history_turns * 2):]
693
+ condensed_chunks, _ = condense_context(chunks, max_context_tokens=700)
694
+ messages = _build_prompt(config, query, condensed_chunks, trimmed_history)
695
 
696
  # ── Step 4: Tier 2 — Stream buffer for OUT_OF_SYLLABUS detection ────────────
697
  # We do NOT emit citations before we know the model is grounded.
 
710
  if grounded:
711
  yield_data = {
712
  "type": "citations",
713
+ "citations": [c.to_citation().model_dump() for c in condensed_chunks],
714
  "is_grounded": True,
715
  }
716
  else:
 
732
  max_tokens=llm_cfg.get("max_tokens"),
733
  )
734
 
735
+ full_response_text = []
736
  tokens_yielded = 0
737
  async for token in raw_stream:
738
  tokens_yielded += 1
 
766
  citation_emitted = True
767
  yield {
768
  "type": "citations",
769
+ "citations": [c.to_citation().model_dump() for c in condensed_chunks],
770
  "is_grounded": True,
771
  }
772
  # Flush buffer
773
  expanded = make_citations_readable(combined, config)
774
+ full_response_text.append(expanded)
775
  yield {"type": "delta", "content": expanded}
776
  # Still accumulating buffer — don't yield yet
777
  else:
778
  # Post-buffer: emit tokens with citation expansion applied
779
  expanded = make_citations_readable(token, config)
780
+ full_response_text.append(expanded)
781
  yield {"type": "delta", "content": expanded}
782
 
783
  # Edge case: stream ended while we were still buffering (very short response)
 
798
  citation_emitted = True
799
  yield {
800
  "type": "citations",
801
+ "citations": [c.to_citation().model_dump() for c in condensed_chunks],
802
  "is_grounded": True,
803
  }
804
  expanded = make_citations_readable(combined, config)
805
+ full_response_text.append(expanded)
806
  yield {"type": "delta", "content": expanded}
807
 
808
  # Empty stream fallback: if we got absolutely no tokens from the LLM,
 
814
  citation_emitted = True
815
  yield {
816
  "type": "citations",
817
+ "citations": [c.to_citation().model_dump() for c in condensed_chunks],
818
  "is_grounded": True,
819
  }
820
 
821
+ # Format fallback content from condensed_chunks
822
  context_parts = []
823
+ for chunk in condensed_chunks:
824
  context_parts.append(chunk.content)
825
  context_text = "\n\n".join(context_parts).strip()
826
 
 
834
  yield {"type": "delta", "content": word + " "}
835
  await asyncio.sleep(0.01)
836
  yield {"type": "delta", "content": "\n\n"}
837
+
838
+ # Cache the fallback response
839
+ await write_semantic_cache(
840
+ query,
841
+ query_embeddings,
842
+ intro + context_text,
843
+ [c.to_citation().model_dump() for c in condensed_chunks],
844
+ True,
845
+ f"answer_cache_{domain}"
846
+ )
847
+ elif is_grounded_confirmed:
848
+ # Cache the generated LLM response
849
+ final_text = "".join(full_response_text)
850
+ await write_semantic_cache(
851
+ query,
852
+ query_embeddings,
853
+ final_text,
854
+ [c.to_citation().model_dump() for c in condensed_chunks],
855
+ True,
856
+ f"answer_cache_{domain}"
857
+ )
858
+
859
  async for event in _stream_with_buffer():
860
  yield event
861
 
 
865
  yield {"type": "error", "error": str(e)}
866
 
867
 
868
+ ANSWER_CACHE_COLLECTION = "answer_cache"
869
+ SIMILARITY_THRESHOLD = 0.92
870
+
871
+
872
+ async def check_semantic_cache(query_embeddings: list[list[float]], collection_name: str = ANSWER_CACHE_COLLECTION) -> dict | None:
873
+ try:
874
+ from app.services.rag import get_or_create_collection
875
+ collection = get_or_create_collection(collection_name)
876
+
877
+ # Query the cache collection using the query embeddings
878
+ results = await asyncio.to_thread(
879
+ collection.query,
880
+ query_embeddings=query_embeddings,
881
+ n_results=1,
882
+ include=["documents", "metadatas", "distances"]
883
+ )
884
+ if not results["distances"] or not results["distances"][0]:
885
+ return None
886
+
887
+ distance = results["distances"][0][0]
888
+ similarity = 1.0 - distance
889
+
890
+ if similarity >= SIMILARITY_THRESHOLD:
891
+ meta = results["metadatas"][0][0]
892
+ citations_raw = meta.get("citations", "[]")
893
+ try:
894
+ citations = json.loads(citations_raw)
895
+ except Exception:
896
+ citations = []
897
+ return {
898
+ "answer": meta.get("answer", ""),
899
+ "citations": citations,
900
+ "is_grounded": meta.get("is_grounded", True)
901
+ }
902
+ except Exception as e:
903
+ logger.warning("Semantic cache lookup failed: %s", e)
904
+ return None
905
+
906
+
907
+ async def write_semantic_cache(
908
+ query: str,
909
+ query_embeddings: list[list[float]],
910
+ answer: str,
911
+ citations: list[dict],
912
+ is_grounded: bool = True,
913
+ collection_name: str = ANSWER_CACHE_COLLECTION
914
+ ) -> None:
915
+ try:
916
+ from app.services.rag import get_or_create_collection
917
+ collection = get_or_create_collection(collection_name)
918
+
919
+ doc_id = hashlib.sha256(query.encode("utf-8")).hexdigest()
920
+
921
+ # Write/Update the cache record
922
+ await asyncio.to_thread(
923
+ collection.upsert,
924
+ ids=[doc_id],
925
+ embeddings=query_embeddings,
926
+ documents=[query],
927
+ metadatas=[{
928
+ "answer": answer,
929
+ "citations": json.dumps(citations),
930
+ "is_grounded": is_grounded,
931
+ "query": query
932
+ }]
933
+ )
934
+ logger.info("Saved response to semantic cache for query: %s", query[:50])
935
+ except Exception as e:
936
+ logger.warning("Failed to write to semantic cache: %s", e)
937
+
938
+
939
 
app/services/llm.py CHANGED
@@ -68,6 +68,7 @@ async def stream_completion(
68
  stop=["<|im_end|>", "<|im_start|>", "--- STUDENT QUESTION ---", "--- SYLLABUS CONTEXT ---"],
69
  # Frequency penalty prevents the model from getting stuck in repetition loops (e.g. repeating emojis)
70
  frequency_penalty=0.1,
 
71
  )
72
  async for chunk in stream:
73
  delta = chunk.choices[0].delta.content
 
68
  stop=["<|im_end|>", "<|im_start|>", "--- STUDENT QUESTION ---", "--- SYLLABUS CONTEXT ---"],
69
  # Frequency penalty prevents the model from getting stuck in repetition loops (e.g. repeating emojis)
70
  frequency_penalty=0.1,
71
+ extra_body={"cache_prompt": True},
72
  )
73
  async for chunk in stream:
74
  delta = chunk.choices[0].delta.content
parallel_10_results.json ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "user_id": 1,
4
+ "question": "What is Rasa Dhatu?",
5
+ "duration_s": 88.5884416103363,
6
+ "first_token_time_s": 84.02139806747437,
7
+ "words": 305,
8
+ "grounded": true,
9
+ "citations": 3,
10
+ "heartbeats": 0,
11
+ "cache_hit": false,
12
+ "status": "Success",
13
+ "error": ""
14
+ },
15
+ {
16
+ "user_id": 7,
17
+ "question": "What is Shukra Dhatu?",
18
+ "duration_s": 117.67143845558167,
19
+ "first_token_time_s": null,
20
+ "words": 0,
21
+ "grounded": false,
22
+ "citations": 0,
23
+ "heartbeats": 0,
24
+ "cache_hit": false,
25
+ "status": "Failed (Too Short)",
26
+ "error": ""
27
+ },
28
+ {
29
+ "user_id": 6,
30
+ "question": "What is Rasa Dhatu?",
31
+ "duration_s": 118.67596745491028,
32
+ "first_token_time_s": null,
33
+ "words": 0,
34
+ "grounded": false,
35
+ "citations": 0,
36
+ "heartbeats": 0,
37
+ "cache_hit": false,
38
+ "status": "Failed (Too Short)",
39
+ "error": ""
40
+ },
41
+ {
42
+ "user_id": 4,
43
+ "question": "Explain Kedara-Kulya Nyaya.",
44
+ "duration_s": 120.67894673347473,
45
+ "first_token_time_s": null,
46
+ "words": 0,
47
+ "grounded": false,
48
+ "citations": 0,
49
+ "heartbeats": 0,
50
+ "cache_hit": false,
51
+ "status": "Failed (Too Short)",
52
+ "error": ""
53
+ },
54
+ {
55
+ "user_id": 2,
56
+ "question": "What is Shukra Dhatu?",
57
+ "duration_s": 122.68142056465149,
58
+ "first_token_time_s": null,
59
+ "words": 0,
60
+ "grounded": false,
61
+ "citations": 0,
62
+ "heartbeats": 0,
63
+ "cache_hit": false,
64
+ "status": "Failed (Too Short)",
65
+ "error": ""
66
+ },
67
+ {
68
+ "user_id": 8,
69
+ "question": "Explain Ksheera-Dadhi Nyaya.",
70
+ "duration_s": 116.67163467407227,
71
+ "first_token_time_s": null,
72
+ "words": 0,
73
+ "grounded": false,
74
+ "citations": 0,
75
+ "heartbeats": 0,
76
+ "cache_hit": false,
77
+ "status": "Failed (Too Short)",
78
+ "error": ""
79
+ },
80
+ {
81
+ "user_id": 9,
82
+ "question": "What is the function of Rasa Dhatu?",
83
+ "duration_s": 115.66793322563171,
84
+ "first_token_time_s": null,
85
+ "words": 0,
86
+ "grounded": false,
87
+ "citations": 0,
88
+ "heartbeats": 0,
89
+ "cache_hit": false,
90
+ "status": "Failed (Too Short)",
91
+ "error": ""
92
+ },
93
+ {
94
+ "user_id": 5,
95
+ "question": "Explain Khale-Kapotanya Nyaya.",
96
+ "duration_s": 119.67767238616943,
97
+ "first_token_time_s": null,
98
+ "words": 0,
99
+ "grounded": false,
100
+ "citations": 0,
101
+ "heartbeats": 0,
102
+ "cache_hit": false,
103
+ "status": "Failed (Too Short)",
104
+ "error": ""
105
+ },
106
+ {
107
+ "user_id": 10,
108
+ "question": "What are the theories of tissue nutrition in Ayurveda?",
109
+ "duration_s": 114.66833090782166,
110
+ "first_token_time_s": null,
111
+ "words": 0,
112
+ "grounded": false,
113
+ "citations": 0,
114
+ "heartbeats": 0,
115
+ "cache_hit": false,
116
+ "status": "Failed (Too Short)",
117
+ "error": ""
118
+ },
119
+ {
120
+ "user_id": 3,
121
+ "question": "Explain Ksheera-Dadhi Nyaya.",
122
+ "duration_s": 121.68023824691772,
123
+ "first_token_time_s": null,
124
+ "words": 0,
125
+ "grounded": false,
126
+ "citations": 0,
127
+ "heartbeats": 0,
128
+ "cache_hit": false,
129
+ "status": "Failed (Too Short)",
130
+ "error": ""
131
+ }
132
+ ]
start.sh CHANGED
@@ -18,17 +18,18 @@ else
18
  echo "Model already exists at $MODEL_PATH"
19
  fi
20
 
21
- # 2. Start llama-cpp-python server in the background (runs on CPU, port 8001)
22
- echo "Starting local llama-cpp-python server on port 8001..."
23
- python -m llama_cpp.server \
24
  --model "$MODEL_PATH" \
25
  --port 8001 \
26
  --host 127.0.0.1 \
27
- --n_ctx 4096 \
28
- --n_threads 2 \
29
- --n_threads_batch 2 \
30
- --n_batch 256 \
31
- --chat_format chatml &
 
32
 
33
  # 3. Wait for the local llama.cpp server to be ready
34
  echo "Waiting for llama.cpp server to initialize..."
@@ -45,14 +46,6 @@ export MODEL_NAME="qwen2.5-3b-instruct-q4_k_m.gguf"
45
  export LLM_MODEL_NAME="qwen2.5-3b-instruct-q4_k_m.gguf"
46
  export LLM_MAX_TOKENS=2048
47
 
48
-
49
- # 4.5. Extract pre-built ChromaDB index from zip if present
50
- if [ -f "chroma_store.zip" ]; then
51
- echo "Extracting pre-built ChromaDB index from zip..."
52
- python -c "import zipfile; zipfile.ZipFile('chroma_store.zip').extractall('chroma_store')"
53
- echo "Extraction complete."
54
- fi
55
-
56
  # 5. Run ingest only if the collection is empty or new files exist
57
  echo "Checking knowledge base..."
58
  CHROMA_COUNT=$(python -c "
 
18
  echo "Model already exists at $MODEL_PATH"
19
  fi
20
 
21
+ # 2. Start native llama-server in the background (runs on CPU, port 8001)
22
+ echo "Starting native llama-server on port 8001..."
23
+ llama-server \
24
  --model "$MODEL_PATH" \
25
  --port 8001 \
26
  --host 127.0.0.1 \
27
+ --ctx-size 6144 \
28
+ --parallel 3 \
29
+ --cont-batching \
30
+ --threads 2 \
31
+ --threads-batch 2 &
32
+
33
 
34
  # 3. Wait for the local llama.cpp server to be ready
35
  echo "Waiting for llama.cpp server to initialize..."
 
46
  export LLM_MODEL_NAME="qwen2.5-3b-instruct-q4_k_m.gguf"
47
  export LLM_MAX_TOKENS=2048
48
 
 
 
 
 
 
 
 
 
49
  # 5. Run ingest only if the collection is empty or new files exist
50
  echo "Checking knowledge base..."
51
  CHROMA_COUNT=$(python -c "