WHG2023 commited on
Commit
3ab4541
·
verified ·
1 Parent(s): 1ccfba4

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +370 -0
  2. 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