Spaces:
Running
Running
GitHub Actions commited on
Commit ·
8ccf339
1
Parent(s): 7664072
Deploy 0c3a28c
Browse files- app/pipeline/nodes/generate.py +14 -6
- app/services/vector_store.py +12 -3
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
|
| 43 |
-
"
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 59 |
-
#
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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):
|