From b3b507d2bdf579cfcf7e1735e143e5fe689720e1 Mon Sep 17 00:00:00 2001 From: danrixd Date: Sat, 2 Aug 2025 14:28:39 +0300 Subject: [PATCH 1/3] Add exact DB lookup and integrate with chatbot --- chatbot/response_generator.py | 28 +++++++++++++++++++- db/query_engine.py | 31 ++++++++++++++++++++++ tests/test_exact_lookup.py | 49 +++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/test_exact_lookup.py diff --git a/chatbot/response_generator.py b/chatbot/response_generator.py index 85c4dfc..74cbac2 100644 --- a/chatbot/response_generator.py +++ b/chatbot/response_generator.py @@ -2,10 +2,17 @@ from __future__ import annotations +import re import requests from typing import Iterable, Mapping from ai.rag_pipeline import RAGPipeline +from db.query_engine import exact_lookup + + +def looks_like_exact_lookup(message: str) -> bool: + """Return True if the message contains an ISO date-time pattern.""" + return bool(re.search(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}", message)) class OllamaModel: @@ -38,6 +45,25 @@ def __init__(self, tenant_id: str, model_type: str = "ollama", model_name: str = else: raise ValueError(f"Unsupported model type: {model_type}") - def generate_response(self, user_message: str, history: Iterable[Mapping[str, str]] | None = None) -> str: + def query_rag(self, user_message: str, history: Iterable[Mapping[str, str]] | None = None) -> str: + """Fallback to the RAG pipeline and language model.""" prompt = self.rag.augment_prompt(user_message, history) return self.model.generate(prompt) + + def generate_response(self, user_message: str, history: Iterable[Mapping[str, str]] | None = None) -> str: + """Generate a response handling exact lookup or RAG fallback.""" + if looks_like_exact_lookup(user_message): + date_match = re.search(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}", user_message) + if date_match: + date_str = date_match.group(0) + row = exact_lookup(date_str, self.tenant_id) + if row: + return ( + f"Close value for {date_str} is {row['close']} " + f"(Open: {row['open']}, High: {row['high']}, " + f"Low: {row['low']}, Volume: {row['volume']})" + ) + else: + return f"No data found for {date_str}." + + return self.query_rag(user_message, history) diff --git a/db/query_engine.py b/db/query_engine.py index 2fc703d..deaa74d 100644 --- a/db/query_engine.py +++ b/db/query_engine.py @@ -36,3 +36,34 @@ def execute(self, query: str, params=None): def close(self) -> None: """Close the active connector.""" self.connector.close() + + +def exact_lookup(date_str: str, tenant_id: str, table: str = "market_data") -> dict: + """Retrieve exact row from the tenant's database by date. + + Args: + date_str: Date-time string in ISO format "YYYY-MM-DD HH:MM" + tenant_id: Current tenant identifier + table: Target table name + + Returns: + dict: Matching row or {} if not found + """ + import sqlite3 + + db_path = f"data/{tenant_id}.db" + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + query = f"SELECT * FROM {table} WHERE date = ? LIMIT 1" + cursor.execute(query, (date_str,)) + row = cursor.fetchone() + conn.close() + except Exception: + return {} + + if not row: + return {} + + columns = ["date", "open", "high", "low", "close", "volume"] + return dict(zip(columns, row)) diff --git a/tests/test_exact_lookup.py b/tests/test_exact_lookup.py new file mode 100644 index 0000000..17c99ad --- /dev/null +++ b/tests/test_exact_lookup.py @@ -0,0 +1,49 @@ +import os +import sqlite3 + +from chatbot.response_generator import ResponseGenerator +from db.query_engine import exact_lookup + + +TENANT = "testtenant" +DATE_STR = "2023-07-09 22:01" + + +def setup_module(module): + """Create a temporary SQLite DB for the tenant.""" + os.makedirs("data", exist_ok=True) + db_path = f"data/{TENANT}.db" + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute( + "CREATE TABLE IF NOT EXISTS market_data (date TEXT, open REAL, high REAL, low REAL, close REAL, volume REAL)" + ) + cursor.execute( + "INSERT INTO market_data VALUES (?, ?, ?, ?, ?, ?)", + (DATE_STR, 10.0, 12.0, 9.0, 11.0, 1000.0), + ) + conn.commit() + conn.close() + + +def test_exact_lookup_function(): + row = exact_lookup(DATE_STR, TENANT) + assert row["close"] == 11.0 + + +def test_response_generator_exact_and_rag(): + rg = ResponseGenerator(TENANT) + + def fake_query_rag(self, user_message, history=None): + return "RAG" + + # Replace query_rag to avoid actual model calls + rg.query_rag = fake_query_rag.__get__(rg, ResponseGenerator) + + exact_msg = f"What is the close value for date: {DATE_STR}?" + response = rg.generate_response(exact_msg) + assert "Close value for" in response + + general_msg = "What happened on 2023-07-09?" + assert rg.generate_response(general_msg) == "RAG" + From 7abcb5f8ff3fcf82b8a4737abea43c8d184552c3 Mon Sep 17 00:00:00 2001 From: danrixd Date: Sat, 2 Aug 2025 17:20:11 +0300 Subject: [PATCH 2/3] Seed tenant DB and add check script --- scripts/check_db.py | 25 +++++++++++++++++++++++++ scripts/load_tenant_data.py | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 scripts/check_db.py diff --git a/scripts/check_db.py b/scripts/check_db.py new file mode 100644 index 0000000..9fbab15 --- /dev/null +++ b/scripts/check_db.py @@ -0,0 +1,25 @@ +"""Simple helper to check a tenant's market_data table for a specific date.""" + +import sqlite3 +import sys + + +def main(tenant_id: str, date_to_check: str) -> None: + db_path = f"data/{tenant_id}.db" + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute("SELECT * FROM market_data WHERE date = ?", (date_to_check,)) + row = cursor.fetchone() + conn.close() + if row: + print(row) + else: + print("No data found") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python scripts/check_db.py ") + sys.exit(1) + main(sys.argv[1], sys.argv[2]) + diff --git a/scripts/load_tenant_data.py b/scripts/load_tenant_data.py index 5908f84..a1beac3 100644 --- a/scripts/load_tenant_data.py +++ b/scripts/load_tenant_data.py @@ -1,6 +1,8 @@ -"""Utility script to seed tenant2's vector store with example data.""" +"""Utility script to seed tenant2's data stores with example data.""" from pathlib import Path +import os +import sqlite3 import sys # Ensure the project root is on the Python path so ``ai`` imports work when @@ -9,13 +11,39 @@ from ai.vector_stores.chroma_store import TenantVectorStore -def load_tenant2_data(): + +def load_tenant2_data() -> None: + """Populate tenant2's vector store and SQLite DB with sample data.""" + store = TenantVectorStore("tenant2") - store.add_document("doc1", "Dan middle name is the king69$$$, he is 186 cm tall and he likes banana flavoured ice cream") - store.add_document("doc2", "Bitcoin is down past 115000 usd, showing that distribution started last week around july 25th") + store.add_document( + "doc1", + "Dan middle name is the king69$$$, he is 186 cm tall and he likes banana flavoured ice cream", + ) + store.add_document( + "doc2", + "Bitcoin is down past 115000 usd, showing that distribution started last week around july 25th", + ) print("\u2705 Data loaded for tenant2") + os.makedirs("data", exist_ok=True) + db_path = "data/tenant2.db" + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute( + "CREATE TABLE IF NOT EXISTS market_data (date TEXT, open REAL, high REAL, low REAL, close REAL, volume REAL)" + ) + cursor.execute( + "INSERT OR IGNORE INTO market_data VALUES (?, ?, ?, ?, ?, ?)", + ("2023-07-09 22:01", 10.0, 12.0, 9.0, 11.0, 1000.0), + ) + conn.commit() + conn.close() + print(f"\u2705 market_data table initialized at {db_path}") + + if __name__ == "__main__": load_tenant2_data() + From 6b4206bede8abcb8df7c59897f4cef12140e84a4 Mon Sep 17 00:00:00 2001 From: danrixd <7621928+danrixd@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:23:22 +0300 Subject: [PATCH 3/3] important --- data/tenant2.db | 0 data/testtenant.db | Bin 0 -> 8192 bytes scripts/check_db.py | 20 ++++++++++++++++++ .../length.bin | Bin 40000 -> 40000 bytes vector_store/ctx/chroma.sqlite3 | Bin 163840 -> 172032 bytes vector_store/ol/chroma.sqlite3 | Bin 163840 -> 163840 bytes .../length.bin | Bin 40000 -> 40000 bytes vector_store/rag/chroma.sqlite3 | Bin 172032 -> 176128 bytes .../length.bin | Bin 40000 -> 40000 bytes vector_store/t1/chroma.sqlite3 | Bin 172032 -> 176128 bytes vector_store/tenant2/chroma.sqlite3 | Bin 3956736 -> 3956736 bytes vector_store/testtenant/chroma.sqlite3 | Bin 0 -> 163840 bytes 12 files changed, 20 insertions(+) create mode 100644 data/tenant2.db create mode 100644 data/testtenant.db create mode 100644 scripts/check_db.py create mode 100644 vector_store/testtenant/chroma.sqlite3 diff --git a/data/tenant2.db b/data/tenant2.db new file mode 100644 index 0000000..e69de29 diff --git a/data/testtenant.db b/data/testtenant.db new file mode 100644 index 0000000000000000000000000000000000000000..fb0c9a51017964fb3d2eb42929b71f5ea3505e2a GIT binary patch literal 8192 zcmeI#yKcfT6b9hqfP}b-Zk>>h!~znjLKv!cDy<@xEIkHgl- zr~wab-OGRE%WwHC{x**W@zN-s>2)Rzcj<l4_MDBLz0V&6CNHC35hzbiADN+U0#_cDxn zNyJI-Iga?^f}6HeaT1M_HtP?yV%r$Ed9j=?Tuk-WrMFanDwj7p{mRaV*7B3ZoMk Y_F99q!e|BuYoHN8#T)7%=JbgI09(i&rT_o{ delta 85 zcmX@GgXzEyCcXehi3j%>85kN4eqMTyaiicYF=G}61_rhzml!@UK%`)F0)sVB2?GO+ WX86Ff48&(((B3F`J%6J@pC|zPvm5*X diff --git a/vector_store/ctx/chroma.sqlite3 b/vector_store/ctx/chroma.sqlite3 index b51e3c0180b748ca6bcf768ed2da9d1f79de1364..cb813ed41340b1668f5aa789adb05179f3868caf 100644 GIT binary patch delta 156 zcmZo@;A%L)H9?w{pMilvYodZZBmc&PCHh><{2v+kfAfFi|F~Ju;4wcdGcz*-XY)7v z?ceMfIRhA(mrYgZ4kS_CEm$ ZPyqs_kO6R(U;+fE1b_k%w*(LZ{BORP6;1#E diff --git a/vector_store/ol/chroma.sqlite3 b/vector_store/ol/chroma.sqlite3 index fa09648626fab06d1cbd76371b761a0df3b16b94..aa796a2aaf876632c0d665472a3d1fbef66b9914 100644 GIT binary patch delta 58 zcmV-A0LA}+fC_+s3XmHC3XvQ`0Sd8Tq%RHy59a_6`w#XH@ek||=d%$o)ei*&1p@#H QgX}+t>^}jw>^}k&KqlZ5Jpcdz delta 50 zcmV-20L}k^fC_+s3XmHC36UH_0SU2Sq%RBv59|OB`w#XH@ek~?5kTAzgX}+t>^}jw I>^}k&K&^NZMF0Q* diff --git a/vector_store/rag/1b70bdf9-03f9-42ad-a18e-bcb6e9ae72dc/length.bin b/vector_store/rag/1b70bdf9-03f9-42ad-a18e-bcb6e9ae72dc/length.bin index 26a7ca76b82222d0bba8f16967cb802d9874c828..b5ec4d94c49b62773e2bcc2da1c59196c7ba99a7 100644 GIT binary patch delta 89 zcmX@GgXzEyCcXg1gwkt_3=9t*9JId1xKZ%C)kXzlX=4@!1_l#GVTKP35NQ~lz_1sn VjDZ10GZdT$^BEdmLG<3^0suBh9U%Y! delta 85 zcmX@GgXzEyCcXehi3j%>85kTMY*~7baiicbD`OT028KJ!x*0w&K%`)F0)sVB2?GO+ VW|(jk%x6d_+9;?Vwo&087XT?39V`F< diff --git a/vector_store/rag/chroma.sqlite3 b/vector_store/rag/chroma.sqlite3 index 536d688ff3730bbbba5a11b6a768e7753709c8da..90c8ccab5014ffd75b0a916cba32340be5b7ab5b 100644 GIT binary patch delta 161 zcmZoTz}4`8Yl1YZC<6n7_Cy7HM$wH4OZ53z`JXfJ|K|V3|B?R<|MSg)23Pr6nOT_` zIGf+tZ+~OYC>+4Zx@`g@O92lP|2Lpk25pe)&4vnBn6^J*ViMwI1&V30Z2!&A#5!Tx z1SS?XM$RGz&LW_S3{Hh!ZdPsvV~$3C{uw;dp+*Kqrn&|ex&}rHhK5$g23AJfStc|2 LuuTu(WKseElLIO+ delta 85 zcmV-b0IL6h;0l1i3XmHG5&!@IDv=;R0TQubq%RBv59|OB`w#XH@ek~?5kTAzgX}-I r>^}h$PyqzCkO2g63IPxM00000Di8a!F(B3gx8#BX7Y(=j4*~^{ql6p) diff --git a/vector_store/t1/56dc3db9-3bb6-4e63-9e3f-02e0fce17e67/length.bin b/vector_store/t1/56dc3db9-3bb6-4e63-9e3f-02e0fce17e67/length.bin index cd8ed3be1ec04de980ccd433789b9548c84b9829..96e02fe27f1791e3802c3ca29e793bfebeb0b8c7 100644 GIT binary patch literal 40000 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 af#KH$kbW4=ARu-TY~F(hn;rR{vH<|lZY3W8 diff --git a/vector_store/t1/chroma.sqlite3 b/vector_store/t1/chroma.sqlite3 index e02f92470def7a7495814c9c270c43e691fe0760..dce63895ca27aaaa65ebd4d16410e37129928820 100644 GIT binary patch delta 167 zcmZoTz}4`8Yl1YZ1Oo$u_Cy7HMv09HOY{ZU_#ZLw|K|V3|B?R<|8xFFn*|Nd@pCb= zF*9&7vjU0cH}>1#*fRlTahUU|IIGLQcTTNy%W@BW9OBq>OnHs^R4A}s; CH7)A^ delta 85 zcmV-b0IL6h;0l1i3XmHG5&!@IDv=;R0TQubq%RBv59|OB`w#XH@ek~?5kTAzgX}-I r>^}h$PyqzCkO2g63IPxM00000Di8a!F(AkSx7LCJB@MUx4*~^{qLv%- diff --git a/vector_store/tenant2/chroma.sqlite3 b/vector_store/tenant2/chroma.sqlite3 index 59cc0ef828f116336ffbf512b81882cf562e5c9e..27ecce864af0c19ecccdee8c1461ba7bed2cc8aa 100644 GIT binary patch delta 7199 zcmcIpcU;uRw%=db%F5COQCvWYNM8kIe=}IH#EObW#fYwSslp;gqbrI5BB+46h{Tcr z)@TG}m7T$Eq9N9+CP9rQ8cmF`BzEPUA9(NQz5DLx{`1~Nznn8?+L@X2J+sU%KJ
    9)V$`tMDt4XRP#u4zrp^{6^)AHIf;_v;8(-b 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 delta 6468 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 HACLYUz0v#^ diff --git a/vector_store/testtenant/chroma.sqlite3 b/vector_store/testtenant/chroma.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..7ff3d8f68b4ff367bcde940da959e27646f6704e GIT binary patch literal 163840 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 hg#?fQ5YiE{~s>1Wsd*= literal 0 HcmV?d00001