mirror of
https://github.com/vee1e/InstaFuel_Chatbot_public.git
synced 2026-09-01 10:49:13 +00:00
268 lines
9.3 KiB
Python
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()
|