Skip to content
Draft
Show file tree
Hide file tree
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
89 changes: 89 additions & 0 deletions backend/agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,74 @@ def parse_search_products_markdown(text: str, limit: int = 6) -> list[dict]:
return products


def parse_tracking_result(text: str) -> dict:
raw = text or ""
result = {
"order_number": "",
"status": "",
"recipient": "",
"estimated_delivery": "",
"location": "",
"items": [],
"events": [],
"raw": raw,
}

if not raw:
return result

try:
data = json.loads(raw) if isinstance(raw, str) else raw
except json.JSONDecodeError:
return result

if not isinstance(data, dict):
return result

result["order_number"] = str(
data.get("order_number")
or data.get("orderNumber")
or data.get("id")
or ""
).strip()
result["status"] = str(data.get("status") or data.get("order_status") or "").strip()
result["recipient"] = str(data.get("recipient_name") or data.get("recipient") or "").strip()
result["estimated_delivery"] = str(
data.get("estimated_delivery") or data.get("delivery_date") or data.get("estimatedDelivery") or ""
).strip()
result["location"] = str(data.get("current_location") or data.get("location") or "").strip()

items = data.get("items") or data.get("products") or data.get("cart") or []
if isinstance(items, list):
for item in items[:8]:
if isinstance(item, dict):
name = str(item.get("name") or item.get("title") or item.get("product_name") or "").strip()
quantity_raw = item.get("quantity")
quantity = None
if quantity_raw not in (None, ""):
try:
quantity = _coerce_int(quantity_raw)
except (TypeError, ValueError):
quantity = None
if name:
result["items"].append({"name": name, "quantity": quantity})
elif isinstance(item, str) and item.strip():
result["items"].append({"name": item.strip(), "quantity": None})

events = data.get("timeline") or data.get("history") or data.get("events") or []
if isinstance(events, list):
for event in events[:8]:
if not isinstance(event, dict):
continue
label = str(event.get("status") or event.get("event") or event.get("description") or "").strip()
time = str(event.get("timestamp") or event.get("date") or event.get("time") or "").strip()
location = str(event.get("location") or "").strip()
if label:
result["events"].append({"label": label, "time": time, "location": location})

return result


def build_tool_result_event(tool_name: str, result: Any) -> dict[str, Any]:
event: dict[str, Any] = {"result": result}
if tool_name == "search_products" and isinstance(result, str):
Expand All @@ -296,6 +364,11 @@ def build_tool_result_event(tool_name: str, result: Any) -> dict[str, Any]:
if product.get("name") or product.get("product_id") or product.get("product_url"):
event["product"] = product
event["raw"] = result
elif tool_name == "track_order" and isinstance(result, str):
tracking = parse_tracking_result(result)
if tracking.get("order_number") or tracking.get("status") or tracking.get("events"):
event["tracking"] = tracking
event["raw"] = result
return event


Expand Down Expand Up @@ -364,6 +437,10 @@ async def _check_delivery(city: str, delivery_date: str, product_id: str, **_) -
})


async def _track_order(order_number: str, **_) -> Any:
return await mcp_client.call_tool("kapruka_track_order", {"order_number": order_number})


async def _add_to_cart(session_id: str, product_id: str, quantity: int = 1, **_) -> str:
raw = await mcp_client.call_tool("kapruka_get_product", {"product_id": product_id})

Expand Down Expand Up @@ -568,6 +645,17 @@ async def _checkout(session_id: str, **_) -> str:
"required": ["city", "delivery_date", "product_id"],
},
},
{
"name": "track_order",
"description": "Track a Kapruka order by order number and return the latest status and timeline.",
"parameters": {
"type": "object",
"properties": {
"order_number": {"type": "string", "description": "Kapruka order number"},
},
"required": ["order_number"],
},
},
{
"name": "add_to_cart",
"description": "Add a product to the user's shopping cart. Use this when user wants to add an item.",
Expand Down Expand Up @@ -666,6 +754,7 @@ async def _checkout(session_id: str, **_) -> str:
"list_categories": _list_categories,
"list_delivery_cities": _list_delivery_cities,
"check_delivery": _check_delivery,
"track_order": _track_order,
"add_to_cart": _add_to_cart,
"remove_from_cart": _remove_from_cart,
"update_cart_quantity": _update_cart_quantity,
Expand Down
24 changes: 24 additions & 0 deletions backend/routes/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import logging
import re
import uuid

from fastapi import APIRouter
Expand All @@ -21,6 +22,23 @@ class ChatRequest(BaseModel):
history: list[dict] | None = None


def _needs_order_number_prompt(message_text: str) -> bool:
normalized = message_text.lower()
asks_to_track = any(
phrase in normalized
for phrase in (
"track order",
"track my order",
"track package",
"order status",
"where is my order",
"delivery status",
)
)
has_order_number = bool(re.search(r"\b\d{5,}\b", message_text))
return asks_to_track and not has_order_number


def _provider_fallback_order(message_text: str) -> list[str]:
preferred = resolve_provider_config(message_text).provider
ordered: list[str] = []
Expand Down Expand Up @@ -58,6 +76,12 @@ async def chat_endpoint(req: ChatRequest):
provider_config = resolve_provider_config(req.message)

async def event_stream():
if _needs_order_number_prompt(req.message):
yield f"data: {json.dumps({'type': 'session_id', 'session_id': session_id, 'provider': 'local', 'model': 'order-tracking-guard'})}\n\n"
yield f"data: {json.dumps({'type': 'text', 'text': 'Please share your Kapruka order number and I will track the latest status for you.'})}\n\n"
yield 'data: [DONE]\n\n'
return

candidates = _provider_fallback_order(req.message)
if not candidates:
yield (
Expand Down
16 changes: 16 additions & 0 deletions backend/tests/test_chat_route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import unittest

from routes.chat import _needs_order_number_prompt


class ChatRouteTest(unittest.TestCase):
def test_prompts_for_order_number_when_tracking_without_one(self):
self.assertTrue(_needs_order_number_prompt("I want to track my order"))
self.assertTrue(_needs_order_number_prompt("Can you check my order status?"))

def test_skips_prompt_when_order_number_is_present(self):
self.assertFalse(_needs_order_number_prompt("Track order 12345678"))


if __name__ == "__main__":
unittest.main()
27 changes: 26 additions & 1 deletion backend/tests/test_tool_arg_sanitizer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest
from unittest.mock import AsyncMock, patch

from agent.tools import _search_products, build_tool_result_event, coerce_tool_args, format_tool_result_for_model, parse_failed_tool_generation, parse_product_markdown, parse_search_products_markdown
from agent.tools import _search_products, build_tool_result_event, coerce_tool_args, format_tool_result_for_model, parse_failed_tool_generation, parse_product_markdown, parse_search_products_markdown, parse_tracking_result


class ToolArgSanitizerTest(unittest.TestCase):
Expand Down Expand Up @@ -115,6 +115,31 @@ def test_parses_bracketed_failed_generation(self):
"sort": "relevance",
}))

def test_parses_tracking_result_and_emits_tracking_event(self):
tracking_json = """{
"order_number": "12345678",
"status": "Out for delivery",
"recipient_name": "Alex",
"estimated_delivery": "2026-06-30",
"current_location": "Colombo",
"items": [{"name": "Chocolate Box", "quantity": 1}],
"timeline": [
{"status": "Out for delivery", "timestamp": "2026-06-29 10:30", "location": "Colombo"},
{"status": "Processed", "timestamp": "2026-06-28 18:00"}
]
}"""

parsed = parse_tracking_result(tracking_json)
self.assertEqual(parsed["order_number"], "12345678")
self.assertEqual(parsed["status"], "Out for delivery")
self.assertEqual(parsed["recipient"], "Alex")
self.assertEqual(parsed["items"][0]["name"], "Chocolate Box")
self.assertEqual(parsed["events"][0]["label"], "Out for delivery")

event = build_tool_result_event("track_order", tracking_json)
self.assertIn("tracking", event)
self.assertEqual(event["tracking"]["location"], "Colombo")


if __name__ == "__main__":
unittest.main()
33 changes: 33 additions & 0 deletions docs/feature-checklists/02-order-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Feature 02: Order Tracking

## Goal

Add order-tracking capability inspired by the candidate repos, including a friendly prompt flow, MCP-backed tracking tool support, and a dedicated tracking status card in chat.

## Checklist

- [x] Review candidate tracking implementations and card layouts
- [x] Pick the smallest tracking flow that fits the current architecture
- [x] Add backend `track_order` tool wrapper around `kapruka_track_order`
- [x] Add structured tracking payload support to tool-result events
- [x] Add tracking card component in the frontend
- [x] Add clear entry points from quick actions or common prompts
- [x] Handle the no-order-number case with a helpful assistant response
- [x] Verify tracking render logic with a representative response shape
- [x] Run backend tests relevant to provider/tool behavior
- [x] Run `npm run lint`
- [x] Run `npm run build`
- [x] Verify the flow in a live browser session
- [x] Commit, push branch, and open draft PR

## Notes

- Candidate reference: `tmp_compare/kavi-kapruka/components/checkout/TrackOrderCard.tsx`
- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/components/TrackingCard.js`
- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/agents/tracking-agent.js`
- Browser verification used `http://localhost:3000` through a Playwright fallback because the in-app Browser tool was not callable in this thread.
- Live browser checks covered:
- Missing-order-number flow: "I want to track my order" returns a direct request for the Kapruka order number.
- Tool path flow: "Track order 12345678" executes the tracking path and returns a graceful not-found response.
- Follow-up UI polish added empty-state hero buttons for tracking and category browsing.
- Structured tracking payload normalization is covered by backend unit tests with a representative JSON response shape.
25 changes: 25 additions & 0 deletions docs/feature-checklists/03-pay-link-card.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Feature 03: Pay Link Card

## Goal

Upgrade the checkout completion experience so successful order placement produces a visible payment-link or order-summary card instead of only plain chat text.

## Checklist

- [ ] Review candidate pay-link and order-summary implementations
- [ ] Confirm current `checkout` tool result shape from the MCP
- [ ] Add structured order payload support to backend tool-result events
- [ ] Add a dedicated pay-link or order-summary card component
- [ ] Surface price lock / payment guidance clearly in the UI
- [ ] Preserve the existing cart and checkout drawer flow
- [ ] Verify behavior for success and incomplete-checkout cases
- [ ] Run backend tests relevant to order payload parsing
- [ ] Run `npm run lint`
- [ ] Run `npm run build`
- [ ] Verify the flow in a live browser session
- [ ] Commit, push branch, and open draft PR

## Notes

- Candidate reference: `tmp_compare/kavi-kapruka/components/checkout/PayLinkCard.tsx`
- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/components/OrderSummary.js`
24 changes: 24 additions & 0 deletions docs/feature-checklists/04-voice-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Feature 04: Voice Mode

## Goal

Add browser-native voice input and optional spoken assistant replies using built-in Web Speech APIs, following the candidate approach without new paid services.

## Checklist

- [ ] Review candidate voice input/output implementation
- [ ] Decide the smallest supported browser behavior and fallback text
- [ ] Add voice input button and listening state
- [ ] Add optional voice output toggle for assistant replies
- [ ] Keep the chat input usable when voice APIs are unavailable
- [ ] Ensure the UI remains clean on desktop and mobile
- [ ] Add at least one lightweight check for prompt/input behavior
- [ ] Run `npm run lint`
- [ ] Run `npm run build`
- [ ] Verify the flow in a live browser session
- [ ] Commit, push branch, and open draft PR

## Notes

- Candidate reference: `tmp_compare/kavi-kapruka/lib/speech.ts`
- Candidate reference: `tmp_compare/kavi-kapruka/components/chat/VoiceButton.tsx`
23 changes: 23 additions & 0 deletions docs/feature-checklists/05-quick-actions-and-welcome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Feature 05: Quick Actions And Welcome

## Goal

Improve discovery and usability with better quick actions, a stronger welcome state, and candidate-inspired shortcuts into gifts, categories, checkout, and tracking.

## Checklist

- [ ] Review candidate welcome screens and quick-action patterns
- [ ] Decide which shortcuts belong on the empty state versus active chat state
- [ ] Add better quick-action chips for common shopping intents
- [ ] Improve the welcome state so it advertises core capabilities clearly
- [ ] Keep the new layout consistent with the current visual direction
- [ ] Verify prompts generated by quick actions still lead to cards or useful assistant text
- [ ] Run `npm run lint`
- [ ] Run `npm run build`
- [ ] Verify the flow in a live browser session
- [ ] Commit, push branch, and open draft PR

## Notes

- Candidate reference: `tmp_compare/kavi-kapruka/components/chat/QuickActions.tsx`
- Candidate reference: `tmp_compare/kavi-kapruka/components/chat/WelcomeScreen.tsx`
24 changes: 24 additions & 0 deletions docs/feature-checklists/06-multilingual-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Feature 06: Multilingual UI

## Goal

Make the product feel more Sri Lanka-aware by adding localized UI copy and lightweight language-aware helper responses on top of the current backend language handling.

## Checklist

- [ ] Review candidate multilingual dictionaries and language-detection behavior
- [ ] Decide the minimum viable language surface for this app
- [ ] Add localized labels and helper copy for the main UI flows
- [ ] Keep language changes lightweight and avoid duplicating business logic
- [ ] Verify English remains the default fallback
- [ ] Add at least one focused test for language selection or copy mapping
- [ ] Run `npm run lint`
- [ ] Run `npm run build`
- [ ] Verify the flow in a live browser session
- [ ] Commit, push branch, and open draft PR

## Notes

- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/i18n/index.js`
- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/i18n/en.js`
- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/i18n/si.js`
25 changes: 25 additions & 0 deletions docs/feature-checklists/07-reliability-and-debug-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Feature 07: Reliability And Debug Surface

## Goal

Finish the candidate-inspired hardening work: predictable small-model defaults, provider fallback, and a minimal MCP debug surface for easier iteration and demos.

## Checklist

- [ ] Review candidate MCP route and rate-limiting/debug patterns
- [ ] Confirm which reliability work is already shipped versus still missing
- [ ] Add or refine a minimal MCP debug route if still valuable
- [ ] Add lightweight request hardening only where it helps the demo app
- [ ] Keep the solution small and avoid unnecessary platform complexity
- [ ] Verify provider fallback behavior against live or simulated failures
- [ ] Run backend tests relevant to provider selection and fallback
- [ ] Run `npm run lint`
- [ ] Run `npm run build`
- [ ] Verify the flow in a live browser session
- [ ] Commit, push branch, and open draft PR

## Notes

- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/app/api/mcp/route.js`
- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/rate-limiter.js`
- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/mcp/client.js`
Loading