Spaces:
Sleeping
Sleeping
Riley Claude commited on
Commit ·
477e08a
1
Parent(s): e616a87
fix: Restore exact Gradio UI from working commit c72a240
Browse filesRestored the exact demo_app.py, chat_tools.py, and tool_schemas.py
from c72a240 which was the last known working Hugging Face deployment.
Key differences from current version:
- 7 preset questions (includes past winners questions)
- Clean, simpler system prompt
- No emojis, no websocket code
- Working tool integration
This is the exact state that was deployed and working on HF Spaces
before all the merge/push issues started.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- analyzer/chat/chat_tools.py +136 -237
- analyzer/chat/demo_app.py +114 -235
- analyzer/chat/tool_schemas.py +39 -7
analyzer/chat/chat_tools.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
| 3 |
import asyncio
|
| 4 |
import logging
|
| 5 |
import re
|
|
|
|
| 6 |
from dataclasses import dataclass
|
| 7 |
from datetime import datetime
|
| 8 |
from pathlib import Path
|
|
@@ -55,6 +56,51 @@ def _norm(s: Any) -> str:
|
|
| 55 |
return clean(str(s)) # Use utils.text.clean() for final normalization
|
| 56 |
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
# ---------------------------------------------------------------------
|
| 59 |
# Main ChatTools class
|
| 60 |
# ---------------------------------------------------------------------
|
|
@@ -86,17 +132,6 @@ class ChatTools:
|
|
| 86 |
# Initialize cache for summaries (NEW: Optimized caching)
|
| 87 |
self.summary_cache = SummaryCache(ttl_seconds=3600)
|
| 88 |
|
| 89 |
-
# -----------------------------------------------------------------
|
| 90 |
-
# Cache stats logging helper
|
| 91 |
-
# -----------------------------------------------------------------
|
| 92 |
-
def _log_cache_stats(self) -> None:
|
| 93 |
-
"""Log cache statistics for monitoring."""
|
| 94 |
-
stats = self.summary_cache.stats()
|
| 95 |
-
logging.info(
|
| 96 |
-
f"📊 Cache stats: {stats['valid']}/{stats['cached']} valid entries "
|
| 97 |
-
f"({stats['valid']/max(stats['cached'], 1)*100:.1f}% hit rate)"
|
| 98 |
-
)
|
| 99 |
-
|
| 100 |
# -----------------------------------------------------------------
|
| 101 |
# Status calculation (NEW)
|
| 102 |
# -----------------------------------------------------------------
|
|
@@ -154,7 +189,8 @@ class ChatTools:
|
|
| 154 |
results = []
|
| 155 |
for r in self.current:
|
| 156 |
txt = _norm(r)
|
| 157 |
-
|
|
|
|
| 158 |
continue
|
| 159 |
if max_award is not None:
|
| 160 |
ma = to_number(r.get("max_award") or r.get("funding_max"))
|
|
@@ -183,6 +219,76 @@ class ChatTools:
|
|
| 183 |
return results
|
| 184 |
return results[:limit]
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
# -----------------------------------------------------------------
|
| 187 |
# Retrieve a single grant
|
| 188 |
# -----------------------------------------------------------------
|
|
@@ -254,13 +360,8 @@ class ChatTools:
|
|
| 254 |
# Yield each result as it's ready
|
| 255 |
for result in results:
|
| 256 |
yield result
|
| 257 |
-
|
| 258 |
-
# Log cache stats after batch completion
|
| 259 |
-
self._log_cache_stats()
|
| 260 |
except Exception as e:
|
| 261 |
logging.error(f"Batch summarization failed: {e}")
|
| 262 |
-
# Log cache stats even on error
|
| 263 |
-
self._log_cache_stats()
|
| 264 |
raise
|
| 265 |
|
| 266 |
async def get_all_grant_summaries(self, batch_size: int = 5):
|
|
@@ -292,20 +393,16 @@ class ChatTools:
|
|
| 292 |
async for result in self.summarize_grants_batch(all_grant_ids, batch_size=batch_size):
|
| 293 |
yield result
|
| 294 |
|
| 295 |
-
# Log final cache stats
|
| 296 |
-
self._log_cache_stats()
|
| 297 |
-
|
| 298 |
# -----------------------------------------------------------------
|
| 299 |
# Summarize a grant
|
| 300 |
# -----------------------------------------------------------------
|
| 301 |
-
def summarize_grant(self, gid: str, include_supporting: bool = True
|
| 302 |
"""
|
| 303 |
-
Summarize a grant using LLM with
|
| 304 |
|
| 305 |
Args:
|
| 306 |
gid: Grant ID
|
| 307 |
include_supporting: If True, include supporting PDFs and materials in context
|
| 308 |
-
summary_type: Type of summary to retrieve ("layman", "technical", "exec")
|
| 309 |
|
| 310 |
Returns:
|
| 311 |
Dict with summary_md, title, id
|
|
@@ -314,27 +411,10 @@ class ChatTools:
|
|
| 314 |
title = row.get("title", "(untitled)")
|
| 315 |
grant_id = row.get("id") or gid
|
| 316 |
|
| 317 |
-
# NEW: Check
|
| 318 |
-
try:
|
| 319 |
-
from ...database import SummaryStore
|
| 320 |
-
summary_store = SummaryStore()
|
| 321 |
-
mongodb_summary = summary_store.get_summary(grant_id, summary_type)
|
| 322 |
-
|
| 323 |
-
if mongodb_summary:
|
| 324 |
-
logging.info(f"📦 MongoDB HIT for grant {grant_id} ({summary_type})")
|
| 325 |
-
return {
|
| 326 |
-
"summary_md": mongodb_summary,
|
| 327 |
-
"title": title,
|
| 328 |
-
"id": grant_id,
|
| 329 |
-
}
|
| 330 |
-
except Exception as e:
|
| 331 |
-
logging.warning(f"MongoDB lookup failed for {grant_id}: {e}")
|
| 332 |
-
# Continue to memory cache/LLM fallback
|
| 333 |
-
|
| 334 |
-
# Check memory cache
|
| 335 |
cached_summary = self.summary_cache.get(row)
|
| 336 |
if cached_summary:
|
| 337 |
-
logging.info("📦
|
| 338 |
return {
|
| 339 |
"summary_md": cached_summary,
|
| 340 |
"title": title,
|
|
@@ -372,9 +452,6 @@ class ChatTools:
|
|
| 372 |
# NEW: Cache the summary
|
| 373 |
self.summary_cache.set(row, text)
|
| 374 |
|
| 375 |
-
# Log cache stats
|
| 376 |
-
self._log_cache_stats()
|
| 377 |
-
|
| 378 |
return {"summary_md": text, "title": title, "id": grant_id}
|
| 379 |
|
| 380 |
# -----------------------------------------------------------------
|
|
@@ -405,150 +482,6 @@ class ChatTools:
|
|
| 405 |
parts.append(f"{k.upper()}:\n{_norm(v)}")
|
| 406 |
return "\n".join(parts)
|
| 407 |
|
| 408 |
-
# -----------------------------------------------------------------
|
| 409 |
-
# Batch process multiple grants
|
| 410 |
-
# -----------------------------------------------------------------
|
| 411 |
-
def batch_process_grants(
|
| 412 |
-
self,
|
| 413 |
-
grant_ids: List[str],
|
| 414 |
-
operation_type: str = "summarize",
|
| 415 |
-
batch_size: int = 5
|
| 416 |
-
) -> Dict[str, str]:
|
| 417 |
-
"""
|
| 418 |
-
Batch process multiple grants with a single LLM call per batch.
|
| 419 |
-
|
| 420 |
-
This method groups grants into batches and sends them as a single prompt
|
| 421 |
-
with numbered sections, then parses the response to extract individual results.
|
| 422 |
-
|
| 423 |
-
Args:
|
| 424 |
-
grant_ids: List of grant IDs to process
|
| 425 |
-
operation_type: Type of operation - "summarize", "translate", "simplify", etc.
|
| 426 |
-
batch_size: Number of grants per batch (default 5)
|
| 427 |
-
|
| 428 |
-
Returns:
|
| 429 |
-
Dictionary mapping grant_id to result text
|
| 430 |
-
|
| 431 |
-
Example:
|
| 432 |
-
results = tools.batch_process_grants(
|
| 433 |
-
["competition-2315", "competition-2316"],
|
| 434 |
-
operation_type="summarize"
|
| 435 |
-
)
|
| 436 |
-
# Returns: {"competition-2315": "summary text...", "competition-2316": "..."}
|
| 437 |
-
"""
|
| 438 |
-
import hashlib
|
| 439 |
-
|
| 440 |
-
if not grant_ids:
|
| 441 |
-
return {}
|
| 442 |
-
|
| 443 |
-
if not self.client or not self.client.is_ready():
|
| 444 |
-
logging.warning("LLM client not available for batch processing")
|
| 445 |
-
return {gid: "LLM unavailable" for gid in grant_ids}
|
| 446 |
-
|
| 447 |
-
# Build operation-specific instruction
|
| 448 |
-
operation_instructions = {
|
| 449 |
-
"summarize": "Provide a concise summary highlighting key information, deadlines, and funding details.",
|
| 450 |
-
"translate": "Translate the grant information into simple, everyday language that anyone can understand.",
|
| 451 |
-
"simplify": "Explain this grant in layman's terms, avoiding technical jargon.",
|
| 452 |
-
"analyze": "Analyze this grant's strengths, requirements, and suitability for different applicants.",
|
| 453 |
-
}
|
| 454 |
-
instruction = operation_instructions.get(operation_type, "Process this grant information.")
|
| 455 |
-
|
| 456 |
-
results = {}
|
| 457 |
-
|
| 458 |
-
# Process grants in batches
|
| 459 |
-
for batch_start in range(0, len(grant_ids), batch_size):
|
| 460 |
-
batch_ids = grant_ids[batch_start:batch_start + batch_size]
|
| 461 |
-
|
| 462 |
-
# Build batch prompt with numbered sections
|
| 463 |
-
prompt_parts = [
|
| 464 |
-
f"Process the following {len(batch_ids)} grants. {instruction}",
|
| 465 |
-
"\nFor each grant, start your response with '### Grant N:' where N is the grant number.",
|
| 466 |
-
"\n---\n"
|
| 467 |
-
]
|
| 468 |
-
|
| 469 |
-
# Add each grant with its context
|
| 470 |
-
grant_contexts = []
|
| 471 |
-
for idx, grant_id in enumerate(batch_ids, 1):
|
| 472 |
-
try:
|
| 473 |
-
grant = self.get_grant(grant_id)
|
| 474 |
-
# Use extract_minimal_context for efficiency
|
| 475 |
-
from ..summarizer_optimized import extract_minimal_context
|
| 476 |
-
context = extract_minimal_context(grant)
|
| 477 |
-
|
| 478 |
-
prompt_parts.append(f"### Grant {idx}:")
|
| 479 |
-
prompt_parts.append(f"ID: {grant_id}")
|
| 480 |
-
prompt_parts.append(context)
|
| 481 |
-
prompt_parts.append("\n---\n")
|
| 482 |
-
|
| 483 |
-
grant_contexts.append((idx, grant_id))
|
| 484 |
-
|
| 485 |
-
except Exception as e:
|
| 486 |
-
logging.error(f"Failed to load grant {grant_id}: {e}")
|
| 487 |
-
results[grant_id] = f"Error loading grant: {e}"
|
| 488 |
-
|
| 489 |
-
if not grant_contexts:
|
| 490 |
-
continue
|
| 491 |
-
|
| 492 |
-
# Build final prompt
|
| 493 |
-
full_prompt = "\n".join(prompt_parts)
|
| 494 |
-
|
| 495 |
-
# Check cache for batch
|
| 496 |
-
cache_key = hashlib.md5(full_prompt.encode()).hexdigest()[:12]
|
| 497 |
-
cache_dict = {"id": f"batch_{operation_type}_{cache_key}"}
|
| 498 |
-
cached_response = self.summary_cache.get(cache_dict)
|
| 499 |
-
|
| 500 |
-
if cached_response:
|
| 501 |
-
logging.info(f"📦 Cache HIT for batch {cache_key}")
|
| 502 |
-
response_text = cached_response
|
| 503 |
-
else:
|
| 504 |
-
# Call LLM with batch prompt
|
| 505 |
-
try:
|
| 506 |
-
messages = [
|
| 507 |
-
{"role": "system", "content": "You are a grant analyst. Process each grant separately and clearly mark each response with the grant number."},
|
| 508 |
-
{"role": "user", "content": full_prompt}
|
| 509 |
-
]
|
| 510 |
-
response_text = self.client.chat(
|
| 511 |
-
messages,
|
| 512 |
-
max_tokens=batch_size * 400, # ~400 tokens per grant
|
| 513 |
-
temperature=0.3
|
| 514 |
-
)
|
| 515 |
-
|
| 516 |
-
# Cache the response
|
| 517 |
-
self.summary_cache.set(cache_dict, response_text)
|
| 518 |
-
logging.info(f"✅ Batch processed {len(batch_ids)} grants")
|
| 519 |
-
|
| 520 |
-
except Exception as e:
|
| 521 |
-
logging.error(f"Batch processing failed: {e}")
|
| 522 |
-
for _, grant_id in grant_contexts:
|
| 523 |
-
results[grant_id] = f"Batch processing error: {e}"
|
| 524 |
-
continue
|
| 525 |
-
|
| 526 |
-
# Parse response to extract individual results
|
| 527 |
-
# Use regex to split by "### Grant N:" markers
|
| 528 |
-
import re
|
| 529 |
-
pattern = r'### Grant (\d+):(.*?)(?=### Grant \d+:|$)'
|
| 530 |
-
matches = re.findall(pattern, response_text, re.DOTALL)
|
| 531 |
-
|
| 532 |
-
# Map results back to grant IDs
|
| 533 |
-
for grant_num, result_text in matches:
|
| 534 |
-
grant_idx = int(grant_num)
|
| 535 |
-
# Find corresponding grant_id
|
| 536 |
-
for idx, grant_id in grant_contexts:
|
| 537 |
-
if idx == grant_idx:
|
| 538 |
-
results[grant_id] = result_text.strip()
|
| 539 |
-
break
|
| 540 |
-
|
| 541 |
-
# Handle any grants that didn't get matched
|
| 542 |
-
for idx, grant_id in grant_contexts:
|
| 543 |
-
if grant_id not in results:
|
| 544 |
-
logging.warning(f"No result found for grant {grant_id} (index {idx})")
|
| 545 |
-
results[grant_id] = "No response generated"
|
| 546 |
-
|
| 547 |
-
# Log cache stats
|
| 548 |
-
self._log_cache_stats()
|
| 549 |
-
|
| 550 |
-
return results
|
| 551 |
-
|
| 552 |
# -----------------------------------------------------------------
|
| 553 |
# Compare two grants (deterministic)
|
| 554 |
# -----------------------------------------------------------------
|
|
@@ -617,30 +550,18 @@ class ChatTools:
|
|
| 617 |
|
| 618 |
insight = ""
|
| 619 |
if self.client and self.client.is_ready() and context_text.strip():
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
"highlighting *meaningful differences* that matter to SMEs (funding size, "
|
| 633 |
-
"duration, eligibility, etc.). Do not restate identical facts.\n\n"
|
| 634 |
-
+ "\n".join(table)
|
| 635 |
-
+ "\n\n"
|
| 636 |
-
+ context_text
|
| 637 |
-
)
|
| 638 |
-
insight = self.client.summarize(prompt)
|
| 639 |
-
# Cache the insight
|
| 640 |
-
self.summary_cache.set(cache_key, insight)
|
| 641 |
-
logging.info("✅ Generated and cached comparison insight")
|
| 642 |
-
except Exception as e:
|
| 643 |
-
logging.warning("compare_grants insight failed: %s", e)
|
| 644 |
|
| 645 |
md = [
|
| 646 |
"### Comparison",
|
|
@@ -650,9 +571,6 @@ class ChatTools:
|
|
| 650 |
if insight:
|
| 651 |
md += ["\n### Key differences", insight]
|
| 652 |
|
| 653 |
-
# Log cache stats
|
| 654 |
-
self._log_cache_stats()
|
| 655 |
-
|
| 656 |
return {"comparison_md": "\n".join(md)}
|
| 657 |
|
| 658 |
# -----------------------------------------------------------------
|
|
@@ -818,22 +736,8 @@ RECOMMENDED GRANTS:
|
|
| 818 |
- Why: [1-2 sentence explanation of fit]
|
| 819 |
"""
|
| 820 |
|
| 821 |
-
#
|
| 822 |
-
|
| 823 |
-
url_hash = hashlib.md5(company_url.encode()).hexdigest()[:12]
|
| 824 |
-
cache_key = {"id": f"company_{url_hash}"}
|
| 825 |
-
|
| 826 |
-
# Check cache first
|
| 827 |
-
cached_analysis = self.summary_cache.get(cache_key)
|
| 828 |
-
if cached_analysis:
|
| 829 |
-
logging.info("📦 Cache HIT for company analysis: %s", company_url)
|
| 830 |
-
analysis_text = cached_analysis
|
| 831 |
-
else:
|
| 832 |
-
# Get LLM analysis
|
| 833 |
-
analysis_text = self.client.summarize(prompt)
|
| 834 |
-
# Cache the analysis
|
| 835 |
-
self.summary_cache.set(cache_key, analysis_text)
|
| 836 |
-
logging.info("✅ Generated and cached company analysis")
|
| 837 |
|
| 838 |
# Extract recommended grant IDs from the response
|
| 839 |
recommended_ids = []
|
|
@@ -861,9 +765,6 @@ RECOMMENDED GRANTS:
|
|
| 861 |
except KeyError:
|
| 862 |
continue
|
| 863 |
|
| 864 |
-
# Log cache stats
|
| 865 |
-
self._log_cache_stats()
|
| 866 |
-
|
| 867 |
return {
|
| 868 |
"company_url": company_url,
|
| 869 |
"analysis": analysis_text,
|
|
@@ -872,8 +773,6 @@ RECOMMENDED GRANTS:
|
|
| 872 |
|
| 873 |
except Exception as e:
|
| 874 |
logging.error(f"Failed to analyze company: {e}")
|
| 875 |
-
# Log cache stats even on error
|
| 876 |
-
self._log_cache_stats()
|
| 877 |
return {
|
| 878 |
"error": f"Analysis failed: {str(e)}",
|
| 879 |
"company_url": company_url,
|
|
|
|
| 3 |
import asyncio
|
| 4 |
import logging
|
| 5 |
import re
|
| 6 |
+
import difflib
|
| 7 |
from dataclasses import dataclass
|
| 8 |
from datetime import datetime
|
| 9 |
from pathlib import Path
|
|
|
|
| 56 |
return clean(str(s)) # Use utils.text.clean() for final normalization
|
| 57 |
|
| 58 |
|
| 59 |
+
# Fuzzy matching helper - supports partial/typo matches
|
| 60 |
+
def _fuzzy_match(keyword: str, text: str, threshold: float = 0.6) -> bool:
|
| 61 |
+
"""
|
| 62 |
+
Check if keyword matches text using fuzzy matching.
|
| 63 |
+
|
| 64 |
+
Returns True if:
|
| 65 |
+
- Exact substring match (e.g., "ai" in "agentic ai")
|
| 66 |
+
- Fuzzy word match with high similarity (e.g., "agent" vs "agentic" @ 86%)
|
| 67 |
+
- Any word in text starts with the keyword
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
keyword: The search keyword
|
| 71 |
+
text: The text to search in
|
| 72 |
+
threshold: Minimum similarity score (0-1) for fuzzy match
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
True if match found, False otherwise
|
| 76 |
+
"""
|
| 77 |
+
keyword_lower = keyword.lower()
|
| 78 |
+
text_lower = text.lower()
|
| 79 |
+
|
| 80 |
+
# Exact substring match (fastest, most common case)
|
| 81 |
+
if keyword_lower in text_lower:
|
| 82 |
+
return True
|
| 83 |
+
|
| 84 |
+
# Split into words and try fuzzy matching on individual words
|
| 85 |
+
text_words = text_lower.split()
|
| 86 |
+
keyword_words = keyword_lower.split()
|
| 87 |
+
|
| 88 |
+
for kw_word in keyword_words:
|
| 89 |
+
# Check if any text word starts with the keyword word
|
| 90 |
+
for text_word in text_words:
|
| 91 |
+
# Allow leading punctuation in text_word
|
| 92 |
+
clean_text_word = re.sub(r'^[^a-z0-9]+', '', text_word)
|
| 93 |
+
if clean_text_word.startswith(kw_word):
|
| 94 |
+
return True
|
| 95 |
+
|
| 96 |
+
# Fuzzy match single words (e.g., "agent" vs "agentic")
|
| 97 |
+
ratio = difflib.SequenceMatcher(None, kw_word, clean_text_word).ratio()
|
| 98 |
+
if ratio >= threshold:
|
| 99 |
+
return True
|
| 100 |
+
|
| 101 |
+
return False
|
| 102 |
+
|
| 103 |
+
|
| 104 |
# ---------------------------------------------------------------------
|
| 105 |
# Main ChatTools class
|
| 106 |
# ---------------------------------------------------------------------
|
|
|
|
| 132 |
# Initialize cache for summaries (NEW: Optimized caching)
|
| 133 |
self.summary_cache = SummaryCache(ttl_seconds=3600)
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
# -----------------------------------------------------------------
|
| 136 |
# Status calculation (NEW)
|
| 137 |
# -----------------------------------------------------------------
|
|
|
|
| 189 |
results = []
|
| 190 |
for r in self.current:
|
| 191 |
txt = _norm(r)
|
| 192 |
+
# Use fuzzy matching instead of exact substring match
|
| 193 |
+
if kw and not _fuzzy_match(kw, txt):
|
| 194 |
continue
|
| 195 |
if max_award is not None:
|
| 196 |
ma = to_number(r.get("max_award") or r.get("funding_max"))
|
|
|
|
| 219 |
return results
|
| 220 |
return results[:limit]
|
| 221 |
|
| 222 |
+
# -----------------------------------------------------------------
|
| 223 |
+
# Search past winners
|
| 224 |
+
# -----------------------------------------------------------------
|
| 225 |
+
def search_past_winners(
|
| 226 |
+
self,
|
| 227 |
+
keyword: Optional[str] = None,
|
| 228 |
+
competition: Optional[str] = None,
|
| 229 |
+
limit: Optional[int] = None,
|
| 230 |
+
) -> List[Dict[str, Any]]:
|
| 231 |
+
"""
|
| 232 |
+
Search past winners by keyword or competition name.
|
| 233 |
+
|
| 234 |
+
Args:
|
| 235 |
+
keyword: Filter by keyword in project title, organization, or description
|
| 236 |
+
competition: Filter by competition/programme name
|
| 237 |
+
limit: Max results to return. If None, returns ALL matching winners.
|
| 238 |
+
|
| 239 |
+
Returns:
|
| 240 |
+
List of past winner records sorted by year (newest first)
|
| 241 |
+
"""
|
| 242 |
+
kw = (keyword or "").lower()
|
| 243 |
+
comp = (competition or "").lower()
|
| 244 |
+
results = []
|
| 245 |
+
|
| 246 |
+
for r in self.past:
|
| 247 |
+
# Filter by keyword (searches project title, org name, and description)
|
| 248 |
+
if kw:
|
| 249 |
+
searchable_text = _norm({
|
| 250 |
+
"project_title": r.get("project_title"),
|
| 251 |
+
"lead_org": r.get("lead_org"),
|
| 252 |
+
"participant_name": r.get("participant_name"),
|
| 253 |
+
"public_description": r.get("public_description"),
|
| 254 |
+
"abstract": r.get("abstract"),
|
| 255 |
+
})
|
| 256 |
+
# Use fuzzy matching instead of exact substring match
|
| 257 |
+
if not _fuzzy_match(kw, searchable_text):
|
| 258 |
+
continue
|
| 259 |
+
|
| 260 |
+
# Filter by competition name
|
| 261 |
+
if comp:
|
| 262 |
+
comp_text = _norm({
|
| 263 |
+
"competition": r.get("competition"),
|
| 264 |
+
"competition_title": r.get("competition_title"),
|
| 265 |
+
"programme_title": r.get("programme_title"),
|
| 266 |
+
})
|
| 267 |
+
# Use fuzzy matching instead of exact substring match
|
| 268 |
+
if not _fuzzy_match(comp, comp_text):
|
| 269 |
+
continue
|
| 270 |
+
|
| 271 |
+
# Extract key fields for display
|
| 272 |
+
results.append({
|
| 273 |
+
"project_title": r.get("project_title") or "(untitled)",
|
| 274 |
+
"lead_org": r.get("lead_org") or r.get("participant_name") or "(unknown)",
|
| 275 |
+
"competition": r.get("competition_title") or r.get("competition") or "(unknown)",
|
| 276 |
+
"award_amount": r.get("award_amount"),
|
| 277 |
+
"year": r.get("year") or r.get("project_start_date"),
|
| 278 |
+
"abstract": r.get("public_description") or r.get("abstract"),
|
| 279 |
+
})
|
| 280 |
+
|
| 281 |
+
# Sort by year (newest first)
|
| 282 |
+
results.sort(
|
| 283 |
+
key=lambda x: _parse_date(x.get("year")) or datetime.min,
|
| 284 |
+
reverse=True
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
# Return all results if limit is None
|
| 288 |
+
if limit is None:
|
| 289 |
+
return results
|
| 290 |
+
return results[:limit]
|
| 291 |
+
|
| 292 |
# -----------------------------------------------------------------
|
| 293 |
# Retrieve a single grant
|
| 294 |
# -----------------------------------------------------------------
|
|
|
|
| 360 |
# Yield each result as it's ready
|
| 361 |
for result in results:
|
| 362 |
yield result
|
|
|
|
|
|
|
|
|
|
| 363 |
except Exception as e:
|
| 364 |
logging.error(f"Batch summarization failed: {e}")
|
|
|
|
|
|
|
| 365 |
raise
|
| 366 |
|
| 367 |
async def get_all_grant_summaries(self, batch_size: int = 5):
|
|
|
|
| 393 |
async for result in self.summarize_grants_batch(all_grant_ids, batch_size=batch_size):
|
| 394 |
yield result
|
| 395 |
|
|
|
|
|
|
|
|
|
|
| 396 |
# -----------------------------------------------------------------
|
| 397 |
# Summarize a grant
|
| 398 |
# -----------------------------------------------------------------
|
| 399 |
+
def summarize_grant(self, gid: str, include_supporting: bool = True) -> Dict[str, Any]:
|
| 400 |
"""
|
| 401 |
+
Summarize a grant using LLM with caching.
|
| 402 |
|
| 403 |
Args:
|
| 404 |
gid: Grant ID
|
| 405 |
include_supporting: If True, include supporting PDFs and materials in context
|
|
|
|
| 406 |
|
| 407 |
Returns:
|
| 408 |
Dict with summary_md, title, id
|
|
|
|
| 411 |
title = row.get("title", "(untitled)")
|
| 412 |
grant_id = row.get("id") or gid
|
| 413 |
|
| 414 |
+
# NEW: Check cache first
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
cached_summary = self.summary_cache.get(row)
|
| 416 |
if cached_summary:
|
| 417 |
+
logging.info("📦 Cache HIT for grant %s", grant_id)
|
| 418 |
return {
|
| 419 |
"summary_md": cached_summary,
|
| 420 |
"title": title,
|
|
|
|
| 452 |
# NEW: Cache the summary
|
| 453 |
self.summary_cache.set(row, text)
|
| 454 |
|
|
|
|
|
|
|
|
|
|
| 455 |
return {"summary_md": text, "title": title, "id": grant_id}
|
| 456 |
|
| 457 |
# -----------------------------------------------------------------
|
|
|
|
| 482 |
parts.append(f"{k.upper()}:\n{_norm(v)}")
|
| 483 |
return "\n".join(parts)
|
| 484 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
# -----------------------------------------------------------------
|
| 486 |
# Compare two grants (deterministic)
|
| 487 |
# -----------------------------------------------------------------
|
|
|
|
| 550 |
|
| 551 |
insight = ""
|
| 552 |
if self.client and self.client.is_ready() and context_text.strip():
|
| 553 |
+
try:
|
| 554 |
+
prompt = (
|
| 555 |
+
"Given the factual table and context below, write 3-5 bullet points "
|
| 556 |
+
"highlighting *meaningful differences* that matter to SMEs (funding size, "
|
| 557 |
+
"duration, eligibility, etc.). Do not restate identical facts.\n\n"
|
| 558 |
+
+ "\n".join(table)
|
| 559 |
+
+ "\n\n"
|
| 560 |
+
+ context_text
|
| 561 |
+
)
|
| 562 |
+
insight = self.client.summarize(prompt)
|
| 563 |
+
except Exception as e:
|
| 564 |
+
logging.warning("compare_grants insight failed: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
|
| 566 |
md = [
|
| 567 |
"### Comparison",
|
|
|
|
| 571 |
if insight:
|
| 572 |
md += ["\n### Key differences", insight]
|
| 573 |
|
|
|
|
|
|
|
|
|
|
| 574 |
return {"comparison_md": "\n".join(md)}
|
| 575 |
|
| 576 |
# -----------------------------------------------------------------
|
|
|
|
| 736 |
- Why: [1-2 sentence explanation of fit]
|
| 737 |
"""
|
| 738 |
|
| 739 |
+
# Get LLM analysis
|
| 740 |
+
analysis_text = self.client.summarize(prompt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
|
| 742 |
# Extract recommended grant IDs from the response
|
| 743 |
recommended_ids = []
|
|
|
|
| 765 |
except KeyError:
|
| 766 |
continue
|
| 767 |
|
|
|
|
|
|
|
|
|
|
| 768 |
return {
|
| 769 |
"company_url": company_url,
|
| 770 |
"analysis": analysis_text,
|
|
|
|
| 773 |
|
| 774 |
except Exception as e:
|
| 775 |
logging.error(f"Failed to analyze company: {e}")
|
|
|
|
|
|
|
| 776 |
return {
|
| 777 |
"error": f"Analysis failed: {str(e)}",
|
| 778 |
"company_url": company_url,
|
analyzer/chat/demo_app.py
CHANGED
|
@@ -23,13 +23,6 @@ except ImportError:
|
|
| 23 |
print("ERROR: Gradio not installed. Install with: pip install gradio")
|
| 24 |
sys.exit(1)
|
| 25 |
|
| 26 |
-
try:
|
| 27 |
-
import httpx
|
| 28 |
-
except ImportError:
|
| 29 |
-
print("ERROR: httpx not installed. Install with: pip install httpx")
|
| 30 |
-
httpx = None
|
| 31 |
-
|
| 32 |
-
|
| 33 |
from ..config import load_config
|
| 34 |
from ..data_loader import load_current_grants, load_past_winners
|
| 35 |
from ..llm_client import LLMClient
|
|
@@ -47,7 +40,9 @@ PRESET_QUESTIONS = {
|
|
| 47 |
"Show upcoming deadlines": "What are the upcoming grant deadlines?",
|
| 48 |
"Compare two grants": "Compare competition-2313 and competition-2314",
|
| 49 |
"Grant details": "Tell me about competition-2317 in detail",
|
| 50 |
-
"SME funding options": "What grants are available for SMEs with funding over £100k?"
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
|
|
@@ -92,7 +87,7 @@ class GrantAnalystDemo:
|
|
| 92 |
# Initialize LLM
|
| 93 |
self.llm_client = LLMClient(self.cfg)
|
| 94 |
if not self.llm_client.is_ready():
|
| 95 |
-
return False, "
|
| 96 |
|
| 97 |
# Load index
|
| 98 |
try:
|
|
@@ -122,38 +117,50 @@ class GrantAnalystDemo:
|
|
| 122 |
{
|
| 123 |
"role": "system",
|
| 124 |
"content": (
|
| 125 |
-
"You are an expert UK grant analyst assistant. Your PRIMARY DIRECTIVE is to
|
| 126 |
-
"
|
| 127 |
-
"-
|
| 128 |
-
"-
|
| 129 |
-
"-
|
| 130 |
-
"-
|
| 131 |
-
"-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
"CRITICAL RULES:\n"
|
| 133 |
-
"-
|
| 134 |
-
"-
|
| 135 |
-
"-
|
| 136 |
-
"-
|
| 137 |
-
"-
|
| 138 |
-
"-
|
| 139 |
-
"
|
| 140 |
-
"
|
| 141 |
-
"-
|
| 142 |
-
"-
|
| 143 |
-
"- summarize_grant:
|
| 144 |
-
"-
|
| 145 |
-
"-
|
| 146 |
-
"-
|
| 147 |
-
"-
|
|
|
|
| 148 |
"RESPONSE FORMAT:\n"
|
| 149 |
-
"-
|
| 150 |
-
"-
|
| 151 |
-
"-
|
| 152 |
-
"-
|
| 153 |
-
"
|
| 154 |
-
"-
|
| 155 |
-
"-
|
| 156 |
-
"
|
|
|
|
|
|
|
|
|
|
| 157 |
)
|
| 158 |
}
|
| 159 |
]
|
|
@@ -189,9 +196,9 @@ class GrantAnalystDemo:
|
|
| 189 |
batch_size = tool_args.get("batch_size", 5)
|
| 190 |
|
| 191 |
if not grant_ids:
|
| 192 |
-
return "
|
| 193 |
|
| 194 |
-
logging.info(f"Starting batch summarization of {len(grant_ids)} grants")
|
| 195 |
|
| 196 |
# Collect results from async generator
|
| 197 |
results = []
|
|
@@ -221,7 +228,7 @@ class GrantAnalystDemo:
|
|
| 221 |
|
| 222 |
# Format results for display
|
| 223 |
if not results:
|
| 224 |
-
return "
|
| 225 |
|
| 226 |
formatted = f"Batch summarization complete for {len(results)} grants:\n\n"
|
| 227 |
for i, result in enumerate(results, 1):
|
|
@@ -238,7 +245,7 @@ class GrantAnalystDemo:
|
|
| 238 |
# Get summaries of ALL grants in one batch
|
| 239 |
batch_size = tool_args.get("batch_size", 5)
|
| 240 |
|
| 241 |
-
logging.info(f"Starting to get summaries for ALL grants (batch_size={batch_size})")
|
| 242 |
|
| 243 |
# Collect results from async generator
|
| 244 |
results = []
|
|
@@ -263,7 +270,7 @@ class GrantAnalystDemo:
|
|
| 263 |
|
| 264 |
# Format results for display
|
| 265 |
if not results:
|
| 266 |
-
return "
|
| 267 |
|
| 268 |
formatted = f"Summaries for ALL {len(results)} grants:\n\n"
|
| 269 |
for i, result in enumerate(results, 1):
|
|
@@ -309,6 +316,37 @@ class GrantAnalystDemo:
|
|
| 309 |
])
|
| 310 |
return formatted
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
elif tool_name == "fetch_link":
|
| 313 |
# NEW: Handle external link fetching
|
| 314 |
try:
|
|
@@ -340,168 +378,9 @@ class GrantAnalystDemo:
|
|
| 340 |
logging.error(f"Tool {tool_name} failed: {e}", exc_info=True)
|
| 341 |
return {"error": str(e)}
|
| 342 |
|
| 343 |
-
def chat_stream(self, user_message: str, history: List, use_sse: bool = False, use_websocket: bool = False):
|
| 344 |
-
"""
|
| 345 |
-
Process a chat message with optional SSE or WebSocket streaming.
|
| 346 |
-
|
| 347 |
-
Args:
|
| 348 |
-
user_message: User's input
|
| 349 |
-
history: Gradio chat history
|
| 350 |
-
use_sse: If True, use SSE streaming from API endpoint
|
| 351 |
-
use_websocket: If True, use WebSocket streaming (takes precedence over SSE)
|
| 352 |
-
|
| 353 |
-
Yields:
|
| 354 |
-
Updated history for streaming response
|
| 355 |
-
"""
|
| 356 |
-
import time
|
| 357 |
-
import json
|
| 358 |
-
|
| 359 |
-
if not self.initialized:
|
| 360 |
-
yield history + [[user_message, "WARNING: System not initialized. Please restart the app."]]
|
| 361 |
-
return
|
| 362 |
-
|
| 363 |
-
# WebSocket streaming takes precedence
|
| 364 |
-
if use_websocket and HAS_WEBSOCKET:
|
| 365 |
-
try:
|
| 366 |
-
accumulated_response = ""
|
| 367 |
-
intent = None
|
| 368 |
-
citations = []
|
| 369 |
-
|
| 370 |
-
# Connect to WebSocket endpoint
|
| 371 |
-
ws = websocket.create_connection("ws://localhost:8000/ws/query", timeout=60)
|
| 372 |
-
|
| 373 |
-
# Send query
|
| 374 |
-
message = {
|
| 375 |
-
"query": user_message,
|
| 376 |
-
"session_id": "gradio_session"
|
| 377 |
-
}
|
| 378 |
-
ws.send(json.dumps(message))
|
| 379 |
-
|
| 380 |
-
# Receive and process stream
|
| 381 |
-
while True:
|
| 382 |
-
try:
|
| 383 |
-
msg = ws.recv()
|
| 384 |
-
data = json.loads(msg)
|
| 385 |
-
msg_type = data.get("type")
|
| 386 |
-
|
| 387 |
-
if msg_type == "metadata":
|
| 388 |
-
# Initial metadata received
|
| 389 |
-
logging.info(f"WebSocket session: {data.get('session_id')}")
|
| 390 |
-
|
| 391 |
-
elif msg_type == "intent":
|
| 392 |
-
intent = data.get("intent")
|
| 393 |
-
# Show intent in response
|
| 394 |
-
status_msg = f"*Detected intent: {intent}*\n\n"
|
| 395 |
-
yield history + [[user_message, status_msg]]
|
| 396 |
-
|
| 397 |
-
elif msg_type == "token":
|
| 398 |
-
# Stream token to UI
|
| 399 |
-
token = data.get("token", "")
|
| 400 |
-
accumulated_response += token
|
| 401 |
-
# Yield updated history with partial response
|
| 402 |
-
status_prefix = f"*Intent: {intent}*\n\n" if intent else ""
|
| 403 |
-
yield history + [[user_message, status_prefix + accumulated_response]]
|
| 404 |
-
|
| 405 |
-
elif msg_type == "citations":
|
| 406 |
-
citations = data.get("citations", [])
|
| 407 |
-
|
| 408 |
-
elif msg_type == "done":
|
| 409 |
-
latency_ms = data.get("latency_ms")
|
| 410 |
-
logging.info(f"WebSocket stream completed in {latency_ms}ms")
|
| 411 |
-
break
|
| 412 |
-
|
| 413 |
-
elif msg_type == "error":
|
| 414 |
-
error_msg = data.get("error", "Unknown error")
|
| 415 |
-
yield history + [[user_message, f"ERROR: Error: {error_msg}"]]
|
| 416 |
-
ws.close()
|
| 417 |
-
return
|
| 418 |
-
|
| 419 |
-
except websocket.WebSocketTimeoutException:
|
| 420 |
-
logging.warning("WebSocket timeout")
|
| 421 |
-
break
|
| 422 |
-
except json.JSONDecodeError as e:
|
| 423 |
-
logging.error(f"Failed to parse WebSocket message: {e}")
|
| 424 |
-
continue
|
| 425 |
-
|
| 426 |
-
ws.close()
|
| 427 |
-
|
| 428 |
-
# Add citations to final response
|
| 429 |
-
if citations:
|
| 430 |
-
accumulated_response += "\n\n**Citations:**\n"
|
| 431 |
-
for cite in citations:
|
| 432 |
-
title = cite.get("title", "Unknown")
|
| 433 |
-
grant_id = cite.get("grant_id", "N/A")
|
| 434 |
-
accumulated_response += f"- **{title}** (ID: {grant_id})\n"
|
| 435 |
-
|
| 436 |
-
# Add intent badge if available
|
| 437 |
-
if intent:
|
| 438 |
-
final_response = f"*Intent: {intent}*\n\n{accumulated_response}"
|
| 439 |
-
else:
|
| 440 |
-
final_response = accumulated_response
|
| 441 |
-
|
| 442 |
-
yield history + [[user_message, final_response]]
|
| 443 |
-
|
| 444 |
-
except Exception as e:
|
| 445 |
-
logging.error(f"WebSocket streaming error: {e}")
|
| 446 |
-
yield history + [[user_message, f"ERROR: WebSocket error: {e}"]]
|
| 447 |
-
return
|
| 448 |
-
|
| 449 |
-
if use_sse and httpx:
|
| 450 |
-
# Use SSE streaming from API endpoint
|
| 451 |
-
try:
|
| 452 |
-
accumulated_response = ""
|
| 453 |
-
citations = []
|
| 454 |
-
|
| 455 |
-
with httpx.Client(timeout=60.0) as client:
|
| 456 |
-
with client.stream(
|
| 457 |
-
"POST",
|
| 458 |
-
"http://localhost:8000/qa/stream", # Adjust URL as needed
|
| 459 |
-
json={"query": user_message, "use_llm_routing": True}
|
| 460 |
-
) as response:
|
| 461 |
-
for line in response.iter_lines():
|
| 462 |
-
if line.startswith("data: "):
|
| 463 |
-
data_str = line[6:] # Remove "data: " prefix
|
| 464 |
-
try:
|
| 465 |
-
data = json.loads(data_str)
|
| 466 |
-
event_type = data.get("type")
|
| 467 |
-
|
| 468 |
-
if event_type == "token":
|
| 469 |
-
token = data.get("token", "")
|
| 470 |
-
accumulated_response += token
|
| 471 |
-
# Yield updated history with partial response
|
| 472 |
-
yield history + [[user_message, accumulated_response]]
|
| 473 |
-
|
| 474 |
-
elif event_type == "citations":
|
| 475 |
-
citations = data.get("citations", [])
|
| 476 |
-
|
| 477 |
-
elif event_type == "error":
|
| 478 |
-
error = data.get("error", "Unknown error")
|
| 479 |
-
yield history + [[user_message, f"ERROR: Error: {error}"]]
|
| 480 |
-
return
|
| 481 |
-
|
| 482 |
-
except json.JSONDecodeError:
|
| 483 |
-
continue
|
| 484 |
-
|
| 485 |
-
# Add citations to final response
|
| 486 |
-
if citations:
|
| 487 |
-
accumulated_response += "\n\n**Citations:**\n"
|
| 488 |
-
for cite in citations:
|
| 489 |
-
accumulated_response += f"- {cite.get('title')} (ID: {cite.get('grant_id')})\n"
|
| 490 |
-
|
| 491 |
-
yield history + [[user_message, accumulated_response]]
|
| 492 |
-
|
| 493 |
-
except Exception as e:
|
| 494 |
-
logging.error(f"SSE streaming error: {e}")
|
| 495 |
-
yield history + [[user_message, f"ERROR: Streaming error: {e}"]]
|
| 496 |
-
return
|
| 497 |
-
|
| 498 |
-
# Fallback to original non-streaming chat
|
| 499 |
-
response, updated_history = self.chat(user_message, history)
|
| 500 |
-
yield updated_history
|
| 501 |
-
|
| 502 |
def chat(self, user_message: str, history: List) -> Tuple[str, List]:
|
| 503 |
"""
|
| 504 |
-
Process a chat message
|
| 505 |
|
| 506 |
Args:
|
| 507 |
user_message: User's input
|
|
@@ -513,7 +392,7 @@ class GrantAnalystDemo:
|
|
| 513 |
import time
|
| 514 |
|
| 515 |
if not self.initialized:
|
| 516 |
-
return "
|
| 517 |
|
| 518 |
start_time = time.time()
|
| 519 |
tools_called = []
|
|
@@ -530,7 +409,7 @@ class GrantAnalystDemo:
|
|
| 530 |
messages=self.messages,
|
| 531 |
tools=self.available_tools,
|
| 532 |
tool_choice="auto",
|
| 533 |
-
temperature=0.
|
| 534 |
max_tokens=4096, # INCREASED to allow detailed summaries without truncation
|
| 535 |
)
|
| 536 |
timing_info["llm_call"] = time.time() - llm_start
|
|
@@ -557,7 +436,7 @@ class GrantAnalystDemo:
|
|
| 557 |
tool_start = time.time()
|
| 558 |
tool_result = self._dispatch_tool(function_name, function_args)
|
| 559 |
tool_time = time.time() - tool_start
|
| 560 |
-
logging.info(f"{function_name} took {tool_time:.2f}s")
|
| 561 |
timing_info[f"tool_{function_name}"] = tool_time
|
| 562 |
|
| 563 |
# Add tool result
|
|
@@ -573,7 +452,7 @@ class GrantAnalystDemo:
|
|
| 573 |
final_response = self.llm_client.client.chat.completions.create(
|
| 574 |
model=self.llm_client.model,
|
| 575 |
messages=self.messages,
|
| 576 |
-
temperature=0.
|
| 577 |
max_tokens=4096, # INCREASED to allow complete answers without truncation
|
| 578 |
)
|
| 579 |
timing_info["final_llm_call"] = time.time() - final_start
|
|
@@ -588,7 +467,7 @@ class GrantAnalystDemo:
|
|
| 588 |
# Log timing info
|
| 589 |
response_time_ms = int((time.time() - start_time) * 1000)
|
| 590 |
timing_str = " | ".join([f"{k}:{v:.2f}s" for k, v in timing_info.items()])
|
| 591 |
-
logging.info(f"Total: {response_time_ms}ms | {timing_str}")
|
| 592 |
|
| 593 |
# Direct logging to CSV (simpler, more reliable)
|
| 594 |
try:
|
|
@@ -630,12 +509,12 @@ class GrantAnalystDemo:
|
|
| 630 |
logging.info(f"Query logged to {csv_path}")
|
| 631 |
|
| 632 |
except Exception as log_error:
|
| 633 |
-
logging.error(f"
|
| 634 |
|
| 635 |
return assistant_message, history + [[user_message, assistant_message]]
|
| 636 |
|
| 637 |
except Exception as e:
|
| 638 |
-
error_msg = f"
|
| 639 |
logging.error(f"Chat error: {e}", exc_info=True)
|
| 640 |
|
| 641 |
# Log failed interactions too
|
|
@@ -681,7 +560,7 @@ class GrantAnalystDemo:
|
|
| 681 |
logging.info(f"Failed query logged to {csv_path}")
|
| 682 |
|
| 683 |
except Exception as log_error:
|
| 684 |
-
logging.error(f"
|
| 685 |
|
| 686 |
return error_msg, history + [[user_message, error_msg]]
|
| 687 |
|
|
@@ -693,8 +572,17 @@ def create_demo_ui(demo: GrantAnalystDemo) -> gr.Blocks:
|
|
| 693 |
title="Grant Analyst Demo",
|
| 694 |
theme=gr.themes.Soft(),
|
| 695 |
css="""
|
| 696 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
#status-box { padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
|
|
|
|
|
|
|
| 698 |
"""
|
| 699 |
) as app:
|
| 700 |
|
|
@@ -720,7 +608,7 @@ def create_demo_ui(demo: GrantAnalystDemo) -> gr.Blocks:
|
|
| 720 |
if success:
|
| 721 |
return f"**System Ready** — {msg}"
|
| 722 |
else:
|
| 723 |
-
return f"
|
| 724 |
|
| 725 |
# Chatbot interface
|
| 726 |
with gr.Row():
|
|
@@ -739,8 +627,7 @@ def create_demo_ui(demo: GrantAnalystDemo) -> gr.Blocks:
|
|
| 739 |
)
|
| 740 |
send_btn = gr.Button("Send", scale=1, variant="primary")
|
| 741 |
|
| 742 |
-
|
| 743 |
-
clear_btn = gr.Button("Clear Conversation")
|
| 744 |
|
| 745 |
with gr.Column(scale=1):
|
| 746 |
gr.Markdown("### Preset Questions")
|
|
@@ -778,27 +665,15 @@ def create_demo_ui(demo: GrantAnalystDemo) -> gr.Blocks:
|
|
| 778 |
|
| 779 |
# Event handlers
|
| 780 |
def respond(message, chat_history):
|
| 781 |
-
"""Handle user message."""
|
| 782 |
-
# Use non-streaming response
|
| 783 |
bot_response, updated_history = demo.chat(message, chat_history)
|
| 784 |
-
|
| 785 |
|
| 786 |
def use_preset(question, chat_history):
|
| 787 |
-
|
| 788 |
-
for result in respond(question, chat_history):
|
| 789 |
-
yield result
|
| 790 |
|
| 791 |
# Wire up events
|
| 792 |
-
msg_input.submit(
|
| 793 |
-
|
| 794 |
-
[msg_input, chatbot],
|
| 795 |
-
[msg_input, chatbot]
|
| 796 |
-
)
|
| 797 |
-
send_btn.click(
|
| 798 |
-
respond,
|
| 799 |
-
[msg_input, chatbot],
|
| 800 |
-
[msg_input, chatbot]
|
| 801 |
-
)
|
| 802 |
clear_btn.click(lambda: [], None, chatbot)
|
| 803 |
|
| 804 |
for btn, question in preset_btns:
|
|
@@ -816,12 +691,16 @@ def create_demo_ui(demo: GrantAnalystDemo) -> gr.Blocks:
|
|
| 816 |
|
| 817 |
def main():
|
| 818 |
"""Launch the demo application."""
|
|
|
|
| 819 |
|
| 820 |
parser = argparse.ArgumentParser(description="Grant Analyst Demo App")
|
| 821 |
parser.add_argument("--share", action="store_true", help="Create public shareable link")
|
| 822 |
-
parser.add_argument("--port", type=int, default=
|
| 823 |
args = parser.parse_args()
|
| 824 |
|
|
|
|
|
|
|
|
|
|
| 825 |
# Setup logging (create log directory if needed)
|
| 826 |
log_handlers = [logging.StreamHandler(sys.stderr)]
|
| 827 |
try:
|
|
@@ -845,12 +724,12 @@ def main():
|
|
| 845 |
app = create_demo_ui(demo)
|
| 846 |
|
| 847 |
print("\n" + "="*60)
|
| 848 |
-
print("Launching Grant Analyst Demo...")
|
| 849 |
print("="*60)
|
| 850 |
|
| 851 |
app.launch(
|
| 852 |
server_name="0.0.0.0",
|
| 853 |
-
server_port=
|
| 854 |
share=args.share,
|
| 855 |
show_error=True,
|
| 856 |
)
|
|
|
|
| 23 |
print("ERROR: Gradio not installed. Install with: pip install gradio")
|
| 24 |
sys.exit(1)
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
from ..config import load_config
|
| 27 |
from ..data_loader import load_current_grants, load_past_winners
|
| 28 |
from ..llm_client import LLMClient
|
|
|
|
| 40 |
"Show upcoming deadlines": "What are the upcoming grant deadlines?",
|
| 41 |
"Compare two grants": "Compare competition-2313 and competition-2314",
|
| 42 |
"Grant details": "Tell me about competition-2317 in detail",
|
| 43 |
+
"SME funding options": "What grants are available for SMEs with funding over £100k?",
|
| 44 |
+
"Past winners of open grants": "Of the grants that are currently open, are there any past winners listed as reference?",
|
| 45 |
+
"Past AI winners": "Show me past winners related to AI projects"
|
| 46 |
}
|
| 47 |
|
| 48 |
|
|
|
|
| 87 |
# Initialize LLM
|
| 88 |
self.llm_client = LLMClient(self.cfg)
|
| 89 |
if not self.llm_client.is_ready():
|
| 90 |
+
return False, "❌ LLM client not ready. Check API key configuration."
|
| 91 |
|
| 92 |
# Load index
|
| 93 |
try:
|
|
|
|
| 117 |
{
|
| 118 |
"role": "system",
|
| 119 |
"content": (
|
| 120 |
+
"You are an expert UK grant analyst assistant. Your PRIMARY DIRECTIVE is to PROVIDE COMPLETE, THOROUGH RESPONSES.\n\n"
|
| 121 |
+
"CORE BEHAVIOR:\n"
|
| 122 |
+
"- When user asks about a grant, ALWAYS call tools to get the information\n"
|
| 123 |
+
"- When tools return data, ALWAYS present ALL the data to the user\n"
|
| 124 |
+
"- NEVER say 'I don't have information' without first calling tools\n"
|
| 125 |
+
"- NEVER give partial answers - if more information exists, include it\n"
|
| 126 |
+
"- If user challenges your answer, you likely gave incomplete information - call tools again\n\n"
|
| 127 |
+
"TOOL USAGE - WHEN USER ASKS FOR:\n"
|
| 128 |
+
"- 'tell me about [grant name]' → call search_grants with the grant name, then summarize_grant with the ID\n"
|
| 129 |
+
"- 'biomedical catalyst grant' → call search_grants with query='biomedical catalyst'\n"
|
| 130 |
+
"- 'description/summaries of all/every grant' → call get_all_grant_summaries\n"
|
| 131 |
+
"- 'list all grants' (NO descriptions) → call list_grants with limit=None\n"
|
| 132 |
+
"- 'find grants about [topic]' → call search_grants with query=[topic]\n"
|
| 133 |
+
"- 'who won', 'past winners' → call search_past_winners\n"
|
| 134 |
+
"- 'competition-XXXX details' → call get_grant then summarize_grant\n\n"
|
| 135 |
"CRITICAL RULES:\n"
|
| 136 |
+
"- ALWAYS call tools first, respond second\n"
|
| 137 |
+
"- If tool returns data, present ALL of it - don't truncate or paraphrase\n"
|
| 138 |
+
"- If user asks 'tell me more' or 'is that all', you missed information - call tools again\n"
|
| 139 |
+
"- search_grants finds grants by fuzzy matching - use it liberally\n"
|
| 140 |
+
"- If search finds 0 results, try a simpler/shorter query\n"
|
| 141 |
+
"- NEVER assume a grant doesn't exist - always search first\n\n"
|
| 142 |
+
"AVAILABLE TOOLS:\n"
|
| 143 |
+
"- search_grants: Find grants by keyword/topic (fuzzy matching, searches titles + descriptions)\n"
|
| 144 |
+
"- list_grants: List all grants with optional filters (keyword, status, max_award)\n"
|
| 145 |
+
"- get_grant: Get full structured data for one grant ID\n"
|
| 146 |
+
"- summarize_grant: Get AI-generated summary of one grant\n"
|
| 147 |
+
"- summarize_grants_batch: Get summaries for multiple grants\n"
|
| 148 |
+
"- get_all_grant_summaries: Get summaries for ALL grants at once\n"
|
| 149 |
+
"- compare_grants: Side-by-side comparison of two grants\n"
|
| 150 |
+
"- search_past_winners: Search historical winners (10+ years of data)\n"
|
| 151 |
+
"- deadlines_overview: Show upcoming deadlines\n\n"
|
| 152 |
"RESPONSE FORMAT:\n"
|
| 153 |
+
"- Use markdown: headers (##), numbered lists (1. 2. 3.), bold (**text**)\n"
|
| 154 |
+
"- Include ALL details from tool results: funding amounts, dates, eligibility, scope\n"
|
| 155 |
+
"- Be comprehensive and thorough - no length limits\n"
|
| 156 |
+
"- Present complete information on first response, not piecemeal\n\n"
|
| 157 |
+
"WRITING STYLE:\n"
|
| 158 |
+
"- Write in a tight, conversational voice — confident, warm, and clear\n"
|
| 159 |
+
"- Avoid fluff, filler, or over-formality\n"
|
| 160 |
+
"- Skip headings, bullet points, or 'as an AI' disclaimers\n"
|
| 161 |
+
"- Prefer short, active sentences\n"
|
| 162 |
+
"- Be bold, human, and efficient\n\n"
|
| 163 |
+
"Current date: 2025-11-05"
|
| 164 |
)
|
| 165 |
}
|
| 166 |
]
|
|
|
|
| 196 |
batch_size = tool_args.get("batch_size", 5)
|
| 197 |
|
| 198 |
if not grant_ids:
|
| 199 |
+
return "❌ No grant IDs provided for batch summarization"
|
| 200 |
|
| 201 |
+
logging.info(f"📦 Starting batch summarization of {len(grant_ids)} grants")
|
| 202 |
|
| 203 |
# Collect results from async generator
|
| 204 |
results = []
|
|
|
|
| 228 |
|
| 229 |
# Format results for display
|
| 230 |
if not results:
|
| 231 |
+
return "No grants could be summarized"
|
| 232 |
|
| 233 |
formatted = f"Batch summarization complete for {len(results)} grants:\n\n"
|
| 234 |
for i, result in enumerate(results, 1):
|
|
|
|
| 245 |
# Get summaries of ALL grants in one batch
|
| 246 |
batch_size = tool_args.get("batch_size", 5)
|
| 247 |
|
| 248 |
+
logging.info(f"📦 Starting to get summaries for ALL grants (batch_size={batch_size})")
|
| 249 |
|
| 250 |
# Collect results from async generator
|
| 251 |
results = []
|
|
|
|
| 270 |
|
| 271 |
# Format results for display
|
| 272 |
if not results:
|
| 273 |
+
return "No grants could be summarized"
|
| 274 |
|
| 275 |
formatted = f"Summaries for ALL {len(results)} grants:\n\n"
|
| 276 |
for i, result in enumerate(results, 1):
|
|
|
|
| 316 |
])
|
| 317 |
return formatted
|
| 318 |
|
| 319 |
+
elif tool_name == "search_past_winners":
|
| 320 |
+
results = self.tools.search_past_winners(
|
| 321 |
+
keyword=tool_args.get("keyword"),
|
| 322 |
+
competition=tool_args.get("competition"),
|
| 323 |
+
limit=tool_args.get("limit") # If None, returns ALL
|
| 324 |
+
)
|
| 325 |
+
# Format results for better conversation flow
|
| 326 |
+
if not results:
|
| 327 |
+
return "No past winners found matching those criteria."
|
| 328 |
+
|
| 329 |
+
if len(results) > 50:
|
| 330 |
+
# If too many, return summary + first 20
|
| 331 |
+
summary = f"Found {len(results)} past winners matching the criteria. Showing first 20:\n\n"
|
| 332 |
+
display = results[:20]
|
| 333 |
+
else:
|
| 334 |
+
summary = f"Found {len(results)} past winner(s):\n\n"
|
| 335 |
+
display = results
|
| 336 |
+
|
| 337 |
+
formatted_items = []
|
| 338 |
+
for i, r in enumerate(display, 1):
|
| 339 |
+
item = f"{i}. **{r['project_title']}**\n"
|
| 340 |
+
item += f" Organization: {r['lead_org']}\n"
|
| 341 |
+
item += f" Competition: {r['competition']}\n"
|
| 342 |
+
if r.get('award_amount'):
|
| 343 |
+
item += f" Award: £{r['award_amount']}\n"
|
| 344 |
+
if r.get('year'):
|
| 345 |
+
item += f" Year: {r['year']}\n"
|
| 346 |
+
formatted_items.append(item)
|
| 347 |
+
|
| 348 |
+
return summary + "\n".join(formatted_items)
|
| 349 |
+
|
| 350 |
elif tool_name == "fetch_link":
|
| 351 |
# NEW: Handle external link fetching
|
| 352 |
try:
|
|
|
|
| 378 |
logging.error(f"Tool {tool_name} failed: {e}", exc_info=True)
|
| 379 |
return {"error": str(e)}
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
def chat(self, user_message: str, history: List) -> Tuple[str, List]:
|
| 382 |
"""
|
| 383 |
+
Process a chat message.
|
| 384 |
|
| 385 |
Args:
|
| 386 |
user_message: User's input
|
|
|
|
| 392 |
import time
|
| 393 |
|
| 394 |
if not self.initialized:
|
| 395 |
+
return "⚠️ System not initialized. Please restart the app.", history
|
| 396 |
|
| 397 |
start_time = time.time()
|
| 398 |
tools_called = []
|
|
|
|
| 409 |
messages=self.messages,
|
| 410 |
tools=self.available_tools,
|
| 411 |
tool_choice="auto",
|
| 412 |
+
temperature=0.2, # Lower temperature for more consistent, thorough responses
|
| 413 |
max_tokens=4096, # INCREASED to allow detailed summaries without truncation
|
| 414 |
)
|
| 415 |
timing_info["llm_call"] = time.time() - llm_start
|
|
|
|
| 436 |
tool_start = time.time()
|
| 437 |
tool_result = self._dispatch_tool(function_name, function_args)
|
| 438 |
tool_time = time.time() - tool_start
|
| 439 |
+
logging.info(f"⏱️ {function_name} took {tool_time:.2f}s")
|
| 440 |
timing_info[f"tool_{function_name}"] = tool_time
|
| 441 |
|
| 442 |
# Add tool result
|
|
|
|
| 452 |
final_response = self.llm_client.client.chat.completions.create(
|
| 453 |
model=self.llm_client.model,
|
| 454 |
messages=self.messages,
|
| 455 |
+
temperature=0.2, # Lower temperature for more complete, consistent responses
|
| 456 |
max_tokens=4096, # INCREASED to allow complete answers without truncation
|
| 457 |
)
|
| 458 |
timing_info["final_llm_call"] = time.time() - final_start
|
|
|
|
| 467 |
# Log timing info
|
| 468 |
response_time_ms = int((time.time() - start_time) * 1000)
|
| 469 |
timing_str = " | ".join([f"{k}:{v:.2f}s" for k, v in timing_info.items()])
|
| 470 |
+
logging.info(f"⏱️ Total: {response_time_ms}ms | {timing_str}")
|
| 471 |
|
| 472 |
# Direct logging to CSV (simpler, more reliable)
|
| 473 |
try:
|
|
|
|
| 509 |
logging.info(f"Query logged to {csv_path}")
|
| 510 |
|
| 511 |
except Exception as log_error:
|
| 512 |
+
logging.error(f"Logging failed: {log_error}", exc_info=True)
|
| 513 |
|
| 514 |
return assistant_message, history + [[user_message, assistant_message]]
|
| 515 |
|
| 516 |
except Exception as e:
|
| 517 |
+
error_msg = f"❌ Error: {e}"
|
| 518 |
logging.error(f"Chat error: {e}", exc_info=True)
|
| 519 |
|
| 520 |
# Log failed interactions too
|
|
|
|
| 560 |
logging.info(f"Failed query logged to {csv_path}")
|
| 561 |
|
| 562 |
except Exception as log_error:
|
| 563 |
+
logging.error(f"Logging failed: {log_error}", exc_info=True)
|
| 564 |
|
| 565 |
return error_msg, history + [[user_message, error_msg]]
|
| 566 |
|
|
|
|
| 572 |
title="Grant Analyst Demo",
|
| 573 |
theme=gr.themes.Soft(),
|
| 574 |
css="""
|
| 575 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 576 |
+
|
| 577 |
+
* {
|
| 578 |
+
font-family: 'Inter', system-ui, -apple-system, sans-serif !important;
|
| 579 |
+
}
|
| 580 |
+
.gradio-container { max-width: 100% !important; padding-left: 0 !important; padding-right: 0 !important; }
|
| 581 |
+
.main { max-width: 100% !important; }
|
| 582 |
+
.contain { max-width: 100% !important; }
|
| 583 |
#status-box { padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
|
| 584 |
+
.chatbot { min-height: 500px; }
|
| 585 |
+
code, pre { font-family: 'Fira Code', 'Consolas', monospace !important; }
|
| 586 |
"""
|
| 587 |
) as app:
|
| 588 |
|
|
|
|
| 608 |
if success:
|
| 609 |
return f"**System Ready** — {msg}"
|
| 610 |
else:
|
| 611 |
+
return f"**Initialization Failed** — {msg}"
|
| 612 |
|
| 613 |
# Chatbot interface
|
| 614 |
with gr.Row():
|
|
|
|
| 627 |
)
|
| 628 |
send_btn = gr.Button("Send", scale=1, variant="primary")
|
| 629 |
|
| 630 |
+
clear_btn = gr.Button("Clear Conversation")
|
|
|
|
| 631 |
|
| 632 |
with gr.Column(scale=1):
|
| 633 |
gr.Markdown("### Preset Questions")
|
|
|
|
| 665 |
|
| 666 |
# Event handlers
|
| 667 |
def respond(message, chat_history):
|
|
|
|
|
|
|
| 668 |
bot_response, updated_history = demo.chat(message, chat_history)
|
| 669 |
+
return "", updated_history
|
| 670 |
|
| 671 |
def use_preset(question, chat_history):
|
| 672 |
+
return respond(question, chat_history)
|
|
|
|
|
|
|
| 673 |
|
| 674 |
# Wire up events
|
| 675 |
+
msg_input.submit(respond, [msg_input, chatbot], [msg_input, chatbot])
|
| 676 |
+
send_btn.click(respond, [msg_input, chatbot], [msg_input, chatbot])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
clear_btn.click(lambda: [], None, chatbot)
|
| 678 |
|
| 679 |
for btn, question in preset_btns:
|
|
|
|
| 691 |
|
| 692 |
def main():
|
| 693 |
"""Launch the demo application."""
|
| 694 |
+
import os
|
| 695 |
|
| 696 |
parser = argparse.ArgumentParser(description="Grant Analyst Demo App")
|
| 697 |
parser.add_argument("--share", action="store_true", help="Create public shareable link")
|
| 698 |
+
parser.add_argument("--port", type=int, default=None, help="Port to run on")
|
| 699 |
args = parser.parse_args()
|
| 700 |
|
| 701 |
+
# Use PORT environment variable if set (for AWS/cloud deployments), otherwise use CLI arg or default
|
| 702 |
+
port = int(os.environ.get("PORT", args.port or 7860))
|
| 703 |
+
|
| 704 |
# Setup logging (create log directory if needed)
|
| 705 |
log_handlers = [logging.StreamHandler(sys.stderr)]
|
| 706 |
try:
|
|
|
|
| 724 |
app = create_demo_ui(demo)
|
| 725 |
|
| 726 |
print("\n" + "="*60)
|
| 727 |
+
print("🚀 Launching Grant Analyst Demo...")
|
| 728 |
print("="*60)
|
| 729 |
|
| 730 |
app.launch(
|
| 731 |
server_name="0.0.0.0",
|
| 732 |
+
server_port=port,
|
| 733 |
share=args.share,
|
| 734 |
show_error=True,
|
| 735 |
)
|
analyzer/chat/tool_schemas.py
CHANGED
|
@@ -92,9 +92,10 @@ def openai_tools(*, extended: bool = False) -> List[Dict[str, Any]]:
|
|
| 92 |
"function": {
|
| 93 |
"name": "list_grants",
|
| 94 |
"description": (
|
| 95 |
-
"List all
|
| 96 |
-
"
|
| 97 |
-
"
|
|
|
|
| 98 |
),
|
| 99 |
"parameters": {
|
| 100 |
"type": "object",
|
|
@@ -125,6 +126,36 @@ def openai_tools(*, extended: bool = False) -> List[Dict[str, Any]]:
|
|
| 125 |
},
|
| 126 |
},
|
| 127 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
{
|
| 129 |
"type": "function",
|
| 130 |
"function": {
|
|
@@ -203,9 +234,10 @@ def openai_tools(*, extended: bool = False) -> List[Dict[str, Any]]:
|
|
| 203 |
"function": {
|
| 204 |
"name": "get_all_grant_summaries",
|
| 205 |
"description": (
|
| 206 |
-
"Get detailed summaries of ALL
|
| 207 |
-
"
|
| 208 |
-
"
|
|
|
|
| 209 |
),
|
| 210 |
"parameters": {
|
| 211 |
"type": "object",
|
|
@@ -394,7 +426,7 @@ def get_tools(provider: str = "openai", *, extended: bool = False) -> List[Dict[
|
|
| 394 |
Example:
|
| 395 |
tools = get_tools("openai", extended=True)
|
| 396 |
response = client.chat.completions.create(
|
| 397 |
-
model="gpt-
|
| 398 |
messages=[...],
|
| 399 |
tools=tools
|
| 400 |
)
|
|
|
|
| 92 |
"function": {
|
| 93 |
"name": "list_grants",
|
| 94 |
"description": (
|
| 95 |
+
"List all grant IDs and titles (no descriptions) with optional filters. "
|
| 96 |
+
"Use this when user asks 'list all grants' or 'show all grants'. "
|
| 97 |
+
"Returns simple list with ID, title, deadline, status ONLY (fast, no summaries). "
|
| 98 |
+
"For full grant descriptions/summaries, use get_all_grant_summaries instead."
|
| 99 |
),
|
| 100 |
"parameters": {
|
| 101 |
"type": "object",
|
|
|
|
| 126 |
},
|
| 127 |
},
|
| 128 |
},
|
| 129 |
+
{
|
| 130 |
+
"type": "function",
|
| 131 |
+
"function": {
|
| 132 |
+
"name": "search_past_winners",
|
| 133 |
+
"description": (
|
| 134 |
+
"Search past winners from 10+ years of UK innovation funding history. "
|
| 135 |
+
"Use this to find previous winners of a grant or search by organization/project name. "
|
| 136 |
+
"Returns past projects with their funding amounts and winning organizations. "
|
| 137 |
+
"If limit is not specified, returns ALL matching winners."
|
| 138 |
+
),
|
| 139 |
+
"parameters": {
|
| 140 |
+
"type": "object",
|
| 141 |
+
"properties": {
|
| 142 |
+
"keyword": {
|
| 143 |
+
"type": "string",
|
| 144 |
+
"description": "Search by project title, organization name, or description."
|
| 145 |
+
},
|
| 146 |
+
"competition": {
|
| 147 |
+
"type": "string",
|
| 148 |
+
"description": "Filter by grant/competition name to find past winners of a specific grant."
|
| 149 |
+
},
|
| 150 |
+
"limit": {
|
| 151 |
+
"type": "integer",
|
| 152 |
+
"description": "Maximum results to return. If omitted, returns all matching winners."
|
| 153 |
+
},
|
| 154 |
+
},
|
| 155 |
+
"required": [],
|
| 156 |
+
},
|
| 157 |
+
},
|
| 158 |
+
},
|
| 159 |
{
|
| 160 |
"type": "function",
|
| 161 |
"function": {
|
|
|
|
| 234 |
"function": {
|
| 235 |
"name": "get_all_grant_summaries",
|
| 236 |
"description": (
|
| 237 |
+
"Get detailed summaries and descriptions of ALL grants. "
|
| 238 |
+
"Use ONLY when user asks 'describe all grants', 'summarize all grants', or wants 'details/descriptions'. "
|
| 239 |
+
"Do NOT use for simple 'list all grants' requests (use list_grants instead). "
|
| 240 |
+
"Returns comprehensive summaries with descriptions and analysis for each grant."
|
| 241 |
),
|
| 242 |
"parameters": {
|
| 243 |
"type": "object",
|
|
|
|
| 426 |
Example:
|
| 427 |
tools = get_tools("openai", extended=True)
|
| 428 |
response = client.chat.completions.create(
|
| 429 |
+
model="gpt-4",
|
| 430 |
messages=[...],
|
| 431 |
tools=tools
|
| 432 |
)
|