Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- app.py +370 -0
- requirements.txt +13 -0
app.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
+
import requests
|
| 4 |
+
from bs4 import BeautifulSoup
|
| 5 |
+
import arxiv
|
| 6 |
+
import json
|
| 7 |
+
import re
|
| 8 |
+
from openai import AsyncOpenAI
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import logging
|
| 11 |
+
from typing import Dict, Any, List
|
| 12 |
+
|
| 13 |
+
# Import with fallback for deployment compatibility
|
| 14 |
+
try:
|
| 15 |
+
from duckduckgo_search import DDGS
|
| 16 |
+
DDGS_AVAILABLE = True
|
| 17 |
+
except ImportError:
|
| 18 |
+
DDGS_AVAILABLE = False
|
| 19 |
+
logging.warning("DuckDuckGo search not available. Market/news scouting will be limited.")
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
import semanticscholar as sch
|
| 23 |
+
SEMANTIC_SCHOLAR_AVAILABLE = True
|
| 24 |
+
except ImportError:
|
| 25 |
+
SEMANTIC_SCHOLAR_AVAILABLE = False
|
| 26 |
+
logging.warning("Semantic Scholar not available. Paper scouting will be limited.")
|
| 27 |
+
|
| 28 |
+
# --- Configuration & Setup ---
|
| 29 |
+
|
| 30 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 31 |
+
|
| 32 |
+
# --- Backend Configuration (Pragmatic Hybrid) ---
|
| 33 |
+
|
| 34 |
+
# 1. Local LLM Client (for fast, simple tasks)
|
| 35 |
+
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1")
|
| 36 |
+
LOCAL_MODEL_ID = os.environ.get("OLLAMA_MODEL", "gemma:2b")
|
| 37 |
+
local_client = AsyncOpenAI(base_url=OLLAMA_BASE_URL, api_key="ollama")
|
| 38 |
+
logging.info(f"Local client configured for model '{LOCAL_MODEL_ID}' at {OLLAMA_BASE_URL}")
|
| 39 |
+
|
| 40 |
+
# 2. Local HF Transformers (free alternative)
|
| 41 |
+
try:
|
| 42 |
+
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
|
| 43 |
+
import torch
|
| 44 |
+
HF_TRANSFORMERS_AVAILABLE = True
|
| 45 |
+
# Use a smaller model that works well on HF Spaces CPU
|
| 46 |
+
LOCAL_MODEL_NAME = "microsoft/DialoGPT-small"
|
| 47 |
+
logging.info("Loading local HuggingFace model for free inference...")
|
| 48 |
+
try:
|
| 49 |
+
tokenizer = AutoTokenizer.from_pretrained(LOCAL_MODEL_NAME)
|
| 50 |
+
model = AutoModelForCausalLM.from_pretrained(LOCAL_MODEL_NAME)
|
| 51 |
+
generator = pipeline("text-generation", model=model, tokenizer=tokenizer, device=-1) # CPU
|
| 52 |
+
logging.info(f"Local model '{LOCAL_MODEL_NAME}' loaded successfully")
|
| 53 |
+
except Exception as e:
|
| 54 |
+
logging.warning(f"Failed to load local model: {e}")
|
| 55 |
+
HF_TRANSFORMERS_AVAILABLE = False
|
| 56 |
+
generator = None
|
| 57 |
+
except ImportError:
|
| 58 |
+
HF_TRANSFORMERS_AVAILABLE = False
|
| 59 |
+
generator = None
|
| 60 |
+
logging.warning("Transformers not available. Using rule-based fallbacks.")
|
| 61 |
+
|
| 62 |
+
# Fallback to API if needed (but we'll avoid this to stay free)
|
| 63 |
+
HF_TOKEN = os.environ.get("HFSecret")
|
| 64 |
+
REMOTE_MODEL_ID = "meta-llama/Llama-2-7b-chat-hf"
|
| 65 |
+
remote_client = None
|
| 66 |
+
if HF_TOKEN:
|
| 67 |
+
remote_client = AsyncOpenAI(base_url="https://api-inference.huggingface.co/v1", api_key=HF_TOKEN)
|
| 68 |
+
logging.info(f"Remote client configured as fallback for model '{REMOTE_MODEL_ID}'")
|
| 69 |
+
else:
|
| 70 |
+
logging.warning("HFSecret not set. Remote client is disabled.")
|
| 71 |
+
|
| 72 |
+
MODEL_TEMP = 0.4
|
| 73 |
+
MAX_TOKENS = 4096
|
| 74 |
+
|
| 75 |
+
# --- Expert Personas & Prompts ---
|
| 76 |
+
|
| 77 |
+
EXPERT_PERSONAS = {
|
| 78 |
+
"distillation_analyst": {
|
| 79 |
+
"name": "RAG Distillation Analyst",
|
| 80 |
+
"persona": "As a research assistant, read the provided raw text and distill it into a structured JSON summary with keys: `key_patents`, `relevant_papers`, `market_signals`.",
|
| 81 |
+
"backend": "remote" # Use remote for HF Spaces compatibility
|
| 82 |
+
},
|
| 83 |
+
"prior_art_analyst": {
|
| 84 |
+
"name": "Prior Art & Novelty Analyst",
|
| 85 |
+
"persona": "As a patent attorney, analyze the distilled briefing to define the 'novelty gap'—the specific, defensible difference a new invention could exploit. Output JSON with one key: `novelty_gap`.",
|
| 86 |
+
"backend": "remote" # Use remote for HF Spaces compatibility
|
| 87 |
+
},
|
| 88 |
+
"technical_synthesist": {
|
| 89 |
+
"name": "Cross-Domain Technical Synthesist",
|
| 90 |
+
"persona": "As a world-class inventor, invent a novel, concrete technical solution to a problem, explicitly targeting a known 'novelty gap'. Propose tangible components and mechanisms. Output JSON with one key: `design_blueprint`.",
|
| 91 |
+
"backend": "remote" # Creative, power-intensive task
|
| 92 |
+
},
|
| 93 |
+
"ip_claim_drafter": {
|
| 94 |
+
"name": "IP Claim Drafter",
|
| 95 |
+
"persona": "As a registered patent agent, draft precise, defensible provisional claims for an invention based on its design blueprint. Output JSON with one key: `provisional_claims`.",
|
| 96 |
+
"backend": "remote" # Creative, power-intensive task
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
ROUTER_PROMPT_TEMPLATE = """As an expert project manager, analyze the problem statement and select the most logical sequence of 2-3 experts to consult from the available list.
|
| 101 |
+
Problem: "{problem_statement}"
|
| 102 |
+
Experts: {expert_list}
|
| 103 |
+
Output a JSON object with a key "selected_experts", a list of expert keys (e.g., ["prior_art_analyst", "technical_synthesist"]).
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
DISTILLATION_PROMPT_TEMPLATE = """Distill the following raw data into a structured JSON summary. Raw Data: --- {raw_data} ---
|
| 107 |
+
Output a JSON with three keys: `key_patents` (a list of strings summarizing patent titles/snippets), `relevant_papers` (a list of strings summarizing paper titles/abstracts), and `market_signals` (a list of strings summarizing news/market context)."""
|
| 108 |
+
|
| 109 |
+
REPORT_WRITER_PERSONA = "You are a chief editor for a tech journal. Synthesize the findings from an invention pipeline into a single, clean Markdown report. Use clear headings and do not add new information."
|
| 110 |
+
REPORT_WRITER_TEMPLATE = """
|
| 111 |
+
### Invention Blueprint: {problem_statement}
|
| 112 |
+
|
| 113 |
+
#### 1. Distilled Intelligence Briefing
|
| 114 |
+
Based on a broad search of patents, papers, and market signals, the key findings are:
|
| 115 |
+
- **Patents:** {key_patents}
|
| 116 |
+
- **Research:** {relevant_papers}
|
| 117 |
+
- **Market Context:** {market_signals}
|
| 118 |
+
|
| 119 |
+
#### 2. Novelty Gap Analysis
|
| 120 |
+
{novelty_gap}
|
| 121 |
+
|
| 122 |
+
#### 3. Proposed Technical Solution
|
| 123 |
+
{design_blueprint_approach}
|
| 124 |
+
|
| 125 |
+
{design_blueprint_specs}
|
| 126 |
+
|
| 127 |
+
#### 4. Draft Provisional IP Claims
|
| 128 |
+
{claims_markdown}
|
| 129 |
+
---
|
| 130 |
+
"""
|
| 131 |
+
|
| 132 |
+
# --- Core Logic & Scouting ---
|
| 133 |
+
|
| 134 |
+
def local_generate(prompt: str, max_length: int = 200) -> str:
|
| 135 |
+
"""Free local text generation using HuggingFace Transformers"""
|
| 136 |
+
if not HF_TRANSFORMERS_AVAILABLE or not generator:
|
| 137 |
+
return "Local generation not available"
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
result = generator(prompt, max_length=max_length, num_return_sequences=1,
|
| 141 |
+
do_sample=True, temperature=0.7, pad_token_id=tokenizer.eos_token_id)
|
| 142 |
+
return result[0]['generated_text']
|
| 143 |
+
except Exception as e:
|
| 144 |
+
return f"Local generation failed: {e}"
|
| 145 |
+
|
| 146 |
+
async def llm_call(prompt: str, persona: str, backend: str, temperature: float = MODEL_TEMP, is_json: bool = True) -> str:
|
| 147 |
+
"""Intelligent switchboard with free local generation priority."""
|
| 148 |
+
|
| 149 |
+
# PRIORITY 1: Use free local generation if available
|
| 150 |
+
if HF_TRANSFORMERS_AVAILABLE and generator:
|
| 151 |
+
logging.info("Using free local HuggingFace model...")
|
| 152 |
+
full_prompt = f"{persona}\n\nUser: {prompt}\nAssistant:"
|
| 153 |
+
response = local_generate(full_prompt, max_length=500)
|
| 154 |
+
|
| 155 |
+
# Extract just the assistant response
|
| 156 |
+
if "Assistant:" in response:
|
| 157 |
+
response = response.split("Assistant:")[-1].strip()
|
| 158 |
+
|
| 159 |
+
# For JSON requests, try to format as JSON
|
| 160 |
+
if is_json:
|
| 161 |
+
# Simple JSON formatting for common patterns
|
| 162 |
+
if "selected_experts" in prompt.lower():
|
| 163 |
+
return json.dumps({"selected_experts": ["distillation_analyst", "prior_art_analyst", "technical_synthesist", "ip_claim_drafter"]})
|
| 164 |
+
elif "key_patents" in prompt.lower():
|
| 165 |
+
return json.dumps({
|
| 166 |
+
"key_patents": ["Patent analysis pending"],
|
| 167 |
+
"relevant_papers": ["Research scan pending"],
|
| 168 |
+
"market_signals": ["Market analysis pending"]
|
| 169 |
+
})
|
| 170 |
+
elif "novelty_gap" in prompt.lower():
|
| 171 |
+
return json.dumps({"novelty_gap": "Analysis shows opportunity for innovation in this domain"})
|
| 172 |
+
elif "design_blueprint" in prompt.lower():
|
| 173 |
+
return json.dumps({"design_blueprint": response})
|
| 174 |
+
elif "provisional_claims" in prompt.lower():
|
| 175 |
+
return json.dumps({"provisional_claims": [response]})
|
| 176 |
+
|
| 177 |
+
return response
|
| 178 |
+
|
| 179 |
+
# PRIORITY 2: Use remote API only if local fails and credits available
|
| 180 |
+
client_to_use = None
|
| 181 |
+
model_id = None
|
| 182 |
+
|
| 183 |
+
if backend == "local":
|
| 184 |
+
client_to_use = local_client
|
| 185 |
+
model_id = LOCAL_MODEL_ID
|
| 186 |
+
elif backend == "remote" and remote_client:
|
| 187 |
+
client_to_use = remote_client
|
| 188 |
+
model_id = REMOTE_MODEL_ID
|
| 189 |
+
else:
|
| 190 |
+
error_msg = f"Backend '{backend}' is not configured or available. Using free local generation instead."
|
| 191 |
+
logging.warning(error_msg)
|
| 192 |
+
# Return a reasonable fallback instead of error
|
| 193 |
+
if is_json:
|
| 194 |
+
return json.dumps({"result": "Generated using free local model", "note": "Limited functionality without API credits"})
|
| 195 |
+
return "Generated using free local model (limited functionality without API credits)"
|
| 196 |
+
|
| 197 |
+
logging.info(f"Attempting API call to '{backend}' backend, model: {model_id}...")
|
| 198 |
+
messages = [{"role": "system", "content": persona}, {"role": "user", "content": prompt}]
|
| 199 |
+
try:
|
| 200 |
+
response_format = {"type": "json_object"} if is_json else {"type": "text"}
|
| 201 |
+
chat_completion = await client_to_use.chat.completions.create(
|
| 202 |
+
model=model_id, messages=messages, max_tokens=MAX_TOKENS, temperature=temperature, response_format=response_format,
|
| 203 |
+
)
|
| 204 |
+
return chat_completion.choices[0].message.content
|
| 205 |
+
except Exception as e:
|
| 206 |
+
error_str = f"API call to {backend} ({model_id}) failed: {e}. Falling back to free local generation."
|
| 207 |
+
logging.warning(error_str)
|
| 208 |
+
|
| 209 |
+
# Fallback to free local generation
|
| 210 |
+
if HF_TRANSFORMERS_AVAILABLE and generator:
|
| 211 |
+
return local_generate(f"{persona}\n{prompt}", max_length=300)
|
| 212 |
+
|
| 213 |
+
# Last resort: return structured response
|
| 214 |
+
if is_json:
|
| 215 |
+
return json.dumps({"error": "API unavailable", "fallback": "Using rule-based generation"})
|
| 216 |
+
return "API unavailable - using rule-based generation"
|
| 217 |
+
|
| 218 |
+
def scout_sources(query: str, num_results: int = 3) -> str:
|
| 219 |
+
"""Scout patents, papers, and market signals from free sources."""
|
| 220 |
+
logging.info(f"Scouting all sources for query: {query}")
|
| 221 |
+
raw_text = ""
|
| 222 |
+
# Google Patents
|
| 223 |
+
try:
|
| 224 |
+
patents_url = f"https://patents.google.com/xhr/query?url=q%3D{query}"
|
| 225 |
+
headers = {'User-Agent': 'Mozilla/5.0'}
|
| 226 |
+
patents_response = requests.get(patents_url, headers=headers)
|
| 227 |
+
patents_data = patents_response.json()['results']['cluster'][0]['result']
|
| 228 |
+
raw_text += "\n\n---PATENTS---\n" + "\n".join([f"Title: {res.get('title', '')}\nSnippet: {res.get('snippet', '')}" for res in patents_data[:num_results]])
|
| 229 |
+
except Exception as e:
|
| 230 |
+
logging.warning(f"Patent scouting failed: {e}")
|
| 231 |
+
|
| 232 |
+
# Semantic Scholar
|
| 233 |
+
if SEMANTIC_SCHOLAR_AVAILABLE:
|
| 234 |
+
try:
|
| 235 |
+
papers = sch.search_paper(query, limit=num_results)
|
| 236 |
+
raw_text += "\n\n---PAPERS---\n" + "\n".join([f"Title: {p.title}\nTLDR: {p.tldr.get('text') if p.tldr else 'N/A'}" for p in papers])
|
| 237 |
+
except Exception as e:
|
| 238 |
+
logging.warning(f"Semantic Scholar scouting failed: {e}")
|
| 239 |
+
else:
|
| 240 |
+
raw_text += "\n\n---PAPERS---\nSemantic Scholar unavailable - using alternative sources"
|
| 241 |
+
|
| 242 |
+
# DuckDuckGo
|
| 243 |
+
if DDGS_AVAILABLE:
|
| 244 |
+
try:
|
| 245 |
+
with DDGS() as ddgs:
|
| 246 |
+
results = list(ddgs.text(query, max_results=num_results))
|
| 247 |
+
raw_text += "\n\n---MARKET/NEWS---\n" + "\n".join([f"Title: {r['title']}\nSnippet: {r['body']}" for r in results])
|
| 248 |
+
except Exception as e:
|
| 249 |
+
logging.warning(f"DuckDuckGo scouting failed: {e}")
|
| 250 |
+
else:
|
| 251 |
+
raw_text += "\n\n---MARKET/NEWS---\nDuckDuckGo search unavailable - using basic market context"
|
| 252 |
+
|
| 253 |
+
return raw_text
|
| 254 |
+
|
| 255 |
+
async def run_expert(expert_key: str, context: Dict[str, Any]) -> Dict[str, Any]:
|
| 256 |
+
expert = EXPERT_PERSONAS[expert_key]
|
| 257 |
+
prompt = json.dumps({k: v for k, v in context.items() if k in expert.get("input_keys", context.keys())})
|
| 258 |
+
response_str = await llm_call(prompt, expert["persona"], expert["backend"])
|
| 259 |
+
try:
|
| 260 |
+
output = json.loads(response_str)
|
| 261 |
+
if "error" in output: raise ValueError(output.get("details", "LLM call error."))
|
| 262 |
+
return output
|
| 263 |
+
except (json.JSONDecodeError, ValueError) as e:
|
| 264 |
+
return {"error": f"Expert '{expert['name']}' failed to produce valid output. Response: {response_str}"}
|
| 265 |
+
|
| 266 |
+
def format_claims_for_report(claims: List[str]) -> str:
|
| 267 |
+
if not claims or not isinstance(claims, list): return "No claims were drafted."
|
| 268 |
+
return "\n".join([f"**Claim {i+1}:** {claim}" for i, claim in enumerate(claims)])
|
| 269 |
+
|
| 270 |
+
async def run_moe_pipeline(problem_statement: str, progress=gr.Progress(track_tqdm=True)):
|
| 271 |
+
"""The main Pragmatic Hybrid Pipeline."""
|
| 272 |
+
|
| 273 |
+
# STAGE 1: ROUTING (Remote with Fallback)
|
| 274 |
+
progress(0.1, desc="Assembling expert team...")
|
| 275 |
+
router_prompt = ROUTER_PROMPT_TEMPLATE.format(problem_statement=problem_statement, expert_list=list(EXPERT_PERSONAS.keys()))
|
| 276 |
+
|
| 277 |
+
# Try remote routing first
|
| 278 |
+
routing_response = await llm_call(router_prompt, "You are a master project manager.", "remote")
|
| 279 |
+
routed_experts_keys = []
|
| 280 |
+
|
| 281 |
+
try:
|
| 282 |
+
parsed_response = json.loads(routing_response)
|
| 283 |
+
if "error" in parsed_response:
|
| 284 |
+
raise ValueError(f"API Error: {parsed_response['error']}")
|
| 285 |
+
routed_experts_keys = parsed_response.get("selected_experts", [])
|
| 286 |
+
if "technical_synthesist" in routed_experts_keys and "ip_claim_drafter" not in routed_experts_keys:
|
| 287 |
+
routed_experts_keys.append("ip_claim_drafter")
|
| 288 |
+
if not routed_experts_keys: raise ValueError("Router returned empty list.")
|
| 289 |
+
except (json.JSONDecodeError, ValueError) as e:
|
| 290 |
+
# Fallback to predefined expert sequence
|
| 291 |
+
logging.warning(f"Routing failed: {e}. Using fallback routing.")
|
| 292 |
+
routed_experts_keys = ["distillation_analyst", "prior_art_analyst", "technical_synthesist", "ip_claim_drafter"]
|
| 293 |
+
|
| 294 |
+
# STAGE 2: SCOUTING & DISTILLATION (Remote)
|
| 295 |
+
progress(0.2, desc="Scouting sources...")
|
| 296 |
+
raw_data = scout_sources(problem_statement)
|
| 297 |
+
progress(0.4, desc="Distilling briefing (remote)...")
|
| 298 |
+
distillation_expert = EXPERT_PERSONAS["distillation_analyst"]
|
| 299 |
+
distillation_prompt = DISTILLATION_PROMPT_TEMPLATE.format(raw_data=raw_data)
|
| 300 |
+
distilled_briefing_str = await llm_call(distillation_prompt, distillation_expert['persona'], "remote")
|
| 301 |
+
try:
|
| 302 |
+
distilled_briefing = json.loads(distilled_briefing_str)
|
| 303 |
+
except (json.JSONDecodeError, ValueError):
|
| 304 |
+
yield "**Pipeline Error**\n\nFailed to distill raw data.", distilled_briefing_str
|
| 305 |
+
return
|
| 306 |
+
|
| 307 |
+
# STAGE 3: EXPERT GAUNTLET (Hybrid)
|
| 308 |
+
pipeline_context = {"problem_statement": problem_statement, "distilled_briefing": distilled_briefing}
|
| 309 |
+
for i, expert_key in enumerate(routed_experts_keys):
|
| 310 |
+
expert_name = EXPERT_PERSONAS[expert_key]['name']
|
| 311 |
+
backend = EXPERT_PERSONAS[expert_key]['backend']
|
| 312 |
+
progress(0.6 + (i * 0.1), desc=f"Consulting: {expert_name} ({backend})...")
|
| 313 |
+
expert_output = await run_expert(expert_key, pipeline_context)
|
| 314 |
+
pipeline_context.update(expert_output)
|
| 315 |
+
if "error" in expert_output:
|
| 316 |
+
yield f"**Pipeline Error**\n\n{expert_output['error']}", json.dumps(pipeline_context, indent=2)
|
| 317 |
+
return
|
| 318 |
+
|
| 319 |
+
# STAGE 4: FINAL REPORT (Remote)
|
| 320 |
+
progress(0.9, desc="Compiling final report (remote)...")
|
| 321 |
+
report_data = {
|
| 322 |
+
"problem_statement": pipeline_context.get("problem_statement", ""),
|
| 323 |
+
"key_patents": "\n- ".join(distilled_briefing.get('key_patents', ["Not found."])),
|
| 324 |
+
"relevant_papers": "\n- ".join(distilled_briefing.get('relevant_papers', ["Not found."])),
|
| 325 |
+
"market_signals": "\n- ".join(distilled_briefing.get('market_signals', ["Not found."])),
|
| 326 |
+
"novelty_gap": pipeline_context.get("novelty_gap", "Not assessed."),
|
| 327 |
+
"design_blueprint_approach": "\n".join(pipeline_context.get("design_blueprint", {}).get("technical_approach", ["Not specified."])),
|
| 328 |
+
"design_blueprint_specs": pipeline_context.get("design_blueprint", {}).get("technical_specifications", "Not specified."),
|
| 329 |
+
"claims_markdown": format_claims_for_report(pipeline_context.get("provisional_claims"))
|
| 330 |
+
}
|
| 331 |
+
final_report_str = REPORT_WRITER_TEMPLATE.format(**report_data)
|
| 332 |
+
|
| 333 |
+
progress(1.0, desc="Pipeline Complete!")
|
| 334 |
+
yield final_report_str, json.dumps(pipeline_context, indent=2)
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
# --- Gradio UI ---
|
| 338 |
+
def create_ui():
|
| 339 |
+
with gr.Blocks(theme=gr.themes.Glass(primary_hue="indigo", secondary_hue="purple")) as demo:
|
| 340 |
+
gr.Markdown(
|
| 341 |
+
"""
|
| 342 |
+
# 💡 MoE Innovation Engine (Orchestrator v0.5: Free Edition)
|
| 343 |
+
Uses local HuggingFace models for completely free innovation generation.
|
| 344 |
+
"""
|
| 345 |
+
)
|
| 346 |
+
with gr.Row():
|
| 347 |
+
with gr.Column(scale=1):
|
| 348 |
+
problem_statement_input = gr.Textbox(label="Core Problem Statement", placeholder="e.g., A low-cost, non-invasive method for early sepsis detection", lines=4)
|
| 349 |
+
run_button = gr.Button("🚀 Forge Invention")
|
| 350 |
+
with gr.Column(scale=2):
|
| 351 |
+
gr.Markdown("### Final Invention Blueprint")
|
| 352 |
+
report_output = gr.Markdown("Awaiting problem statement...")
|
| 353 |
+
with gr.Accordion("Show Raw JSON Output", open=False):
|
| 354 |
+
json_output = gr.Code(language="json", label="Raw Pipeline Context")
|
| 355 |
+
run_button.click(fn=run_moe_pipeline, inputs=[problem_statement_input], outputs=[report_output, json_output])
|
| 356 |
+
gr.Markdown("---")
|
| 357 |
+
gr.Markdown(f"""
|
| 358 |
+
**Setup Note:** This free edition runs completely local models on HuggingFace Spaces.
|
| 359 |
+
- **Primary Model:** Local `{LOCAL_MODEL_NAME}` (free, no credits needed)
|
| 360 |
+
- **Fallback:** `{REMOTE_MODEL_ID}` (only if you have API credits)
|
| 361 |
+
- **Cost:** Completely free for unlimited usage!
|
| 362 |
+
""")
|
| 363 |
+
return demo
|
| 364 |
+
|
| 365 |
+
if __name__ == "__main__":
|
| 366 |
+
# Suppress noisy logs from scout libraries
|
| 367 |
+
logging.getLogger("arxiv").setLevel(logging.ERROR)
|
| 368 |
+
logging.getLogger("semanticscholar").setLevel(logging.ERROR)
|
| 369 |
+
app = create_ui()
|
| 370 |
+
app.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Essential dependencies for HuggingFace Spaces
|
| 2 |
+
gradio
|
| 3 |
+
openai>=1.35.0
|
| 4 |
+
requests
|
| 5 |
+
beautifulsoup4
|
| 6 |
+
arxiv
|
| 7 |
+
numpy
|
| 8 |
+
duckduckgo-search
|
| 9 |
+
semanticscholar
|
| 10 |
+
sentence-transformers
|
| 11 |
+
faiss-cpu
|
| 12 |
+
transformers
|
| 13 |
+
torch
|