Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "4f658b25",
"metadata": {},
"source": [
"# 🛒 Smart Shopping Assistant\n",
"1. An AI agent where users paste a product description.\n",
"2. Estimates a fair price using a fine-tuned pricing model.\n",
"3. Finds real listings via a web search tool.\n",
"Compares listing prices with the estimated price.\n",
"4. Returns a verdict: **Overpriced**, **Fair**, or **A Bargain**."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "b78e7cb6",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import re\n",
"import json\n",
"from typing import List, Optional\n",
"\n",
"import gradio as gr\n",
"from dotenv import load_dotenv\n",
"from openai import OpenAI\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_community.tools import DuckDuckGoSearchRun\n",
"from langchain_core.tools import tool\n",
"from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage, BaseMessage"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "3af032c1",
"metadata": {},
"outputs": [],
"source": [
"load_dotenv(override=True)\n",
"\n",
"FINE_TUNED_MODEL = \"ft:gpt-4.1-nano-2025-04-14:personal:pricer:DFJJ866N\"\n",
"AGENT_MODEL = \"claude-haiku-4-5\"\n",
"\n",
"openai_client = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n",
"llm = ChatAnthropic(model=AGENT_MODEL, temperature=0.0, max_tokens=1800)\n",
"web_search_tool = DuckDuckGoSearchRun()"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "bd7d6b4d",
"metadata": {},
"outputs": [],
"source": [
"@tool\n",
"def estimate_price(product_description: str) -> str:\n",
" \"\"\"Estimate the fair market price of a product using the fine-tuned pricing model.\n",
" Pass the full product description or title and details.\"\"\"\n",
" response = openai_client.chat.completions.create(\n",
" model=FINE_TUNED_MODEL,\n",
" messages=[\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": (\n",
" \"Estimate the price of this product. Respond with the price, no explanation\\n\\n\"\n",
" + product_description\n",
" ),\n",
" }\n",
" ],\n",
" max_tokens=20,\n",
" temperature=0.0,\n",
" )\n",
" return response.choices[0].message.content.strip()\n",
"\n",
"\n",
"@tool\n",
"def web_search(query: str) -> str:\n",
" \"\"\"Search the web for real product listings, current prices, and availability.\"\"\"\n",
" return web_search_tool.run(query)\n",
"\n",
"\n",
"@tool\n",
"def compare_prices(estimated_price: float, listing_price: float) -> str:\n",
" \"\"\"Compare the model's estimated fair price against a real listing price and return a deal verdict.\n",
" Both prices should be plain numbers in USD (e.g. 49.99).\"\"\"\n",
" if listing_price <= 0 or estimated_price <= 0:\n",
" return \"Unable to compare: one or both prices are invalid.\"\n",
"\n",
" ratio = listing_price / estimated_price\n",
" diff = listing_price - estimated_price\n",
" direction = \"above\" if diff > 0 else \"below\"\n",
" pct = abs((ratio - 1) * 100)\n",
"\n",
" if ratio <= 0.80:\n",
" verdict = \"Great deal! The listing price is significantly below fair market value.\"\n",
" elif ratio <= 0.95:\n",
" verdict = \"Good deal. The listing price is slightly below fair market value.\"\n",
" elif ratio <= 1.05:\n",
" verdict = \"Fair price. The listing matches the estimated market value.\"\n",
" elif ratio <= 1.20:\n",
" verdict = \"Slightly overpriced. You may find it cheaper elsewhere.\"\n",
" else:\n",
" verdict = \"Overpriced. Consider looking for a better deal.\"\n",
"\n",
" return (\n",
" f\"Estimated fair price: ${estimated_price:.2f}\\n\"\n",
" f\"Listing price: ${listing_price:.2f}\\n\"\n",
" f\"Difference: ${abs(diff):.2f} ({pct:.1f}% {direction} estimate)\\n\"\n",
" f\"Verdict: {verdict}\"\n",
" )\n",
"\n",
"\n",
"tools = [estimate_price, web_search, compare_prices]\n",
"agent_llm = llm.bind_tools(tools)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "ca68dfbf",
"metadata": {},
"outputs": [],
"source": [
"SYSTEM_PROMPT = \"\"\"\n",
"You are a Smart Shopping Assistant. Your job is to help users decide if a product is worth buying at a given price.\n",
"\n",
"When a user describes a product or asks about a deal, follow these steps in order:\n",
"1. Call `estimate_price` with the product description to get the fair market value.\n",
"2. Call `web_search` to find real current listings and prices for that product.\n",
"3. Extract a specific listing price from the web search results.\n",
"4. Call `compare_prices` with the estimated price and the listing price (both as plain numbers).\n",
"5. Summarise your findings clearly: the estimated value, what real listings show, and a deal recommendation.\n",
"\n",
"If the user just asks a general shopping question (not about a specific product), answer directly without calling tools.\n",
"Be concise, helpful, and friendly.\n",
"\"\"\"\n",
"\n",
"\n",
"def handle_tool_call(tool_call: dict) -> ToolMessage:\n",
" \"\"\"Execute one tool call and return a LangChain ToolMessage.\"\"\"\n",
" tool_name = tool_call.get(\"name\", \"\")\n",
" tool_args = tool_call.get(\"args\", {})\n",
" tool_call_id = tool_call.get(\"id\", \"\")\n",
"\n",
" if tool_name == \"estimate_price\":\n",
" content = estimate_price.invoke(tool_args)\n",
" elif tool_name == \"web_search\":\n",
" content = web_search.invoke(tool_args)\n",
" elif tool_name == \"compare_prices\":\n",
" content = compare_prices.invoke(tool_args)\n",
" else:\n",
" content = f\"Unknown tool: {tool_name}\"\n",
"\n",
" return ToolMessage(content=str(content), tool_call_id=tool_call_id)\n",
"\n",
"\n",
"def convert_history_to_messages(history: List[dict]) -> List[BaseMessage]:\n",
" \"\"\"Convert Gradio chat history into LangChain message objects.\"\"\"\n",
" messages: List[BaseMessage] = []\n",
" for item in history:\n",
" role = item.get(\"role\", \"\")\n",
" content = str(item.get(\"content\", \"\"))\n",
" if role == \"user\":\n",
" messages.append(HumanMessage(content=content))\n",
" elif role == \"assistant\":\n",
" messages.append(AIMessage(content=content))\n",
" return messages\n",
"\n",
"\n",
"def run_agent(user_text: str, history: Optional[List[dict]] = None) -> str:\n",
" \"\"\"Run the tool-calling agent loop and return the final assistant response.\"\"\"\n",
" if history is None:\n",
" history = []\n",
"\n",
" messages: List[BaseMessage] = [SystemMessage(content=SYSTEM_PROMPT.strip())]\n",
" messages.extend(convert_history_to_messages(history))\n",
" messages.append(HumanMessage(content=user_text))\n",
"\n",
" for _ in range(6):\n",
" ai_message = agent_llm.invoke(messages)\n",
" messages.append(ai_message)\n",
"\n",
" if not ai_message.tool_calls:\n",
" return (\n",
" ai_message.content\n",
" if isinstance(ai_message.content, str)\n",
" else str(ai_message.content)\n",
" )\n",
"\n",
" for tool_call in ai_message.tool_calls:\n",
" messages.append(handle_tool_call(tool_call))\n",
"\n",
" return \"I could not complete the request after several steps. Please try rephrasing.\""
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "b8082ac9",
"metadata": {},
"outputs": [],
"source": [
"def chat_handler(user_text: str, history: List[dict]):\n",
" \"\"\"Handle a chat turn: run the agent and update history.\"\"\"\n",
" if history is None:\n",
" history = []\n",
"\n",
" user_text = (user_text or \"\").strip()\n",
" if not user_text:\n",
" return history, \"\"\n",
"\n",
" response = run_agent(user_text, history)\n",
" updated_history = history + [\n",
" {\"role\": \"user\", \"content\": user_text},\n",
" {\"role\": \"assistant\", \"content\": response},\n",
" ]\n",
" return updated_history, \"\"\n",
"\n",
"\n",
"def build_gradio_app() -> gr.Blocks:\n",
" \"\"\"Build the Gradio UI for the Smart Shopping Assistant.\"\"\"\n",
" with gr.Blocks(title=\"Smart Shopping Assistant\") as demo:\n",
" gr.Markdown(\"# Smart Shopping Assistant\")\n",
" gr.Markdown(\n",
" \"Describe a product you're considering buying. \"\n",
" \"The assistant will estimate its fair market value, search for real listings, \"\n",
" \"and tell you if it's a good deal.\"\n",
" )\n",
" gr.Markdown(\n",
" \"**Example:** *Is a Sony WH-1000XM5 for $280 a good deal?* \"\n",
" \"or *What's a fair price for an Apple Watch Series 9?*\"\n",
" )\n",
"\n",
" chatbot = gr.Chatbot(label=\"Shopping Assistant\", height=450)\n",
" user_input = gr.Textbox(\n",
" label=\"Your question\",\n",
" placeholder=\"Describe a product or ask about a price...\",\n",
" )\n",
" send_btn = gr.Button(\"Send\", variant=\"primary\")\n",
"\n",
" send_btn.click(\n",
" fn=chat_handler,\n",
" inputs=[user_input, chatbot],\n",
" outputs=[chatbot, user_input],\n",
" )\n",
" user_input.submit(\n",
" fn=chat_handler,\n",
" inputs=[user_input, chatbot],\n",
" outputs=[chatbot, user_input],\n",
" )\n",
"\n",
" return demo\n",
"\n",
"\n",
"demo_app = build_gradio_app()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "912addb9",
"metadata": {},
"outputs": [],
"source": [
"demo_app.launch(share=True)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "llms2",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}