|
|
""" |
|
|
OutreachAgent for Patent Wake-Up Scenario |
|
|
|
|
|
Generates valorization materials and outreach communications: |
|
|
- Comprehensive valorization briefs (PDF) |
|
|
- Executive summaries |
|
|
- Stakeholder-specific outreach materials |
|
|
""" |
|
|
|
|
|
from typing import List |
|
|
import os |
|
|
from datetime import datetime |
|
|
from loguru import logger |
|
|
from langchain_core.prompts import ChatPromptTemplate |
|
|
|
|
|
from ..base_agent import BaseAgent, Task |
|
|
from ...llm.langchain_ollama_client import LangChainOllamaClient |
|
|
from ...workflow.langgraph_state import ( |
|
|
PatentAnalysis, |
|
|
MarketAnalysis, |
|
|
StakeholderMatch, |
|
|
ValorizationBrief |
|
|
) |
|
|
|
|
|
|
|
|
class OutreachAgent(BaseAgent): |
|
|
""" |
|
|
Specialized agent for generating valorization materials. |
|
|
Creates briefs, summaries, and outreach communications. |
|
|
""" |
|
|
|
|
|
def __init__(self, llm_client: LangChainOllamaClient, memory_agent=None): |
|
|
""" |
|
|
Initialize OutreachAgent. |
|
|
|
|
|
Args: |
|
|
llm_client: LangChain Ollama client |
|
|
memory_agent: Optional memory agent |
|
|
""" |
|
|
|
|
|
self.name = "OutreachAgent" |
|
|
self.description = "Valorization brief and outreach generation" |
|
|
|
|
|
self.llm_client = llm_client |
|
|
self.memory_agent = memory_agent |
|
|
|
|
|
|
|
|
self.llm = llm_client.get_llm('standard') |
|
|
|
|
|
|
|
|
self.brief_chain = self._create_brief_chain() |
|
|
self.summary_chain = self._create_summary_chain() |
|
|
|
|
|
|
|
|
os.makedirs("outputs", exist_ok=True) |
|
|
|
|
|
logger.info("Initialized OutreachAgent") |
|
|
|
|
|
def _create_brief_chain(self): |
|
|
"""Create chain for valorization brief generation""" |
|
|
prompt = ChatPromptTemplate.from_messages([ |
|
|
("system", "You are an expert in technology commercialization and professional business writing."), |
|
|
("human", """ |
|
|
Create a comprehensive valorization brief for this patent. |
|
|
|
|
|
PATENT ANALYSIS: |
|
|
Title: {patent_title} |
|
|
TRL: {trl_level}/9 |
|
|
Key Innovations: |
|
|
{key_innovations} |
|
|
Potential Applications: |
|
|
{applications} |
|
|
|
|
|
MARKET OPPORTUNITIES: |
|
|
{market_opportunities} |
|
|
|
|
|
TOP STAKEHOLDER MATCHES: |
|
|
{stakeholder_matches} |
|
|
|
|
|
Create a professional valorization brief in markdown format with: |
|
|
|
|
|
# Valorization Brief: [Patent Title] |
|
|
|
|
|
## Executive Summary |
|
|
[1-paragraph overview highlighting commercialization potential] |
|
|
|
|
|
## Technology Overview |
|
|
### Key Innovations |
|
|
[Bullet points of key innovations] |
|
|
|
|
|
### Technology Readiness |
|
|
[TRL assessment and readiness for commercialization] |
|
|
|
|
|
### Technical Advantages |
|
|
[What makes this technology unique] |
|
|
|
|
|
## Market Opportunity Analysis |
|
|
### Target Sectors |
|
|
[Top 3-5 sectors with market size data] |
|
|
|
|
|
### Market Gaps Addressed |
|
|
[Specific problems this solves] |
|
|
|
|
|
### Competitive Positioning |
|
|
[How to position vs. alternatives] |
|
|
|
|
|
## Recommended Partners |
|
|
[Top 5 stakeholders with match rationale] |
|
|
|
|
|
## Commercialization Roadmap |
|
|
### Immediate Next Steps (0-6 months) |
|
|
[Specific actions] |
|
|
|
|
|
### Medium-term Goals (6-18 months) |
|
|
[Development milestones] |
|
|
|
|
|
### Long-term Vision (18+ months) |
|
|
[Market expansion] |
|
|
|
|
|
## Key Takeaways |
|
|
[3-5 bullet points with main insights] |
|
|
|
|
|
Write professionally but accessibly. Use specific numbers and data where available. |
|
|
""") |
|
|
]) |
|
|
|
|
|
return prompt | self.llm |
|
|
|
|
|
def _create_summary_chain(self): |
|
|
"""Create chain for executive summary extraction""" |
|
|
prompt = ChatPromptTemplate.from_messages([ |
|
|
("system", "You extract concise executive summaries from longer documents."), |
|
|
("human", "Extract a 2-3 sentence executive summary from this brief:\n\n{brief_content}") |
|
|
]) |
|
|
|
|
|
return prompt | self.llm |
|
|
|
|
|
async def create_valorization_brief( |
|
|
self, |
|
|
patent_analysis: PatentAnalysis, |
|
|
market_analysis: MarketAnalysis, |
|
|
matches: List[StakeholderMatch] |
|
|
) -> ValorizationBrief: |
|
|
""" |
|
|
Generate comprehensive valorization brief. |
|
|
|
|
|
Args: |
|
|
patent_analysis: Patent technical analysis |
|
|
market_analysis: Market opportunities |
|
|
matches: Stakeholder matches |
|
|
|
|
|
Returns: |
|
|
ValorizationBrief with content and PDF path |
|
|
""" |
|
|
logger.info(f"📝 Creating valorization brief for: {patent_analysis.title}") |
|
|
|
|
|
|
|
|
key_innovations = "\n".join([f"- {inn}" for inn in patent_analysis.key_innovations]) |
|
|
applications = "\n".join([f"- {app}" for app in patent_analysis.potential_applications]) |
|
|
|
|
|
market_opps = "\n\n".join([ |
|
|
f"**{opp.sector}** ({opp.technology_fit} fit)\n" |
|
|
f"- Market Size: {f'${opp.market_size_usd/1e9:.1f}B USD' if opp.market_size_usd is not None else 'NaN'}\n" |
|
|
f"- Growth: {f'{opp.growth_rate_percent}% annually' if opp.growth_rate_percent is not None else 'NaN'}\n" |
|
|
f"- Gap: {opp.market_gap}" |
|
|
for opp in market_analysis.opportunities[:5] |
|
|
]) |
|
|
|
|
|
stakeholder_text = "\n\n".join([ |
|
|
f"{i+1}. **{m.stakeholder_name}** ({m.stakeholder_type})\n" |
|
|
f" - Location: {m.location}\n" |
|
|
f" - Fit Score: {m.overall_fit_score:.2f}\n" |
|
|
f" - Why: {m.match_rationale[:200]}..." |
|
|
for i, m in enumerate(matches[:5]) |
|
|
]) |
|
|
|
|
|
|
|
|
logger.info("Generating brief content...") |
|
|
content_response = await self.brief_chain.ainvoke({ |
|
|
"patent_title": patent_analysis.title, |
|
|
"trl_level": patent_analysis.trl_level, |
|
|
"key_innovations": key_innovations, |
|
|
"applications": applications, |
|
|
"market_opportunities": market_opps, |
|
|
"stakeholder_matches": stakeholder_text |
|
|
}) |
|
|
|
|
|
content = content_response.content |
|
|
|
|
|
|
|
|
logger.info("Extracting executive summary...") |
|
|
summary_response = await self.summary_chain.ainvoke({ |
|
|
"brief_content": content[:2000] |
|
|
}) |
|
|
executive_summary = summary_response.content |
|
|
|
|
|
|
|
|
pdf_path = await self._generate_pdf( |
|
|
content=content, |
|
|
patent_id=patent_analysis.patent_id, |
|
|
title=patent_analysis.title |
|
|
) |
|
|
|
|
|
|
|
|
brief = ValorizationBrief( |
|
|
patent_id=patent_analysis.patent_id, |
|
|
content=content, |
|
|
pdf_path=pdf_path, |
|
|
executive_summary=executive_summary, |
|
|
technology_overview=self._extract_section(content, "Technology Overview"), |
|
|
market_analysis_summary=self._extract_section(content, "Market Opportunity"), |
|
|
partner_recommendations=self._extract_section(content, "Recommended Partners"), |
|
|
top_opportunities=market_analysis.top_sectors, |
|
|
recommended_partners=[m.stakeholder_name for m in matches[:5]], |
|
|
key_takeaways=self._extract_takeaways(content), |
|
|
generated_date=datetime.now().strftime("%Y-%m-%d"), |
|
|
version="1.0" |
|
|
) |
|
|
|
|
|
logger.success(f"✅ Valorization brief created: {pdf_path}") |
|
|
|
|
|
return brief |
|
|
|
|
|
async def _generate_pdf(self, content: str, patent_id: str, title: str) -> str: |
|
|
""" |
|
|
Generate PDF from markdown content. |
|
|
|
|
|
Args: |
|
|
content: Markdown content |
|
|
patent_id: Patent identifier |
|
|
title: Brief title |
|
|
|
|
|
Returns: |
|
|
Path to generated PDF |
|
|
""" |
|
|
try: |
|
|
from ...tools.langchain_tools import document_generator_tool |
|
|
|
|
|
|
|
|
filename = f"valorization_brief_{patent_id}_{datetime.now().strftime('%Y%m%d')}.pdf" |
|
|
pdf_path = os.path.join("outputs", filename) |
|
|
|
|
|
|
|
|
await document_generator_tool.ainvoke({ |
|
|
"output_path": pdf_path, |
|
|
"title": f"Valorization Brief: {title}", |
|
|
"content": content, |
|
|
"author": "SPARKNET Valorization System" |
|
|
}) |
|
|
|
|
|
return pdf_path |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"PDF generation failed: {e}") |
|
|
|
|
|
md_path = pdf_path.replace('.pdf', '.md') |
|
|
with open(md_path, 'w', encoding='utf-8') as f: |
|
|
f.write(content) |
|
|
logger.warning(f"Saved as markdown instead: {md_path}") |
|
|
return md_path |
|
|
|
|
|
def _extract_section(self, content: str, section_name: str) -> str: |
|
|
"""Extract a specific section from markdown content""" |
|
|
import re |
|
|
|
|
|
|
|
|
pattern = rf'##\s+{section_name}.*?\n(.*?)(?=##|\Z)' |
|
|
match = re.search(pattern, content, re.DOTALL | re.IGNORECASE) |
|
|
|
|
|
if match: |
|
|
return match.group(1).strip()[:500] |
|
|
return "Section not found" |
|
|
|
|
|
def _extract_takeaways(self, content: str) -> List[str]: |
|
|
"""Extract key takeaways from content""" |
|
|
import re |
|
|
|
|
|
|
|
|
pattern = r'##\s+Key Takeaways.*?\n(.*?)(?=##|\Z)' |
|
|
match = re.search(pattern, content, re.DOTALL | re.IGNORECASE) |
|
|
|
|
|
if match: |
|
|
takeaways_text = match.group(1) |
|
|
|
|
|
bullets = re.findall(r'[-*]\s+(.+)', takeaways_text) |
|
|
return bullets[:5] |
|
|
|
|
|
|
|
|
return [ |
|
|
"Technology demonstrates strong commercialization potential", |
|
|
"Multiple market opportunities identified", |
|
|
"Strategic partners available for collaboration" |
|
|
] |
|
|
|
|
|
async def process_task(self, task: Task) -> Task: |
|
|
""" |
|
|
Process task using agent interface. |
|
|
|
|
|
Args: |
|
|
task: Task with patent_analysis, market_analysis, and matches in metadata |
|
|
|
|
|
Returns: |
|
|
Task with ValorizationBrief result |
|
|
""" |
|
|
task.status = "in_progress" |
|
|
|
|
|
try: |
|
|
patent_dict = task.metadata.get('patent_analysis') |
|
|
market_dict = task.metadata.get('market_analysis') |
|
|
matches_list = task.metadata.get('matches', []) |
|
|
|
|
|
if not patent_dict or not market_dict: |
|
|
raise ValueError("patent_analysis and market_analysis required") |
|
|
|
|
|
|
|
|
patent_analysis = PatentAnalysis(**patent_dict) |
|
|
market_analysis = MarketAnalysis(**market_dict) |
|
|
matches = [StakeholderMatch(**m) for m in matches_list] |
|
|
|
|
|
brief = await self.create_valorization_brief( |
|
|
patent_analysis, |
|
|
market_analysis, |
|
|
matches |
|
|
) |
|
|
|
|
|
task.result = brief.model_dump() |
|
|
task.status = "completed" |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Outreach generation failed: {e}") |
|
|
task.status = "failed" |
|
|
task.error = str(e) |
|
|
|
|
|
return task |
|
|
|