Spaces:
Sleeping
Sleeping
github-actions[bot]
commited on
Commit
Β·
158badf
1
Parent(s):
7055a93
Deploy from GitHub Actions 2025-12-11_03:14:30
Browse files
app.py
CHANGED
|
@@ -12,6 +12,7 @@ SUPABASE_ANON_KEY = os.environ.get("SUPABASE_ANON_KEY")
|
|
| 12 |
EMBEDDING_MODEL = os.environ.get("EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
|
| 13 |
LLM_MODEL = os.environ.get("LLM_MODEL", "HuggingFaceH4/zephyr-7b-beta")
|
| 14 |
RESULTS_K = int(os.environ.get("RESULTS_K", 5))
|
|
|
|
| 15 |
|
| 16 |
# -------- VALIDATE ----------
|
| 17 |
if not HF_API_TOKEN or not SUPABASE_URL or not SUPABASE_ANON_KEY:
|
|
@@ -27,10 +28,12 @@ SYSTEM_PROMPT = """You are an SAP documentation assistant. Your job is to answer
|
|
| 27 |
|
| 28 |
STRICT RULES:
|
| 29 |
1. ONLY use information from the provided context to answer
|
| 30 |
-
2. If the context doesn't contain enough information to answer, say "I don't have enough information in my knowledge base to answer this question."
|
| 31 |
3. DO NOT use any prior knowledge - only the provided documents
|
| 32 |
4. Always be helpful and format your answers clearly
|
| 33 |
5. If relevant, mention which source document the information came from
|
|
|
|
|
|
|
| 34 |
|
| 35 |
Remember: You are grounded to the provided context only. Do not make up information."""
|
| 36 |
|
|
@@ -67,6 +70,15 @@ def search_supabase(query_vector: List[float], k: int = RESULTS_K):
|
|
| 67 |
return resp.data or []
|
| 68 |
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
def format_context(chunks: List[dict]) -> str:
|
| 71 |
"""
|
| 72 |
Format retrieved chunks into a context string for the LLM.
|
|
@@ -75,7 +87,9 @@ def format_context(chunks: List[dict]) -> str:
|
|
| 75 |
for i, chunk in enumerate(chunks, 1):
|
| 76 |
title = chunk.get("title", "Unknown")
|
| 77 |
content = chunk.get("content", "")
|
| 78 |
-
|
|
|
|
|
|
|
| 79 |
return "\n---\n".join(context_parts)
|
| 80 |
|
| 81 |
|
|
@@ -90,7 +104,7 @@ def generate_answer(question: str, context: str) -> str:
|
|
| 90 |
|
| 91 |
Question: {question}
|
| 92 |
|
| 93 |
-
Please answer the question based ONLY on the context documents provided above."""
|
| 94 |
|
| 95 |
messages = [
|
| 96 |
{"role": "system", "content": SYSTEM_PROMPT},
|
|
@@ -126,7 +140,7 @@ for message in st.session_state.messages:
|
|
| 126 |
if message.get("sources"):
|
| 127 |
with st.expander("π View Sources"):
|
| 128 |
for source in message["sources"]:
|
| 129 |
-
st.markdown(f"**{source['title']}** (similarity: {source['similarity']:.
|
| 130 |
st.caption(source['content'][:500] + "..." if len(source['content']) > 500 else source['content'])
|
| 131 |
st.divider()
|
| 132 |
|
|
@@ -147,16 +161,33 @@ if question := st.chat_input("Ask a question about SAP..."):
|
|
| 147 |
query_vector = compute_embedding(question)
|
| 148 |
|
| 149 |
# Step 2: Search Supabase for relevant chunks
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
if not chunks:
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
sources = []
|
| 155 |
else:
|
| 156 |
-
# Step
|
| 157 |
context = format_context(chunks)
|
| 158 |
|
| 159 |
-
# Step
|
| 160 |
with st.spinner("π€ Generating answer..."):
|
| 161 |
answer = generate_answer(question, context)
|
| 162 |
|
|
@@ -165,7 +196,8 @@ if question := st.chat_input("Ask a question about SAP..."):
|
|
| 165 |
{
|
| 166 |
"title": chunk.get("title", "Unknown"),
|
| 167 |
"content": chunk.get("content", ""),
|
| 168 |
-
"similarity": chunk.get("similarity", 0.0)
|
|
|
|
| 169 |
}
|
| 170 |
for chunk in chunks
|
| 171 |
]
|
|
@@ -175,10 +207,18 @@ if question := st.chat_input("Ask a question about SAP..."):
|
|
| 175 |
|
| 176 |
# Display sources
|
| 177 |
if sources:
|
| 178 |
-
with st.expander("π View Sources"):
|
| 179 |
for source in sources:
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
st.divider()
|
| 183 |
|
| 184 |
# Add to history
|
|
@@ -205,17 +245,29 @@ with st.sidebar:
|
|
| 205 |
|
| 206 |
1. π **Search**: Your question is converted to embeddings and matched against our SAP knowledge base
|
| 207 |
2. π **Retrieve**: The most relevant document chunks are retrieved from Supabase
|
| 208 |
-
3.
|
|
|
|
| 209 |
|
| 210 |
This ensures answers are grounded in real documentation, not hallucinated!
|
| 211 |
""")
|
| 212 |
|
| 213 |
st.divider()
|
| 214 |
|
| 215 |
-
st.header("βοΈ
|
| 216 |
-
st.caption(f"Embedding Model
|
| 217 |
-
st.caption(f"LLM Model
|
| 218 |
-
st.caption(f"Results per query
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
st.divider()
|
| 221 |
|
|
|
|
| 12 |
EMBEDDING_MODEL = os.environ.get("EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
|
| 13 |
LLM_MODEL = os.environ.get("LLM_MODEL", "HuggingFaceH4/zephyr-7b-beta")
|
| 14 |
RESULTS_K = int(os.environ.get("RESULTS_K", 5))
|
| 15 |
+
SIMILARITY_THRESHOLD = float(os.environ.get("SIMILARITY_THRESHOLD", 0.35)) # Minimum similarity score
|
| 16 |
|
| 17 |
# -------- VALIDATE ----------
|
| 18 |
if not HF_API_TOKEN or not SUPABASE_URL or not SUPABASE_ANON_KEY:
|
|
|
|
| 28 |
|
| 29 |
STRICT RULES:
|
| 30 |
1. ONLY use information from the provided context to answer
|
| 31 |
+
2. If the context doesn't contain enough information to answer, say "I don't have enough information in my knowledge base to answer this question. Please try asking about a different SAP topic or rephrase your question."
|
| 32 |
3. DO NOT use any prior knowledge - only the provided documents
|
| 33 |
4. Always be helpful and format your answers clearly
|
| 34 |
5. If relevant, mention which source document the information came from
|
| 35 |
+
6. For SAP transaction codes, explain what they do and when to use them
|
| 36 |
+
7. Keep answers concise but comprehensive
|
| 37 |
|
| 38 |
Remember: You are grounded to the provided context only. Do not make up information."""
|
| 39 |
|
|
|
|
| 70 |
return resp.data or []
|
| 71 |
|
| 72 |
|
| 73 |
+
def filter_by_similarity(chunks: List[dict], threshold: float = SIMILARITY_THRESHOLD) -> List[dict]:
|
| 74 |
+
"""
|
| 75 |
+
Filter chunks by minimum similarity threshold.
|
| 76 |
+
Only return chunks with similarity >= threshold.
|
| 77 |
+
"""
|
| 78 |
+
filtered = [c for c in chunks if c.get("similarity", 0) >= threshold]
|
| 79 |
+
return filtered
|
| 80 |
+
|
| 81 |
+
|
| 82 |
def format_context(chunks: List[dict]) -> str:
|
| 83 |
"""
|
| 84 |
Format retrieved chunks into a context string for the LLM.
|
|
|
|
| 87 |
for i, chunk in enumerate(chunks, 1):
|
| 88 |
title = chunk.get("title", "Unknown")
|
| 89 |
content = chunk.get("content", "")
|
| 90 |
+
similarity = chunk.get("similarity", 0)
|
| 91 |
+
source = chunk.get("source", "unknown")
|
| 92 |
+
context_parts.append(f"[Document {i}: {title}]\nSource: {source}\nRelevance: {similarity:.2%}\n\n{content}\n")
|
| 93 |
return "\n---\n".join(context_parts)
|
| 94 |
|
| 95 |
|
|
|
|
| 104 |
|
| 105 |
Question: {question}
|
| 106 |
|
| 107 |
+
Please answer the question based ONLY on the context documents provided above. If the documents don't contain relevant information, say so clearly."""
|
| 108 |
|
| 109 |
messages = [
|
| 110 |
{"role": "system", "content": SYSTEM_PROMPT},
|
|
|
|
| 140 |
if message.get("sources"):
|
| 141 |
with st.expander("π View Sources"):
|
| 142 |
for source in message["sources"]:
|
| 143 |
+
st.markdown(f"**{source['title']}** (similarity: {source['similarity']:.2%})")
|
| 144 |
st.caption(source['content'][:500] + "..." if len(source['content']) > 500 else source['content'])
|
| 145 |
st.divider()
|
| 146 |
|
|
|
|
| 161 |
query_vector = compute_embedding(question)
|
| 162 |
|
| 163 |
# Step 2: Search Supabase for relevant chunks
|
| 164 |
+
all_chunks = search_supabase(query_vector, RESULTS_K)
|
| 165 |
+
|
| 166 |
+
# Step 3: Filter by similarity threshold
|
| 167 |
+
chunks = filter_by_similarity(all_chunks, SIMILARITY_THRESHOLD)
|
| 168 |
|
| 169 |
if not chunks:
|
| 170 |
+
# Check if we got results but they were all below threshold
|
| 171 |
+
if all_chunks:
|
| 172 |
+
best_score = max(c.get("similarity", 0) for c in all_chunks)
|
| 173 |
+
answer = f"""I couldn't find sufficiently relevant information in my knowledge base for your question.
|
| 174 |
+
|
| 175 |
+
**What I found:** The best matching documents had only {best_score:.1%} relevance, which is below my confidence threshold of {SIMILARITY_THRESHOLD:.0%}.
|
| 176 |
+
|
| 177 |
+
**Suggestions:**
|
| 178 |
+
- Try rephrasing your question with different keywords
|
| 179 |
+
- Ask about a specific SAP topic like "SAP Basis administration", "SAP authorization", or "SAP HANA"
|
| 180 |
+
- Check if you're asking about a very specific transaction code - my knowledge base may not cover all of them yet
|
| 181 |
+
|
| 182 |
+
Would you like to try a different question?"""
|
| 183 |
+
else:
|
| 184 |
+
answer = "I couldn't find any relevant documents in my knowledge base for your question. Please try asking about a different SAP topic."
|
| 185 |
sources = []
|
| 186 |
else:
|
| 187 |
+
# Step 4: Format context from retrieved chunks
|
| 188 |
context = format_context(chunks)
|
| 189 |
|
| 190 |
+
# Step 5: Generate answer using LLM
|
| 191 |
with st.spinner("π€ Generating answer..."):
|
| 192 |
answer = generate_answer(question, context)
|
| 193 |
|
|
|
|
| 196 |
{
|
| 197 |
"title": chunk.get("title", "Unknown"),
|
| 198 |
"content": chunk.get("content", ""),
|
| 199 |
+
"similarity": chunk.get("similarity", 0.0),
|
| 200 |
+
"source": chunk.get("source", "unknown")
|
| 201 |
}
|
| 202 |
for chunk in chunks
|
| 203 |
]
|
|
|
|
| 207 |
|
| 208 |
# Display sources
|
| 209 |
if sources:
|
| 210 |
+
with st.expander(f"π View Sources ({len(sources)} relevant documents)"):
|
| 211 |
for source in sources:
|
| 212 |
+
sim_pct = source['similarity'] * 100
|
| 213 |
+
if sim_pct >= 70:
|
| 214 |
+
badge = "π’"
|
| 215 |
+
elif sim_pct >= 50:
|
| 216 |
+
badge = "π‘"
|
| 217 |
+
else:
|
| 218 |
+
badge = "π "
|
| 219 |
+
st.markdown(f"{badge} **{source['title']}** ({source['similarity']:.1%} match)")
|
| 220 |
+
st.caption(f"Source: {source.get('source', 'unknown')}")
|
| 221 |
+
st.text(source['content'][:600] + "..." if len(source['content']) > 600 else source['content'])
|
| 222 |
st.divider()
|
| 223 |
|
| 224 |
# Add to history
|
|
|
|
| 245 |
|
| 246 |
1. π **Search**: Your question is converted to embeddings and matched against our SAP knowledge base
|
| 247 |
2. π **Retrieve**: The most relevant document chunks are retrieved from Supabase
|
| 248 |
+
3. π― **Filter**: Only documents above the similarity threshold are used
|
| 249 |
+
4. π€ **Generate**: An LLM generates an answer based *only* on the retrieved documents
|
| 250 |
|
| 251 |
This ensures answers are grounded in real documentation, not hallucinated!
|
| 252 |
""")
|
| 253 |
|
| 254 |
st.divider()
|
| 255 |
|
| 256 |
+
st.header("βοΈ Configuration")
|
| 257 |
+
st.caption(f"**Embedding Model:** `{EMBEDDING_MODEL}`")
|
| 258 |
+
st.caption(f"**LLM Model:** `{LLM_MODEL}`")
|
| 259 |
+
st.caption(f"**Results per query:** `{RESULTS_K}`")
|
| 260 |
+
st.caption(f"**Similarity threshold:** `{SIMILARITY_THRESHOLD:.0%}`")
|
| 261 |
+
|
| 262 |
+
st.divider()
|
| 263 |
+
|
| 264 |
+
st.header("π‘ Tips")
|
| 265 |
+
st.markdown("""
|
| 266 |
+
- Ask specific questions about SAP topics
|
| 267 |
+
- Try questions about SAP Basis, HANA, Security, etc.
|
| 268 |
+
- Mention transaction codes (SM50, SU01, etc.)
|
| 269 |
+
- Check the sources to verify answers
|
| 270 |
+
""")
|
| 271 |
|
| 272 |
st.divider()
|
| 273 |
|