GitHub Actions commited on
Commit
8ccf339
·
1 Parent(s): 7664072

Deploy 0c3a28c

Browse files
app/pipeline/nodes/generate.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from typing import Callable
2
 
3
  from app.models.pipeline import PipelineState
@@ -39,8 +40,11 @@ def make_generate_node(llm_client: LLMClient) -> Callable[[PipelineState], dict]
39
  context_block = "\n\n".join(context_parts)
40
 
41
  system_prompt = (
42
- "You are Darshan Chheda's personal AI assistant embedded on his portfolio. "
43
- "Answer questions using ONLY the numbered context passages below. "
 
 
 
44
  "Cite sources inline using bracketed numbers like [1], [2] immediately after each claim. "
45
  "Be concise, confident, and factual. Never invent details not present in the context. "
46
  "If the context doesn't contain enough information to answer fully, say so honestly."
@@ -55,12 +59,16 @@ def make_generate_node(llm_client: LLMClient) -> Callable[[PipelineState], dict]
55
  async for chunk in stream:
56
  full_answer += chunk
57
 
58
- # Always return all source refs used as context the LLM is instructed
59
- # to cite inline as [N], so every chunk in context is a potential citation.
60
- # Filtering by regex is fragile; the frontend renders all sources as footnotes.
 
 
 
 
61
  return {
62
  "answer": full_answer,
63
- "sources": source_refs
64
  }
65
 
66
  return generate_node
 
1
+ import re
2
  from typing import Callable
3
 
4
  from app.models.pipeline import PipelineState
 
40
  context_block = "\n\n".join(context_parts)
41
 
42
  system_prompt = (
43
+ "You are an AI assistant embedded on Darshan Chheda's personal portfolio website. "
44
+ "The numbered context passages below describe Darshan's projects, blog posts, skills, and experiences. "
45
+ "Darshan Chheda is the subject — always refer to him by name or as 'he'/'his'. "
46
+ "When a passage uses 'I', 'my', or 'me', that voice belongs to Darshan. "
47
+ "Answer questions using ONLY the context passages provided. "
48
  "Cite sources inline using bracketed numbers like [1], [2] immediately after each claim. "
49
  "Be concise, confident, and factual. Never invent details not present in the context. "
50
  "If the context doesn't contain enough information to answer fully, say so honestly."
 
59
  async for chunk in stream:
60
  full_answer += chunk
61
 
62
+ # Only surface source refs that the LLM actually cited with [N] markers.
63
+ # Returning all context chunks floods the frontend with irrelevant footnotes.
64
+ cited_indices = {int(m) for m in re.findall(r"\[(\d+)\]", full_answer)}
65
+ cited_sources = [
66
+ sr for i, sr in enumerate(source_refs, start=1) if i in cited_indices
67
+ ]
68
+
69
  return {
70
  "answer": full_answer,
71
+ "sources": cited_sources if cited_sources else source_refs[:2]
72
  }
73
 
74
  return generate_node
app/services/vector_store.py CHANGED
@@ -2,7 +2,7 @@ import uuid
2
  from typing import Optional
3
 
4
  from qdrant_client import QdrantClient
5
- from qdrant_client.models import PointStruct, VectorParams, Distance, Filter, FieldCondition, MatchValue
6
 
7
  from app.models.pipeline import Chunk, ChunkMetadata
8
  from app.core.exceptions import RetrievalError
@@ -14,16 +14,25 @@ class VectorStore:
14
  self.collection = collection
15
 
16
  def ensure_collection(self) -> None:
17
- """Creates collection with vectors size=384, distance=Cosine if it does not exist."""
 
18
  collections = self.client.get_collections().collections
19
  exists = any(c.name == self.collection for c in collections)
20
-
21
  if not exists:
22
  self.client.create_collection(
23
  collection_name=self.collection,
24
  vectors_config=VectorParams(size=384, distance=Distance.COSINE),
25
  )
26
 
 
 
 
 
 
 
 
 
27
  def upsert_chunks(self, chunks: list[Chunk], embeddings: list[list[float]]) -> None:
28
  """Builds PointStruct list and calls client.upsert. Batch size 100."""
29
  if len(chunks) != len(embeddings):
 
2
  from typing import Optional
3
 
4
  from qdrant_client import QdrantClient
5
+ from qdrant_client.models import PointStruct, VectorParams, Distance, Filter, FieldCondition, MatchValue, PayloadSchemaType
6
 
7
  from app.models.pipeline import Chunk, ChunkMetadata
8
  from app.core.exceptions import RetrievalError
 
14
  self.collection = collection
15
 
16
  def ensure_collection(self) -> None:
17
+ """Creates collection with vectors size=384, distance=Cosine if it does not exist.
18
+ Also ensures payload index on metadata.doc_id exists for efficient dedup deletes."""
19
  collections = self.client.get_collections().collections
20
  exists = any(c.name == self.collection for c in collections)
21
+
22
  if not exists:
23
  self.client.create_collection(
24
  collection_name=self.collection,
25
  vectors_config=VectorParams(size=384, distance=Distance.COSINE),
26
  )
27
 
28
+ # Keyword index allows filter-by-doc_id in delete_by_doc_id.
29
+ # create_payload_index is idempotent — safe to call on every startup.
30
+ self.client.create_payload_index(
31
+ collection_name=self.collection,
32
+ field_name="metadata.doc_id",
33
+ field_schema=PayloadSchemaType.KEYWORD,
34
+ )
35
+
36
  def upsert_chunks(self, chunks: list[Chunk], embeddings: list[list[float]]) -> None:
37
  """Builds PointStruct list and calls client.upsert. Batch size 100."""
38
  if len(chunks) != len(embeddings):