| """ |
| 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 |
|
|