InstaFuel_Chatbot_public/run_chat.py
2026-04-21 11:57:37 +05:30

268 lines
9.3 KiB
Python

#!/usr/bin/env python3
"""Interactive CLI for the InstaFuel chatbot using QueryEngine with Chain-of-Thought."""
from __future__ import annotations
import asyncio
import os
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path
def _ensure_src_on_path() -> None:
repo_root = Path(__file__).resolve().parent
src_path = repo_root / "src"
if str(src_path) not in sys.path:
sys.path.insert(0, str(src_path))
_ensure_src_on_path()
try:
from dotenv import load_dotenv
except ImportError:
load_dotenv = None
from database.structured import DuckDBChatStore, normalize_context # noqa: E402
from database.vector import QdrantManager # noqa: E402
from llm.models import Model # noqa: E402
from query_modes.query_engine import QueryEngine # noqa: E402
def _format_price(price: float) -> str:
"""Format price with currency symbol."""
return f"{price:,.0f}" if price >= 200 else f"${price:.2f}"
async def run_chat() -> None:
"""Run the interactive CLI chat using QueryEngine."""
print("\n" + "=" * 70)
print(" 🏋️ InstaFuel Wellness - AI Supplement Advisor 💪")
print("=" * 70)
print("\nInitializing chatbot (loading models and database)...")
chat_store = DuckDBChatStore()
user_id = os.environ.get("CHAT_USER_ID") or f"cli-user-{uuid.uuid4().hex[:8]}"
conversation_id = (
os.environ.get("CHAT_CONVERSATION_ID") or f"cli-{uuid.uuid4().hex[:8]}"
)
try:
# Initialize models
llm = Model(
provider=os.environ["MODEL_PROVIDER"],
model_name=os.environ["MODEL_NAME"],
use_provider_prefix=False,
)
embed_base_url = os.environ.get("EMBED_BASE_URL", "http://127.0.0.1:11434/v1")
embed_api_key = os.environ.get("EMBED_API_KEY", "ollama")
dense = Model(
provider="ollama",
model_name="qwen3-embedding:0.6b",
embed_model=True,
use_provider_prefix=False,
base_url=embed_base_url,
api_key=embed_api_key,
)
sparse = Model(
provider="ollama",
model_name="qwen3-embedding:0.6b",
embed_model=True,
use_provider_prefix=False,
base_url=embed_base_url,
api_key=embed_api_key,
)
# Initialize database
qdrant = QdrantManager()
# Initialize query engine with CoT enabled
query_engine = QueryEngine(
llm=llm, dense=dense, sparse=sparse, qdrant_manager=qdrant
)
print("✓ Models loaded")
print("✓ Database connected")
print("✓ Chain-of-Thought reasoning enabled\n")
except Exception as exc:
print(f"\n❌ ERROR: Failed to initialize chatbot: {exc}")
print("\nPlease check:")
print(" 1. Your .env file has OPENROUTER_API_KEY set")
print(" 2. Qdrant database is running (check QDRANT_URL/QDRANT_HOST)")
print(" 3. Products are indexed in the 'products' collection")
chat_store.close()
return
print("=" * 70)
print("\n💬 Chat Tips:")
print(" • Ask for product recommendations based on your goals")
print(" • Request workout plans with supplement stacks")
print(" • Inquire about specific products or ingredients")
print(" • Type 'quit' or 'exit' to end the conversation\n")
print("=" * 70 + "\n")
print(f"User ID: {user_id}")
print(f"Conversation ID: {conversation_id}")
print(
'Type \':context {"goal": "lean bulk"}\' to store context without sending a prompt.'
)
print("=" * 70 + "\n")
conversation_history = chat_store.get_conversation_history(conversation_id)
user_ctx_str = chat_store.get_user_context_string(conversation_id)
while True:
try:
user_input = input("You: ").strip()
except (EOFError, KeyboardInterrupt):
print("\n")
break
if not user_input:
continue
if user_input.startswith(":context"):
payload = user_input[len(":context") :].strip()
ctx_dict = normalize_context(payload)
if not ctx_dict:
print(
'No context detected. Provide JSON like :context {"goal": "fat loss"}.'
)
continue
timestamp = datetime.now(timezone.utc)
chat_store.record_message(
conversation_id,
role="system",
content=f"Context update: {payload}",
user_id=user_id,
message_ts=timestamp,
metadata={"channel": "cli", "kind": "context"},
context=ctx_dict,
)
user_ctx_str = chat_store.get_user_context_string(conversation_id)
print("✓ Context stored.")
continue
if user_input.lower() in {"quit", "exit", "bye", "goodbye", "q"}:
print("\nAssistant: Thanks for chatting with InstaFuel! 💪")
print("Stay consistent and keep crushing your goals! 🎯\n")
break
# Add user message to history
conversation_history.append({"role": "user", "content": user_input})
chat_store.record_message(
conversation_id,
role="user",
content=user_input,
user_id=user_id,
message_ts=datetime.now(timezone.utc),
metadata={"channel": "cli"},
)
user_ctx_str = chat_store.get_user_context_string(conversation_id)
try:
# Stream the response
response_text = ""
assistant_started = False
event_count = 0
async for event in query_engine.stream_response(
query=user_input,
conv_history=conversation_history.copy(),
user_ctx=user_ctx_str,
):
event_count += 1
event_type = getattr(event, "event_type", None)
if event_type == "product_suggestions":
suggestions = []
if hasattr(event, "data"):
suggestions = event.data.get("suggestions", []) or []
if suggestions:
print("\nSuggested products mentioned:")
for item in suggestions:
name = item.get("name", "Unknown product")
link = item.get("product_url") or ""
image = item.get("image_url") or ""
detail_parts = [part for part in [link, image] if part]
detail_text = (
f" ({' | '.join(detail_parts)})" if detail_parts else ""
)
print(f"{name}{detail_text}")
continue
# Skip START and END events
if event_type in ["response_start", "response_end"]:
continue
if hasattr(event, "data") and "content" in event.data:
chunk = event.data["content"]
if chunk:
# Print "Assistant: " prefix only once
if not assistant_started:
print("\nAssistant: ", end="", flush=True)
assistant_started = True
print(chunk, end="", flush=True)
response_text += chunk
# If no response was generated, show error
if not response_text:
print(
f"\n⚠️ Warning: No response generated (received {event_count} events)\n"
)
if conversation_history and conversation_history[-1]["role"] == "user":
conversation_history.pop()
continue
print("\n") # Add assistant response to history
conversation_history.append({"role": "assistant", "content": response_text})
chat_store.record_message(
conversation_id,
role="assistant",
content=response_text,
user_id=user_id,
message_ts=datetime.now(timezone.utc),
metadata={"channel": "cli"},
)
# Keep conversation history manageable (last 10 messages)
if len(conversation_history) > 10:
conversation_history = conversation_history[-10:]
except Exception as exc:
print(f"\n❌ Error processing request: {exc}\n")
# Remove the failed user message from history
if conversation_history and conversation_history[-1]["role"] == "user":
conversation_history.pop()
chat_store.close()
def main() -> None:
"""Main entry point for the CLI chatbot."""
repo_root = Path(__file__).resolve().parent
env_path = repo_root / ".env"
if not env_path.exists():
print("❌ ERROR: .env file not found!")
print(f"\nExpected location: {env_path}")
print("\nPlease create a .env file with:")
print(" OPENROUTER_API_KEY=your_key_here")
print(" QDRANT_URL=your_qdrant_url")
return
if load_dotenv:
load_dotenv(env_path)
try:
asyncio.run(run_chat())
except KeyboardInterrupt:
print("\n\n👋 Session ended by user.\n")
if __name__ == "__main__":
main()