|
|
""" |
|
|
MatchmakingAgent for Patent Wake-Up Scenario |
|
|
|
|
|
Matches patents with potential licensees, partners, and investors: |
|
|
- Semantic search in stakeholder database |
|
|
- Multi-dimensional match scoring |
|
|
- Geographic alignment (EU-Canada focus) |
|
|
- Generates match rationale and collaboration opportunities |
|
|
""" |
|
|
|
|
|
from typing import List, Optional |
|
|
from loguru import logger |
|
|
from langchain_core.prompts import ChatPromptTemplate |
|
|
from langchain_core.output_parsers import JsonOutputParser |
|
|
from langchain_core.messages import HumanMessage |
|
|
|
|
|
from ..base_agent import BaseAgent, Task |
|
|
from ...llm.langchain_ollama_client import LangChainOllamaClient |
|
|
from ...workflow.langgraph_state import ( |
|
|
PatentAnalysis, |
|
|
MarketAnalysis, |
|
|
StakeholderMatch |
|
|
) |
|
|
|
|
|
|
|
|
class MatchmakingAgent(BaseAgent): |
|
|
""" |
|
|
Specialized agent for stakeholder matching. |
|
|
Uses semantic search and LLM reasoning to find best-fit partners. |
|
|
""" |
|
|
|
|
|
def __init__(self, llm_client: LangChainOllamaClient, memory_agent): |
|
|
""" |
|
|
Initialize MatchmakingAgent. |
|
|
|
|
|
Args: |
|
|
llm_client: LangChain Ollama client |
|
|
memory_agent: Memory agent (required for stakeholder search) |
|
|
""" |
|
|
|
|
|
self.name = "MatchmakingAgent" |
|
|
self.description = "Stakeholder matching and partner identification" |
|
|
|
|
|
self.llm_client = llm_client |
|
|
self.memory_agent = memory_agent |
|
|
|
|
|
if not memory_agent: |
|
|
raise ValueError("MatchmakingAgent requires memory_agent for stakeholder database") |
|
|
|
|
|
|
|
|
self.llm = llm_client.get_llm('complex') |
|
|
|
|
|
|
|
|
self.scoring_chain = self._create_scoring_chain() |
|
|
|
|
|
|
|
|
self._stakeholders_initialized = False |
|
|
|
|
|
logger.info("Initialized MatchmakingAgent") |
|
|
|
|
|
def _create_scoring_chain(self): |
|
|
"""Create chain for match scoring""" |
|
|
prompt = ChatPromptTemplate.from_messages([ |
|
|
("system", "You are an expert in technology transfer and business development."), |
|
|
("human", """ |
|
|
Evaluate the match quality between this patent and stakeholder: |
|
|
|
|
|
PATENT: |
|
|
- Title: {patent_title} |
|
|
- Technical Domains: {technical_domains} |
|
|
- Key Innovations: {key_innovations} |
|
|
- TRL: {trl_level} |
|
|
- Target Markets: {target_markets} |
|
|
|
|
|
STAKEHOLDER: |
|
|
- Name: {stakeholder_name} |
|
|
- Type: {stakeholder_type} |
|
|
- Expertise: {stakeholder_expertise} |
|
|
- Focus Sectors: {stakeholder_sectors} |
|
|
- Location: {stakeholder_location} |
|
|
|
|
|
Provide match assessment in JSON format: |
|
|
{{ |
|
|
"technical_fit": 0.85, |
|
|
"market_fit": 0.90, |
|
|
"geographic_fit": 1.0, |
|
|
"strategic_fit": 0.80, |
|
|
"overall_fit_score": 0.88, |
|
|
"match_rationale": "Detailed explanation of why this is a strong match", |
|
|
"collaboration_opportunities": ["Licensing", "Joint development", "Co-marketing"], |
|
|
"potential_value": "High/Medium/Low", |
|
|
"recommended_approach": "How to approach this stakeholder", |
|
|
"talking_points": ["Point 1", "Point 2", "Point 3"] |
|
|
}} |
|
|
|
|
|
Scoring guidelines: |
|
|
- technical_fit: Does stakeholder have expertise in this technology? |
|
|
- market_fit: Does stakeholder operate in target markets? |
|
|
- geographic_fit: Geographic alignment (EU/Canada priority) |
|
|
- strategic_fit: Overall strategic alignment |
|
|
- overall_fit_score: Weighted average (0-1) |
|
|
|
|
|
Return ONLY valid JSON. |
|
|
""") |
|
|
]) |
|
|
|
|
|
parser = JsonOutputParser() |
|
|
return prompt | self.llm | parser |
|
|
|
|
|
async def find_matches( |
|
|
self, |
|
|
patent_analysis: PatentAnalysis, |
|
|
market_analysis: MarketAnalysis, |
|
|
max_matches: int = 10 |
|
|
) -> List[StakeholderMatch]: |
|
|
""" |
|
|
Find best-fit stakeholders for patent commercialization. |
|
|
|
|
|
Args: |
|
|
patent_analysis: Patent technical details |
|
|
market_analysis: Market opportunities |
|
|
max_matches: Maximum number of matches to return |
|
|
|
|
|
Returns: |
|
|
List of StakeholderMatch objects ranked by fit score |
|
|
""" |
|
|
logger.info(f"🤝 Finding matches for: {patent_analysis.title}") |
|
|
|
|
|
|
|
|
if not self._stakeholders_initialized: |
|
|
await self._ensure_stakeholders() |
|
|
|
|
|
|
|
|
query = self._create_search_query(patent_analysis, market_analysis) |
|
|
|
|
|
|
|
|
logger.info("Searching stakeholder database...") |
|
|
stakeholder_docs = await self.memory_agent.retrieve_relevant_context( |
|
|
query=query, |
|
|
context_type="stakeholders", |
|
|
top_k=max_matches * 2 |
|
|
) |
|
|
|
|
|
logger.info(f"Found {len(stakeholder_docs)} potential stakeholders") |
|
|
|
|
|
|
|
|
matches = [] |
|
|
for doc in stakeholder_docs: |
|
|
try: |
|
|
stakeholder = self._parse_stakeholder(doc) |
|
|
match = await self._score_match( |
|
|
patent_analysis, |
|
|
market_analysis, |
|
|
stakeholder |
|
|
) |
|
|
matches.append(match) |
|
|
except Exception as e: |
|
|
logger.warning(f"Failed to score match: {e}") |
|
|
continue |
|
|
|
|
|
|
|
|
matches.sort(key=lambda x: x.overall_fit_score, reverse=True) |
|
|
|
|
|
logger.success(f"✅ Found {len(matches)} matches, returning top {max_matches}") |
|
|
|
|
|
return matches[:max_matches] |
|
|
|
|
|
def _create_search_query( |
|
|
self, |
|
|
patent: PatentAnalysis, |
|
|
market: MarketAnalysis |
|
|
) -> str: |
|
|
"""Create search query for stakeholder matching""" |
|
|
query_parts = [] |
|
|
|
|
|
|
|
|
query_parts.extend(patent.technical_domains) |
|
|
|
|
|
|
|
|
query_parts.extend(market.top_sectors) |
|
|
|
|
|
|
|
|
for innovation in patent.key_innovations[:2]: |
|
|
query_parts.append(innovation.split('.')[0]) |
|
|
|
|
|
return " ".join(query_parts) |
|
|
|
|
|
def _parse_stakeholder(self, doc) -> dict: |
|
|
"""Parse stakeholder document into dict""" |
|
|
import json |
|
|
|
|
|
|
|
|
profile_json = doc.metadata.get('profile', '{}') |
|
|
profile = json.loads(profile_json) |
|
|
|
|
|
|
|
|
profile['search_match_text'] = doc.page_content |
|
|
|
|
|
return profile |
|
|
|
|
|
async def _score_match( |
|
|
self, |
|
|
patent: PatentAnalysis, |
|
|
market: MarketAnalysis, |
|
|
stakeholder: dict |
|
|
) -> StakeholderMatch: |
|
|
""" |
|
|
Score match quality using LLM reasoning. |
|
|
|
|
|
Args: |
|
|
patent: Patent analysis |
|
|
market: Market analysis |
|
|
stakeholder: Stakeholder profile dict |
|
|
|
|
|
Returns: |
|
|
StakeholderMatch with scores and rationale |
|
|
""" |
|
|
|
|
|
scoring = await self.scoring_chain.ainvoke({ |
|
|
"patent_title": patent.title, |
|
|
"technical_domains": ", ".join(patent.technical_domains), |
|
|
"key_innovations": ", ".join(patent.key_innovations[:3]), |
|
|
"trl_level": patent.trl_level, |
|
|
"target_markets": ", ".join(market.top_sectors), |
|
|
"stakeholder_name": stakeholder.get('name', 'Unknown'), |
|
|
"stakeholder_type": stakeholder.get('type', 'Unknown'), |
|
|
"stakeholder_expertise": ", ".join(stakeholder.get('expertise', [])), |
|
|
"stakeholder_sectors": ", ".join(stakeholder.get('focus_sectors', [])), |
|
|
"stakeholder_location": stakeholder.get('location', 'Unknown') |
|
|
}) |
|
|
|
|
|
|
|
|
return StakeholderMatch( |
|
|
stakeholder_name=stakeholder.get('name', 'Unknown'), |
|
|
stakeholder_type=stakeholder.get('type', 'Unknown'), |
|
|
location=stakeholder.get('location', 'Unknown'), |
|
|
contact_info=stakeholder.get('contact_info'), |
|
|
overall_fit_score=scoring.get('overall_fit_score', 0.5), |
|
|
technical_fit=scoring.get('technical_fit', 0.5), |
|
|
market_fit=scoring.get('market_fit', 0.5), |
|
|
geographic_fit=scoring.get('geographic_fit', 0.5), |
|
|
strategic_fit=scoring.get('strategic_fit', 0.5), |
|
|
match_rationale=scoring.get('match_rationale', 'Match assessment'), |
|
|
collaboration_opportunities=scoring.get('collaboration_opportunities', []), |
|
|
potential_value=scoring.get('potential_value', 'Medium'), |
|
|
recommended_approach=scoring.get('recommended_approach', 'Professional outreach'), |
|
|
talking_points=scoring.get('talking_points', []) |
|
|
) |
|
|
|
|
|
async def _ensure_stakeholders(self): |
|
|
"""Ensure sample stakeholders exist in database""" |
|
|
|
|
|
stats = self.memory_agent.get_collection_stats() |
|
|
|
|
|
if stats.get('stakeholders_count', 0) < 5: |
|
|
logger.info("Populating sample stakeholder database...") |
|
|
await self._populate_sample_stakeholders() |
|
|
|
|
|
self._stakeholders_initialized = True |
|
|
|
|
|
async def _populate_sample_stakeholders(self): |
|
|
""" |
|
|
Create sample stakeholder profiles for demonstration. |
|
|
In production, this would be populated from real databases. |
|
|
""" |
|
|
sample_stakeholders = [ |
|
|
{ |
|
|
"name": "BioVentures Capital (Toronto)", |
|
|
"type": "Investor", |
|
|
"expertise": ["AI", "Machine Learning", "Drug Discovery", "Healthcare"], |
|
|
"focus_sectors": ["Pharmaceuticals", "Biotechnology", "Healthcare AI"], |
|
|
"location": "Toronto, Canada", |
|
|
"investment_stage": ["Seed", "Series A"], |
|
|
"description": "Early-stage deep tech venture capital focusing on AI-driven healthcare innovation" |
|
|
}, |
|
|
{ |
|
|
"name": "EuroTech Licensing GmbH", |
|
|
"type": "Licensing Organization", |
|
|
"expertise": ["Materials Science", "Nanotechnology", "Energy", "Manufacturing"], |
|
|
"focus_sectors": ["Renewable Energy", "Advanced Materials", "Industrial IoT"], |
|
|
"location": "Munich, Germany", |
|
|
"description": "Technology licensing and commercialization across European markets" |
|
|
}, |
|
|
{ |
|
|
"name": "McGill University Technology Transfer", |
|
|
"type": "University TTO", |
|
|
"expertise": ["Biomedical Engineering", "Software", "Clean Tech", "AI"], |
|
|
"focus_sectors": ["Healthcare", "Environmental Tech", "AI Applications"], |
|
|
"location": "Montreal, Canada", |
|
|
"description": "Academic technology transfer and industry partnerships" |
|
|
}, |
|
|
{ |
|
|
"name": "PharmaTech Solutions Inc.", |
|
|
"type": "Company", |
|
|
"expertise": ["Drug Discovery", "Clinical Trials", "Regulatory Affairs"], |
|
|
"focus_sectors": ["Pharmaceuticals", "Biotechnology"], |
|
|
"location": "Basel, Switzerland", |
|
|
"description": "Pharmaceutical development and commercialization services" |
|
|
}, |
|
|
{ |
|
|
"name": "Nordic Innovation Partners", |
|
|
"type": "Investor", |
|
|
"expertise": ["Clean Tech", "Sustainability", "Energy", "Manufacturing"], |
|
|
"focus_sectors": ["Renewable Energy", "Circular Economy", "Green Tech"], |
|
|
"location": "Stockholm, Sweden", |
|
|
"investment_stage": ["Series A", "Series B"], |
|
|
"description": "Impact investment in sustainable technologies" |
|
|
}, |
|
|
{ |
|
|
"name": "Canadian AI Consortium", |
|
|
"type": "Industry Consortium", |
|
|
"expertise": ["AI", "Machine Learning", "Computer Vision", "NLP"], |
|
|
"focus_sectors": ["AI Applications", "Software", "Healthcare AI"], |
|
|
"location": "Vancouver, Canada", |
|
|
"description": "Collaborative AI research and commercialization network" |
|
|
}, |
|
|
{ |
|
|
"name": "MedTech Innovators (Amsterdam)", |
|
|
"type": "Company", |
|
|
"expertise": ["Medical Devices", "Digital Health", "AI Diagnostics"], |
|
|
"focus_sectors": ["Healthcare", "Medical Technology"], |
|
|
"location": "Amsterdam, Netherlands", |
|
|
"description": "Medical technology development and distribution" |
|
|
}, |
|
|
{ |
|
|
"name": "Quebec Pension Fund Technology", |
|
|
"type": "Investor", |
|
|
"expertise": ["Technology", "Healthcare", "Clean Tech", "AI"], |
|
|
"focus_sectors": ["Healthcare", "Clean Energy", "AI", "Manufacturing"], |
|
|
"location": "Montreal, Canada", |
|
|
"investment_stage": ["Series B", "Growth"], |
|
|
"description": "Large-scale technology investment fund" |
|
|
}, |
|
|
{ |
|
|
"name": "European Patent Office Services", |
|
|
"type": "IP Services", |
|
|
"expertise": ["Patent Strategy", "IP Licensing", "Technology Transfer"], |
|
|
"focus_sectors": ["All Technology Sectors"], |
|
|
"location": "Munich, Germany", |
|
|
"description": "Patent commercialization and licensing support" |
|
|
}, |
|
|
{ |
|
|
"name": "CleanTech Accelerator Berlin", |
|
|
"type": "Accelerator", |
|
|
"expertise": ["Clean Tech", "Sustainability", "Energy", "Materials"], |
|
|
"focus_sectors": ["Renewable Energy", "Environmental Tech", "Circular Economy"], |
|
|
"location": "Berlin, Germany", |
|
|
"description": "Accelerator program for sustainable technology startups" |
|
|
} |
|
|
] |
|
|
|
|
|
|
|
|
for stakeholder in sample_stakeholders: |
|
|
try: |
|
|
await self.memory_agent.store_stakeholder_profile( |
|
|
name=stakeholder["name"], |
|
|
profile=stakeholder, |
|
|
categories=[stakeholder["type"]] + stakeholder["expertise"][:3] |
|
|
) |
|
|
logger.debug(f"Stored stakeholder: {stakeholder['name']}") |
|
|
except Exception as e: |
|
|
logger.warning(f"Failed to store stakeholder {stakeholder['name']}: {e}") |
|
|
|
|
|
logger.success(f"✅ Populated {len(sample_stakeholders)} sample stakeholders") |
|
|
|
|
|
async def process_task(self, task: Task) -> Task: |
|
|
""" |
|
|
Process task using agent interface. |
|
|
|
|
|
Args: |
|
|
task: Task with patent_analysis and market_analysis in metadata |
|
|
|
|
|
Returns: |
|
|
Task with list of StakeholderMatch results |
|
|
""" |
|
|
task.status = "in_progress" |
|
|
|
|
|
try: |
|
|
patent_dict = task.metadata.get('patent_analysis') |
|
|
market_dict = task.metadata.get('market_analysis') |
|
|
|
|
|
if not patent_dict or not market_dict: |
|
|
raise ValueError("Both patent_analysis and market_analysis required") |
|
|
|
|
|
|
|
|
patent_analysis = PatentAnalysis(**patent_dict) |
|
|
market_analysis = MarketAnalysis(**market_dict) |
|
|
|
|
|
matches = await self.find_matches(patent_analysis, market_analysis) |
|
|
|
|
|
task.result = [m.model_dump() for m in matches] |
|
|
task.status = "completed" |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Matchmaking failed: {e}") |
|
|
task.status = "failed" |
|
|
task.error = str(e) |
|
|
|
|
|
return task |
|
|
|