Skip to content

Commit c53baf6

Browse files
committed
feat: new updating
1 parent 5f1558a commit c53baf6

8 files changed

Lines changed: 351 additions & 275 deletions

File tree

‎app/streamlit_app.py‎

Lines changed: 184 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,199 @@
1+
import os
2+
from typing import Any, Dict, List
3+
14
import requests
25
import streamlit as st
36

4-
API_URL = "http://127.0.0.1:8000/query"
7+
# --------------------------------------------------
8+
# Configuration
9+
# --------------------------------------------------
10+
11+
API_URL = os.getenv("LEGAL_API_URL", "http://127.0.0.1:8000/query")
12+
REQUEST_TIMEOUT = 120
13+
TOP_K = 5
14+
15+
session = requests.Session()
16+
17+
18+
# --------------------------------------------------
19+
# Page Setup
20+
# --------------------------------------------------
21+
22+
st.set_page_config(
23+
page_title="Ethiopian Legal Assistant", page_icon="⚖️", layout="centered"
24+
)
25+
26+
st.title("⚖️ Ethiopian Legal Assistant")
27+
st.caption("Ask questions about Ethiopian law")
28+
29+
st.markdown(
30+
"""
31+
<style>
32+
.block-container {
33+
max-width: 800px;
34+
}
35+
</style>
36+
""",
37+
unsafe_allow_html=True,
38+
)
39+
40+
# --------------------------------------------------
41+
# Session State
42+
# --------------------------------------------------
43+
44+
if "messages" not in st.session_state:
45+
st.session_state.messages = []
46+
47+
48+
# --------------------------------------------------
49+
# Sidebar
50+
# --------------------------------------------------
51+
52+
with st.sidebar:
53+
54+
st.header("Settings")
55+
56+
top_k = st.slider("Number of legal sources", min_value=1, max_value=10, value=TOP_K)
557

6-
st.title("⚖️ የኢትዮጵያ ሕግ አጋዥ(ነጋሪት ጋዜጣ)")
58+
if st.button("Clear Chat"):
59+
st.session_state.messages = []
60+
st.rerun()
761

8-
query = st.text_input("የሕግ ጥያቄዎን ያስገቡ")
962

10-
if st.button("ፈልግ") and query:
63+
# --------------------------------------------------
64+
# API Client
65+
# --------------------------------------------------
1166

12-
payload = {"query": query, "top_k": 5}
67+
68+
def fetch_legal_results(query: str, top_k: int) -> List[Dict[str, Any]]:
69+
70+
payload = {"query": query, "top_k": top_k}
1371

1472
try:
15-
response = requests.post(API_URL, json=payload)
1673

17-
if response.status_code == 200:
18-
results = response.json()
74+
response = session.post(API_URL, json=payload, timeout=REQUEST_TIMEOUT)
75+
76+
response.raise_for_status()
77+
78+
data = response.json()
79+
80+
if not isinstance(data, list):
81+
raise RuntimeError("Invalid API response format")
82+
83+
return data
84+
85+
except requests.exceptions.Timeout:
86+
raise RuntimeError("⏱️ The legal search service timed out.")
87+
88+
except requests.exceptions.ConnectionError:
89+
raise RuntimeError("🚨 Cannot connect to the legal API.")
90+
91+
except requests.exceptions.HTTPError as exc:
92+
raise RuntimeError(f"❌ API error {exc.response.status_code}")
93+
94+
except ValueError:
95+
raise RuntimeError("⚠️ Invalid response from API.")
96+
97+
98+
# --------------------------------------------------
99+
# Format Answer
100+
# --------------------------------------------------
101+
102+
103+
def build_answer(results: List[Dict[str, Any]]) -> str:
104+
105+
if not results:
106+
return "⚠️ ይቅርታ፣ ከጥያቄዎ ጋር የሚዛመድ ህግ አልተገኘም።"
107+
108+
results = sorted(results, key=lambda x: x.get("relevance_score", 0), reverse=True)
109+
110+
answer = []
111+
112+
for doc in results:
113+
114+
content = doc.get("content", "").strip()
115+
meta = doc.get("metadata", {}) or {}
116+
117+
article = meta.get("article", "—")
118+
law_name = meta.get("law_name", "Unknown Law")
119+
page = meta.get("page_number", "—")
120+
121+
score = doc.get("relevance_score", 0)
122+
score_pct = f"{score*100:.1f}%"
123+
124+
block = f"""
125+
### 📜 {law_name}
126+
**አንቀጽ:** {article}
127+
**ገጽ:** {page}
128+
**Relevance:** {score_pct}
129+
{content}
130+
---
131+
"""
132+
133+
answer.append(block)
134+
135+
return "\n".join(answer)
136+
137+
138+
# --------------------------------------------------
139+
# Display Chat History
140+
# --------------------------------------------------
141+
142+
for message in st.session_state.messages:
143+
144+
with st.chat_message(message["role"]):
145+
st.markdown(message["content"])
146+
147+
148+
# --------------------------------------------------
149+
# User Input
150+
# --------------------------------------------------
151+
152+
prompt = st.chat_input("Ask a question about Ethiopian law...")
153+
154+
if prompt:
155+
156+
st.session_state.messages.append({"role": "user", "content": prompt})
157+
158+
with st.chat_message("user"):
159+
st.markdown(prompt)
160+
161+
with st.chat_message("assistant"):
162+
163+
with st.spinner("Searching Ethiopian law..."):
164+
165+
try:
166+
167+
results = fetch_legal_results(prompt, top_k)
168+
169+
answer = build_answer(results)
170+
171+
placeholder = st.empty()
172+
173+
# simulate streaming response
174+
displayed = ""
175+
for chunk in answer.split("\n"):
176+
displayed += chunk + "\n"
177+
placeholder.markdown(displayed)
178+
179+
st.session_state.messages.append(
180+
{"role": "assistant", "content": answer}
181+
)
182+
183+
except RuntimeError as err:
184+
185+
st.error(str(err))
19186

20-
if not results:
21-
st.warning("ውጤት አልተገኘም")
22-
else:
23-
st.success(f"{len(results)} ውጤቶች ተገኝተዋል")
187+
st.session_state.messages.append(
188+
{"role": "assistant", "content": str(err)}
189+
)
24190

25-
for i, doc in enumerate(results, 1):
26-
st.subheader(f"ውጤት {i}")
27-
st.write(doc["content"])
191+
except Exception:
28192

29-
with st.expander("መረጃ (Metadata)"):
30-
st.json(doc["metadata"])
193+
error_msg = "⚠️ Unexpected system error."
31194

32-
else:
33-
st.error(f"የAPI ስህተት: {response.text}")
195+
st.error(error_msg)
34196

35-
except requests.exceptions.RequestException as e:
36-
st.error(f"የግንኙነት ስህተት: {e}")
197+
st.session_state.messages.append(
198+
{"role": "assistant", "content": error_msg}
199+
)

‎setup.py‎

Lines changed: 0 additions & 7 deletions
This file was deleted.

‎src/api/rag_api.py‎

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from typing import List, Optional
44

5+
import numpy as np # Added for type checking
56
from fastapi import FastAPI, HTTPException
67
from pydantic import BaseModel
78

@@ -10,13 +11,12 @@
1011

1112
logger = get_logger("rag_api")
1213

13-
app = FastAPI(title="Legal RAG API", version="1.0.0")
14+
app = FastAPI(title="Ethiopian Legal RAG API", version="1.1.0")
1415

15-
# Initialize retriever once
1616
try:
17-
retriever = LegalRetriever()
18-
except FileNotFoundError as e:
19-
logger.error("Vector store missing: %s", e)
17+
retriever = LegalRetriever(top_k=5)
18+
except Exception as e:
19+
logger.error("Retriever initialization failed: %s", e)
2020
retriever = None
2121

2222

@@ -29,27 +29,57 @@ class QueryRequest(BaseModel):
2929
class QueryResponse(BaseModel):
3030
content: str
3131
metadata: dict
32+
relevance_score: Optional[float] = None
3233

3334

3435
@app.post("/query", response_model=List[QueryResponse])
35-
def query_legal_docs(request: QueryRequest):
36-
"""
37-
Query the legal vector database and return relevant chunks.
38-
"""
39-
36+
async def query_legal_docs(request: QueryRequest):
4037
if retriever is None:
41-
raise HTTPException(status_code=500, detail="Vector store not initialized")
42-
43-
results = retriever.retrieve(
44-
query=request.query,
45-
k=request.top_k,
46-
metadata_filter=request.metadata_filter,
47-
)
48-
49-
response = [
50-
QueryResponse(content=doc.page_content, metadata=doc.metadata)
51-
for doc in results
52-
]
53-
54-
logger.info("Returned %d results for query '%s'", len(results), request.query)
55-
return response
38+
raise HTTPException(
39+
status_code=500, detail="Legal Engine is offline. Check vector store path."
40+
)
41+
42+
try:
43+
logger.info("Processing legal query: '%s'", request.query)
44+
45+
# This calls your Reranked retrieval logic
46+
results = retriever.retrieve(query=request.query)
47+
48+
if not results:
49+
return []
50+
51+
response = []
52+
for doc in results:
53+
# 1. Extract the raw score
54+
raw_score = doc.metadata.get("relevance_score")
55+
56+
# 2. FIX: Convert NumPy types to Python native types
57+
# Flashrank often returns np.float32 which Pydantic can't serialize
58+
clean_score = float(raw_score) if raw_score is not None else None
59+
60+
# 3. Double-check metadata for any other numpy types just in case
61+
safe_metadata = {
62+
k: (v.item() if isinstance(v, np.generic) else v)
63+
for k, v in doc.metadata.items()
64+
}
65+
66+
response.append(
67+
QueryResponse(
68+
content=doc.page_content,
69+
metadata=safe_metadata,
70+
relevance_score=clean_score,
71+
)
72+
)
73+
74+
return response
75+
76+
except Exception as e:
77+
logger.error("API Retrieval Error: %s", str(e))
78+
raise HTTPException(
79+
status_code=500, detail=f"Internal processing error: {str(e)}"
80+
)
81+
82+
83+
@app.get("/health")
84+
def health_check():
85+
return {"status": "ready" if retriever else "error"}

‎src/embeddings/vector_builder.py‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,27 @@ def get_embedding_model() -> HuggingFaceEmbeddings:
2323

2424

2525
def build_vector_store(chunks: List[Document]) -> None:
26-
"""
27-
Build and persist a FAISS vector store.
28-
29-
Args:
30-
chunks: List of document chunks to embed.
31-
"""
32-
3326
if not chunks:
34-
logger.warning("No chunks provided to build vector store")
27+
logger.warning("No chunks provided")
3528
return
3629

30+
# 1. Clean chunks before embedding
31+
# Sometimes OCR adds weird characters like \x0c (page breaks)
32+
# that confuse the embedding model.
33+
for chunk in chunks:
34+
chunk.page_content = chunk.page_content.replace("\x0c", " ")
35+
36+
# Ensure 'law_name' exists so FAISS doesn't crash on nulls
37+
if "law_name" not in chunk.metadata:
38+
chunk.metadata["law_name"] = "Unknown Proclamation"
39+
3740
embedding_model = get_embedding_model()
3841

39-
logger.info("Creating FAISS vector store...")
42+
logger.info("Creating FAISS index for %d chunks...", len(chunks))
43+
44+
# 2. Use 'from_documents' which handles metadata automatically
4045
vector_db = FAISS.from_documents(chunks, embedding_model)
4146

4247
VECTOR_STORE_DIR.mkdir(parents=True, exist_ok=True)
4348
vector_db.save_local(str(VECTOR_STORE_DIR))
44-
45-
logger.info("Vector store successfully saved to %s", VECTOR_STORE_DIR)
49+
logger.info("Vector store successfully saved.")

0 commit comments

Comments
 (0)