| """ |
| CDMS Label Search Tool |
| Search for pesticide product labels from the CDMS database with full citations |
| """ |
|
|
| import os |
| from typing import Dict, Any, Optional |
| from pathlib import Path |
| import sys |
|
|
| |
| project_root = Path(__file__).parent.parent.parent |
| sys.path.insert(0, str(project_root)) |
|
|
| from src.cdms.rag_search import CDMSRAGSearch |
|
|
|
|
| def _offline_index_enabled() -> bool: |
| """Legacy env flag, retained as a hallucination guard elsewhere. |
| |
| Still read by llm_response_generator to refuse ungrounded Tavily-only |
| summaries. It no longer gates retrieval mode here — see _live_fallback_enabled. |
| """ |
| return os.environ.get("CDMS_OFFLINE_INDEX", "1") == "1" |
|
|
|
|
| def _live_fallback_enabled() -> bool: |
| """Auto-mode: on an index miss, live-fetch the label and cache it for the session. |
| |
| On (default) the tool serves from the committed index when the label is present |
| and only reaches out to CDMS (Tavily + download + re-embed) when it isn't — |
| caching the result in-process so repeat asks are fast. Set CDMS_LIVE_FALLBACK=0 |
| to force index-only (e.g. a keyless deploy or to cap live-fetch cost). |
| """ |
| return os.environ.get("CDMS_LIVE_FALLBACK", "1") == "1" |
|
|
|
|
| class CDMSLabelTool: |
| """ |
| Tool for searching CDMS pesticide labels |
| |
| Auto-mode (default): serve from the committed Qdrant index when the label is |
| already indexed; otherwise fetch it live via Tavily (cdms.net domain filter), |
| download + index it, and answer — caching it in the running process so later |
| asks for the same label are fast. With no Tavily key (or CDMS_LIVE_FALLBACK=0) |
| it degrades to index-only and abstains for un-indexed products. |
| """ |
|
|
| def __init__(self, offline: bool = None): |
| """Initialize the CDMS label search tool. |
| |
| offline: None -> auto-mode (index first, live-fetch on miss when a Tavily |
| key is available; see _live_fallback_enabled). True -> force index-only |
| (never live-fetch). False -> allow live fallback. Retained for callers/tests |
| that still pass it explicitly. |
| """ |
| |
| if offline is None: |
| self.force_index_only = not _live_fallback_enabled() |
| else: |
| self.force_index_only = bool(offline) |
| self.tool_name = "cdms_label_search" |
| self.description = "Search for pesticide product labels and safety data sheets from the CDMS database" |
|
|
| |
| self.rag_search = CDMSRAGSearch() |
| self.client = None |
| self.pdf_downloader = None |
| self.document_loader = None |
| |
| |
| |
| self.live_available = False |
| if not self.force_index_only: |
| try: |
| from src.api_clients.tavily_client import TavilyAPIClient |
| from src.cdms.pdf_downloader import CDMSPDFDownloader |
| from src.cdms.document_loader import DocumentLoader |
| from src.config.paths import PDF_DIR |
| |
| |
| |
| |
| self.client = TavilyAPIClient() |
| self.pdf_downloader = CDMSPDFDownloader() |
| self.document_loader = DocumentLoader(pdf_folder=str(PDF_DIR)) |
| self.live_available = True |
| except Exception as e: |
| print(f"⚠️ Live fetch unavailable ({e}); serving from the offline index only.") |
| self.live_available = False |
| |
| def search( |
| self, |
| product_name: str, |
| active_ingredient: Optional[str] = None, |
| max_results: int = 5 |
| ) -> Dict[str, Any]: |
| """ |
| Search for CDMS pesticide labels |
| |
| Args: |
| product_name: Product or brand name (e.g., "Roundup", "Sevin") |
| active_ingredient: Optional active ingredient (e.g., "glyphosate") |
| max_results: Maximum number of label results to return (1-5) |
| |
| Returns: |
| Dict with: |
| - success: bool |
| - product_name: str |
| - active_ingredient: str or None |
| - summary: str (AI-generated summary) |
| - labels: List[Dict] with: |
| - title: str (label name) |
| - url: str (direct PDF link) |
| - snippet: str (preview text) |
| - relevance: float (0-1) |
| - label_count: int |
| - citations: str (formatted citation text) |
| """ |
| |
| raw_results = self.client.search_cdms_labels( |
| product_name=product_name, |
| active_ingredient=active_ingredient, |
| max_results=max_results |
| ) |
| |
| if not raw_results.get("success"): |
| return { |
| "success": False, |
| "error": raw_results.get("error", "Search failed"), |
| "product_name": product_name, |
| "labels": [], |
| "label_count": 0 |
| } |
| |
| |
| labels = [] |
| for result in raw_results.get("results", []): |
| label = { |
| "title": result.get("title", "No title"), |
| "url": result.get("url", ""), |
| "snippet": result.get("content", "")[:300], |
| "relevance": result.get("score", 0.0) |
| } |
| labels.append(label) |
| |
| |
| citations = self._format_citations(labels) |
| |
| |
| return { |
| "success": True, |
| "product_name": product_name, |
| "active_ingredient": active_ingredient, |
| "summary": raw_results.get("answer", ""), |
| "labels": labels, |
| "label_count": len(labels), |
| "citations": citations, |
| "query_used": raw_results.get("query", ""), |
| "search_metadata": raw_results.get("search_metadata", {}), |
| "raw_tavily_results": raw_results |
| } |
| |
| def _format_citations(self, labels: list) -> str: |
| """ |
| Format label results as citation text |
| |
| Args: |
| labels: List of label results |
| |
| Returns: |
| Formatted citation string |
| """ |
| if not labels: |
| return "No citations available." |
| |
| citation_parts = ["**Sources:**\n"] |
| |
| for i, label in enumerate(labels, 1): |
| citation_parts.append( |
| f"{i}. **{label['title']}**\n" |
| f" - URL: {label['url']}\n" |
| f" - Relevance: {label['relevance']:.2f}\n" |
| ) |
| |
| return "\n".join(citation_parts) |
| |
| def format_response_for_user(self, result: Dict[str, Any]) -> str: |
| """ |
| Format search results for user-friendly display |
| |
| Args: |
| result: Search result from search() |
| |
| Returns: |
| Formatted string for display to user |
| """ |
| if not result.get("success"): |
| return f"❌ Could not find labels: {result.get('error', 'Unknown error')}" |
| |
| product = result.get("product_name", "Unknown product") |
| ingredient = result.get("active_ingredient") |
| summary = result.get("summary", "") |
| labels = result.get("labels", []) |
| |
| |
| response_parts = [] |
| |
| |
| if ingredient: |
| response_parts.append(f"**CDMS Labels for {product} ({ingredient})**\n") |
| else: |
| response_parts.append(f"**CDMS Labels for {product}**\n") |
| |
| |
| if summary: |
| response_parts.append(f"**Summary:** {summary}\n") |
| |
| |
| response_parts.append(f"**Found {len(labels)} label(s):**\n") |
| |
| for i, label in enumerate(labels, 1): |
| response_parts.append( |
| f"{i}. **{label['title']}**\n" |
| f" 📄 Download: {label['url']}\n" |
| f" 📝 Preview: {label['snippet'][:150]}...\n" |
| ) |
| |
| |
| response_parts.append(f"\n{result.get('citations', '')}") |
| |
| return "\n".join(response_parts) |
| |
| def download_pdfs(self, tavily_results: Dict[str, Any], product_name: str) -> Dict[str, Any]: |
| """ |
| Download PDFs from Tavily search results |
| |
| Args: |
| tavily_results: Results from search() method (includes raw_tavily_results) |
| product_name: Product name for filename |
| |
| Returns: |
| Dict with: |
| - success: bool |
| - downloaded_pdfs: List[Dict] with filepath, filename, cached status |
| - pdf_count: int |
| - errors: List[str] (any download errors) |
| """ |
| |
| raw_results = tavily_results.get("raw_tavily_results", tavily_results) |
| pdf_urls = self.pdf_downloader.extract_pdf_urls(raw_results) |
| |
| if not pdf_urls: |
| print(f"⚠️ No PDF URLs found in Tavily results for {product_name}") |
| |
| labels = tavily_results.get("labels", []) |
| for label in labels: |
| url = label.get("url", "") |
| if url and (url.lower().endswith('.pdf') or 'pdf' in url.lower()): |
| pdf_urls.append(url) |
| print(f" Found PDF URL in labels: {url}") |
| |
| if not pdf_urls: |
| return { |
| "success": False, |
| "error": "No PDF URLs found in search results", |
| "downloaded_pdfs": [], |
| "pdf_count": 0 |
| } |
| |
| print(f"📥 Found {len(pdf_urls)} PDF URL(s) to download for {product_name}") |
| |
| |
| downloaded_pdfs = [] |
| errors = [] |
| |
| for i, url in enumerate(pdf_urls[:3], 1): |
| print(f" Downloading PDF {i}/{min(len(pdf_urls), 3)}: {url[:60]}...") |
| result = self.pdf_downloader.download_pdf(url, product_name) |
| if result.get("success"): |
| cached_status = "cached" if result.get("cached") else "downloaded" |
| print(f" ✅ {cached_status}: {result.get('filename')}") |
| downloaded_pdfs.append({ |
| "filepath": result["filepath"], |
| "filename": result["filename"], |
| "cached": result["cached"], |
| "url": result["url"], |
| "url_hash": result["url_hash"] |
| }) |
| else: |
| error_msg = result.get('error', 'Unknown error') |
| print(f" ❌ Failed: {error_msg}") |
| errors.append(f"Failed to download {url}: {error_msg}") |
| |
| if downloaded_pdfs: |
| print(f"✅ Successfully downloaded {len(downloaded_pdfs)} PDF(s)") |
| else: |
| print("❌ No PDFs were downloaded") |
| |
| return { |
| "success": len(downloaded_pdfs) > 0, |
| "downloaded_pdfs": downloaded_pdfs, |
| "pdf_count": len(downloaded_pdfs), |
| "errors": errors if errors else None |
| } |
| |
| def _is_pdf_indexed(self, pdf_path: str) -> bool: |
| """ |
| Check if PDF is already indexed in Qdrant |
| |
| Args: |
| pdf_path: Path to PDF file |
| |
| Returns: |
| True if PDF is indexed, False otherwise |
| """ |
| try: |
| from src.cdms.schema import Document, DatabaseManager |
| db_manager = DatabaseManager() |
| session = db_manager.get_session() |
| |
| try: |
| pdf_path_obj = Path(pdf_path) |
| doc_id = Document.generate_id(str(pdf_path_obj)) |
| existing_doc = session.query(Document).filter_by(id=doc_id).first() |
| |
| if existing_doc and existing_doc.processed == 1: |
| return True |
| return False |
| finally: |
| session.close() |
| except Exception: |
| return False |
| |
| def search_with_rag( |
| self, |
| product_name: str, |
| user_question: str, |
| active_ingredient: Optional[str] = None, |
| on_step=None, |
| ) -> Dict[str, Any]: |
| """ |
| Auto-mode RAG pipeline: index-first, live-fetch (Tavily → Download → |
| Process → Index → RAG Search) only on an index miss, then cache. |
| |
| Args: |
| product_name: Product name (e.g., "Roundup") |
| user_question: User's question (e.g., "What's the application rate?") |
| active_ingredient: Optional active ingredient |
| on_step: Optional callback(str) invoked at each stage, so the UI can |
| surface the live pipeline (and show that a first-time label fetch |
| is what's taking the extra time). |
| |
| Returns: |
| Dict with: |
| - success: bool |
| - product_name: str |
| - rag_chunks: List[Dict] with content, page_number, score |
| - pdfs_downloaded: int |
| - pdfs_indexed: int |
| - total_chunks_found: int |
| - source: "index" | "live" (where the answer came from) |
| """ |
| def _step(msg: str) -> None: |
| if on_step: |
| try: |
| on_step(msg) |
| except Exception: |
| pass |
|
|
| |
| |
| _step(f"Searching indexed labels for “{product_name}”…") |
| rag_chunks = self.rag_search.search( |
| query=user_question, |
| product_name=product_name, |
| limit=5, |
| score_threshold=0.4 |
| ) |
|
|
| |
| |
| if rag_chunks or not self.live_available: |
| if rag_chunks: |
| _step("Found matching label pages in the index.") |
| elif self.force_index_only: |
| _step("Label not in the index (index-only mode).") |
| else: |
| _step("Label not in the index and live fetch is unavailable.") |
| return { |
| "success": True, |
| "product_name": product_name, |
| "rag_chunks": rag_chunks, |
| "pdfs_downloaded": 0, |
| "pdfs_indexed": 0, |
| "total_chunks_found": len(rag_chunks), |
| "offline_index": True, |
| "source": "index", |
| } |
|
|
| |
| |
| _step(f"Not indexed yet — fetching “{product_name}” from CDMS…") |
|
|
| |
| print(f"🔍 Step 1: Searching Tavily for '{product_name}' PDFs...") |
| tavily_result = self.search( |
| product_name=product_name, |
| active_ingredient=active_ingredient, |
| max_results=3 |
| ) |
| |
| if not tavily_result.get("success"): |
| error_msg = tavily_result.get("error", "Tavily search failed") |
| print(f"❌ Tavily search failed: {error_msg}") |
| return { |
| "success": False, |
| "error": error_msg, |
| "product_name": product_name |
| } |
| |
| labels_found = tavily_result.get("label_count", 0) |
| print(f"✅ Tavily search successful: Found {labels_found} label(s)") |
|
|
| |
| _step("Downloading the label PDF from CDMS…") |
| print(f"📥 Step 2: Downloading PDFs for '{product_name}'...") |
| download_result = self.download_pdfs(tavily_result, product_name) |
| |
| if not download_result.get("success"): |
| error_msg = download_result.get("error", "PDF download failed") |
| print(f"❌ PDF download failed: {error_msg}") |
| return { |
| "success": False, |
| "error": error_msg, |
| "product_name": product_name |
| } |
| |
| downloaded_pdfs = download_result.get("downloaded_pdfs", []) |
|
|
| |
| _step("Processing & indexing the label (first-time only)…") |
| pdfs_indexed = 0 |
| for pdf_info in downloaded_pdfs: |
| pdf_path = pdf_info["filepath"] |
| pdf_url = pdf_info.get("url", "") |
| |
| |
| if not self._is_pdf_indexed(pdf_path): |
| |
| try: |
| index_result = self.document_loader.load_pdf( |
| pdf_path, |
| force_reprocess=False, |
| pdf_url=pdf_url |
| ) |
| if index_result.get("success"): |
| pdfs_indexed += 1 |
| except Exception as e: |
| print(f"⚠️ Warning: Could not index {pdf_path}: {e}") |
| |
| |
| _step("Reading the freshly indexed label…") |
| rag_chunks = self.rag_search.search( |
| query=user_question, |
| product_name=product_name, |
| limit=5, |
| score_threshold=0.4 |
| ) |
|
|
| |
| |
| filename_to_url = {} |
| |
| url_hash_to_url = {} |
| |
| for pdf_info in downloaded_pdfs: |
| filename = pdf_info.get("filename", "") |
| url = pdf_info.get("url", "") |
| url_hash = pdf_info.get("url_hash", "") |
| |
| if filename and url: |
| filename_to_url[filename] = url |
| if url_hash and url: |
| url_hash_to_url[url_hash] = url |
| |
| |
| tavily_urls = {} |
| tavily_labels = tavily_result.get("labels", []) |
| for label in tavily_labels: |
| url = label.get("url", "") |
| if url and url.lower().endswith('.pdf'): |
| |
| tavily_urls[url] = url |
| |
| |
| if '/ldat/' in url: |
| url_id = url.split('/ldat/')[-1].replace('.pdf', '') |
| tavily_urls[url_id] = url |
| |
| |
| chunks_with_url = 0 |
| chunks_without_url = 0 |
| |
| for chunk in rag_chunks: |
| |
| if chunk.get("pdf_url"): |
| chunks_with_url += 1 |
| continue |
| |
| source_file = chunk.get("source_file", "") |
| document_id = chunk.get("document_id", "") |
| chunk_url_hash = chunk.get("url_hash", "") |
| |
| |
| if chunk_url_hash and chunk_url_hash in url_hash_to_url: |
| chunk["pdf_url"] = url_hash_to_url[chunk_url_hash] |
| chunks_with_url += 1 |
| continue |
| |
| |
| |
| if document_id: |
| for pdf_info in downloaded_pdfs: |
| |
| from src.cdms.schema import Document |
| pdf_doc_id = Document.generate_id(pdf_info["filepath"]) |
| if pdf_doc_id == document_id: |
| chunk["pdf_url"] = pdf_info.get("url", "") |
| if chunk["pdf_url"]: |
| chunks_with_url += 1 |
| break |
| if chunk.get("pdf_url"): |
| continue |
| |
| |
| if source_file in filename_to_url: |
| chunk["pdf_url"] = filename_to_url[source_file] |
| chunks_with_url += 1 |
| continue |
| |
| |
| |
| product_lower = product_name.lower() |
| matched = False |
| for filename, url in filename_to_url.items(): |
| if product_lower in filename.lower(): |
| chunk["pdf_url"] = url |
| chunks_with_url += 1 |
| matched = True |
| break |
| |
| if matched: |
| continue |
| |
| |
| if not chunk.get("pdf_url"): |
| |
| |
| if tavily_urls: |
| |
| chunk["pdf_url"] = list(tavily_urls.values())[0] |
| chunks_with_url += 1 |
| else: |
| chunks_without_url += 1 |
| print(f"⚠️ Warning: Could not find PDF URL for chunk from {source_file} (document_id: {document_id})") |
| |
| |
| if chunks_without_url > 0: |
| print(f"⚠️ Warning: {chunks_without_url} chunk(s) missing PDF URLs") |
| print(f"✅ PDF URL matching: {chunks_with_url}/{len(rag_chunks)} chunks have URLs") |
| |
| |
| _step("Writing the answer…") |
| return { |
| "success": True, |
| "product_name": product_name, |
| "rag_chunks": rag_chunks, |
| "pdfs_downloaded": len(downloaded_pdfs), |
| "pdfs_indexed": pdfs_indexed, |
| "total_chunks_found": len(rag_chunks), |
| "source": "live", |
| "tavily_results": tavily_result, |
| "download_info": download_result, |
| "pdf_urls": list(filename_to_url.values()), |
| "tavily_labels": tavily_labels |
| } |
|
|
|
|
| def execute_cdms_label_tool(question: str, conversation_context: list = None, offline: bool = None, on_step=None) -> Dict: |
| """ |
| Execute CDMS label search tool |
| |
| This is the interface for the tool executor. |
| Extracts product name and active ingredient from the question and searches CDMS. |
| Uses conversation context for follow-up questions. |
| |
| Args: |
| question: User's natural language question |
| conversation_context: Optional list of previous messages for context |
| |
| Returns: |
| Dict with: |
| { |
| "success": True/False, |
| "tool": "cdms_label", |
| "data": {...search results with citations...}, |
| "error": "error message" if failed |
| } |
| """ |
| try: |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| from src.cdms.product_catalog import get_catalog |
| catalog = get_catalog() |
|
|
| product_name = None |
| active_ingredient = None |
|
|
| question_lower = question.lower() |
|
|
| |
| product_name = catalog.resolve(question) |
|
|
| |
| |
| if not product_name and conversation_context: |
| for msg in reversed(conversation_context): |
| resolved = catalog.resolve(msg.get("content", "")) |
| if resolved: |
| product_name = resolved |
| break |
| |
| |
| if not product_name: |
| if "label for" in question_lower: |
| parts = question_lower.split("label for") |
| if len(parts) > 1: |
| product_name = parts[1].strip().split()[0] if parts[1].strip() else None |
| elif "label" in question_lower: |
| parts = question_lower.split("label") |
| if parts[0].strip(): |
| words = parts[0].strip().split() |
| if words: |
| product_name = words[-1] |
| |
| |
| |
| is_pesticide_related = any( |
| kw in question_lower for kw in [ |
| "pesticide", "herbicide", "insecticide", "fungicide", "label", |
| "application rate", "safety", "mixing", "chemical", "cdms" |
| ] |
| ) |
| |
| |
| |
| if not product_name: |
| |
| |
| words = question_lower.split() |
| |
| |
| if "label" in words: |
| label_idx = words.index("label") |
| if label_idx > 0: |
| |
| |
| potential_product_parts = [] |
| for i in range(label_idx - 1, -1, -1): |
| word = words[i] |
| if word in ["the", "a", "an", "find", "get", "show", "search", "for", "of"]: |
| break |
| potential_product_parts.insert(0, word) |
| if len(potential_product_parts) >= 4: |
| break |
| if potential_product_parts: |
| product_name = " ".join(potential_product_parts) |
| |
| |
| if not product_name: |
| for term in ["pesticide", "herbicide", "insecticide", "fungicide"]: |
| if term in words: |
| term_idx = words.index(term) |
| if term_idx > 0: |
| |
| potential_product_parts = [] |
| for i in range(term_idx - 1, -1, -1): |
| word = words[i] |
| if word in ["the", "a", "an", "find", "get", "show", "search", "for", "of"]: |
| break |
| potential_product_parts.insert(0, word) |
| if len(potential_product_parts) >= 4: |
| break |
| if potential_product_parts: |
| product_name = " ".join(potential_product_parts) |
| break |
| |
| |
| if not product_name: |
| if is_pesticide_related: |
| |
| |
| |
| |
| |
| |
| import re as _re |
| _STOP = { |
| "what", "whats", "how", "tell", "me", "about", "find", "get", "show", |
| "give", "search", "for", "the", "a", "an", "is", "are", "was", "were", |
| "do", "does", "did", "can", "could", "will", "would", "should", "i", |
| "my", "need", "want", "know", "of", "on", "in", "at", "to", "and", "or", |
| "this", "that", "it", "its", "please", "label", "labels", "pesticide", |
| "herbicide", "insecticide", "fungicide", "application", "rate", "rates", |
| "apply", "applied", "safety", "mixing", "mix", "interval", "use", |
| "using", "chemical", "information", "info", "product", "amount", "dose", |
| "dosage", "much", |
| } |
| toks = [t for t in (_re.sub(r"[^\w%.-]", "", w) for w in question.split()) if t] |
| lower = [t.lower() for t in toks] |
| anchors = [i for i, lo in enumerate(lower) if lo in ("for", "of", "about")] |
| cand = [] |
| if anchors: |
| cand = [toks[i] for i in range(max(anchors) + 1, len(toks)) if lower[i] not in _STOP] |
| if not cand: |
| cand = [t for t, lo in zip(toks, lower) if lo not in _STOP] |
| product_name = " ".join(cand[:4]) if cand else "pesticide" |
| else: |
| |
| return { |
| "success": False, |
| "tool": "cdms_label", |
| "error": "Could not identify the pesticide product name. Please specify a product (e.g., 'Find Roundup label')", |
| "should_fallback": True |
| } |
| |
| |
| |
| tool = CDMSLabelTool(offline=offline) |
| |
| |
| enhanced_question = question |
| |
| |
| followup_keywords = { |
| "safety": ["safety", "safe", "precaution", "hazard", "danger", "toxic", "poison", "warning", "protective"], |
| "application": ["application", "apply", "rate", "dosage", "amount", "how much", "when to apply"], |
| "mixing": ["mix", "mixing", "dilute", "dilution", "solution", "concentrate", "ratio"], |
| "reentry": ["re-entry", "reentry", "rei", "when can i", "how long", "wait", "interval"], |
| "storage": ["store", "storage", "keep", "shelf life", "expiration"], |
| "crops": ["crop", "crops", "use on", "for", "suitable", "compatible"] |
| } |
| |
| detected_type = None |
| for ftype, fkeywords in followup_keywords.items(): |
| if any(kw in question_lower for kw in fkeywords): |
| detected_type = ftype |
| break |
| |
| |
| is_followup = ( |
| conversation_context and ( |
| |
| (not product_name or product_name == "pesticide") or |
| |
| len(question.split()) <= 5 or |
| |
| detected_type is not None or |
| |
| any(phrase in question_lower for phrase in [ |
| "what about", "how about", "tell me more", "and", "also", "what's the" |
| ]) |
| ) |
| ) |
| |
| if is_followup: |
| |
| |
| |
| context_product = None |
| for msg in reversed(conversation_context): |
| resolved = catalog.resolve(msg.get("content", "")) |
| if resolved: |
| context_product = resolved |
| break |
| |
| |
| if context_product and (not product_name or product_name == "pesticide"): |
| product_name = context_product |
| |
| |
| if product_name and product_name != "pesticide": |
| if detected_type: |
| |
| enhanced_question = f"{question} for {product_name} {detected_type}" |
| else: |
| enhanced_question = f"{question} about {product_name}" |
| elif detected_type: |
| |
| enhanced_question = f"{question} {detected_type}" |
| |
| |
| result = tool.search_with_rag( |
| product_name=product_name, |
| user_question=enhanced_question, |
| active_ingredient=active_ingredient, |
| on_step=on_step, |
| ) |
| |
| if not result.get("success"): |
| return { |
| "success": False, |
| "tool": "cdms_label", |
| "error": result.get("error", "CDMS RAG search failed") |
| } |
| |
| |
| return { |
| "success": True, |
| "tool": "cdms_label", |
| "data": result |
| } |
| |
| except Exception as e: |
| return { |
| "success": False, |
| "tool": "cdms_label", |
| "error": f"Unexpected error: {str(e)}" |
| } |
|
|
|
|
| |
| if __name__ == "__main__": |
| print("=" * 80) |
| print("Testing CDMS Label Tool with Citations") |
| print("=" * 80) |
| |
| tool = CDMSLabelTool() |
| |
| |
| print("\nTEST 1: Search for Roundup labels") |
| print("-" * 80) |
| |
| result = tool.search( |
| product_name="Roundup", |
| active_ingredient="glyphosate", |
| max_results=3 |
| ) |
| |
| |
| print(tool.format_response_for_user(result)) |
| |
| |
| print("\n" + "=" * 80) |
| print("TEST 2: Search for Sevin labels") |
| print("-" * 80) |
| |
| result = tool.search( |
| product_name="Sevin", |
| active_ingredient="carbaryl", |
| max_results=3 |
| ) |
| |
| print(tool.format_response_for_user(result)) |
| |
| |
| print("\n" + "=" * 80) |
| print("TEST 3: Search with product name only") |
| print("-" * 80) |
| |
| result = tool.search( |
| product_name="2,4-D", |
| max_results=3 |
| ) |
| |
| print(tool.format_response_for_user(result)) |
| |
| print("\n" + "=" * 80) |
| print("✅ All tests complete!") |
| print("=" * 80) |
|
|
|
|