mirror of
https://github.com/Manoj-HV30/clawrity.git
synced 2026-05-16 19:35:21 +00:00
prototype
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
Clawrity — Scout Agent
|
||||
|
||||
Fetches real-time competitor updates and sector-specific news.
|
||||
Runs inside HEARTBEAT digest job ONLY — never on ad-hoc /chat queries.
|
||||
Appends "Market Intelligence" section to morning digest.
|
||||
|
||||
If nothing relevant is found, the section is omitted entirely — no filler.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from config.llm_client import get_llm_client, get_model_name
|
||||
from config.client_loader import ClientConfig
|
||||
from config.settings import get_settings
|
||||
from skills.web_search import web_search
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SCOUT_PROMPT = """You are a business intelligence scout for {client_name}.
|
||||
Their sector: {sector}
|
||||
Their competitors: {competitors}
|
||||
|
||||
Below are web search results from the last {lookback} day(s).
|
||||
Extract ONLY what is directly relevant to this client's business.
|
||||
Ignore anything generic or unrelated to their sector.
|
||||
If nothing is relevant, respond with exactly: NO_RELEVANT_NEWS
|
||||
|
||||
Format relevant findings as a clean "Market Intelligence" section with bullet points.
|
||||
Each bullet should summarize one key finding with its source.
|
||||
|
||||
Results:
|
||||
{search_results}"""
|
||||
|
||||
QUERY_PROMPT = """You are a business intelligence scout for {client_name}.
|
||||
Sector: {sector}
|
||||
Competitors: {competitors}
|
||||
|
||||
The user asked: "{query}"
|
||||
|
||||
Below are web search results. Extract ONLY what is directly relevant to the
|
||||
user's question and this client's business context. Ignore generic or unrelated content.
|
||||
If nothing is relevant, respond with exactly: NO_RELEVANT_NEWS
|
||||
|
||||
Format findings as concise bullet points with sources.
|
||||
|
||||
Results:
|
||||
{search_results}"""
|
||||
|
||||
|
||||
class ScoutAgent:
|
||||
"""Competitor and sector intelligence agent."""
|
||||
|
||||
def __init__(self):
|
||||
self.client = get_llm_client()
|
||||
self.model = get_model_name()
|
||||
|
||||
async def gather_intelligence(
|
||||
self,
|
||||
client_config: ClientConfig,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Fetch and summarize competitor/sector news for digest.
|
||||
|
||||
Args:
|
||||
client_config: Client config with scout section
|
||||
|
||||
Returns:
|
||||
Formatted "Market Intelligence" markdown section, or None if nothing relevant
|
||||
"""
|
||||
scout_config = client_config.scout
|
||||
if not scout_config.sector and not scout_config.competitors:
|
||||
logger.info(f"[{client_config.client_id}] No scout config — skipping")
|
||||
return None
|
||||
|
||||
lookback = scout_config.news_lookback_days
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# Gather search results
|
||||
all_results = []
|
||||
|
||||
# Search for each competitor
|
||||
for competitor in scout_config.competitors:
|
||||
query = f"{competitor} latest news"
|
||||
results = web_search(query, max_results=3, lookback_days=lookback)
|
||||
all_results.extend(results)
|
||||
|
||||
# Search for sector keywords
|
||||
for keyword in scout_config.keywords[:3]: # Limit to 3 keywords
|
||||
query = f"{keyword} news {today}"
|
||||
results = web_search(query, max_results=3, lookback_days=lookback)
|
||||
all_results.extend(results)
|
||||
|
||||
if not all_results:
|
||||
logger.info(f"[{client_config.client_id}] No search results found")
|
||||
return None
|
||||
|
||||
# Format results for LLM
|
||||
results_text = "\n\n".join(
|
||||
f"**{r['title']}** ({r['url']})\n{r['content']}"
|
||||
for r in all_results
|
||||
)
|
||||
|
||||
# Summarize with Groq
|
||||
prompt = SCOUT_PROMPT.format(
|
||||
client_name=client_config.client_name,
|
||||
sector=scout_config.sector,
|
||||
competitors=", ".join(scout_config.competitors),
|
||||
lookback=lookback,
|
||||
search_results=results_text,
|
||||
)
|
||||
|
||||
try:
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a business intelligence scout."},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=1024,
|
||||
)
|
||||
|
||||
result = response.choices[0].message.content.strip()
|
||||
|
||||
if result == "NO_RELEVANT_NEWS":
|
||||
logger.info(f"[{client_config.client_id}] Scout: no relevant news found")
|
||||
return None
|
||||
|
||||
section = f"## 🔭 Market Intelligence\n\n{result}"
|
||||
logger.info(f"[{client_config.client_id}] Scout: generated intelligence section")
|
||||
return section
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Scout Agent failed: {e}")
|
||||
return None
|
||||
|
||||
async def search_query(
|
||||
self,
|
||||
client_config: ClientConfig,
|
||||
query: str,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Run a targeted scout search for a specific user query.
|
||||
|
||||
Used by the /scout endpoint for ad-hoc competitor/news queries.
|
||||
|
||||
Args:
|
||||
client_config: Client config with scout section
|
||||
query: User's specific question about competitors/market
|
||||
|
||||
Returns:
|
||||
Formatted intelligence summary, or None if nothing relevant
|
||||
"""
|
||||
scout_config = client_config.scout
|
||||
|
||||
# Search with the user's query directly
|
||||
results = web_search(query, max_results=5, lookback_days=scout_config.news_lookback_days)
|
||||
|
||||
# Also search with competitor names if they appear in the query
|
||||
for competitor in scout_config.competitors:
|
||||
if competitor.lower() in query.lower():
|
||||
extra = web_search(f"{competitor} latest news", max_results=3, lookback_days=scout_config.news_lookback_days)
|
||||
results.extend(extra)
|
||||
|
||||
if not results:
|
||||
logger.info(f"[{client_config.client_id}] Scout query returned no results")
|
||||
return None
|
||||
|
||||
# Deduplicate by URL
|
||||
seen_urls = set()
|
||||
unique_results = []
|
||||
for r in results:
|
||||
if r["url"] not in seen_urls:
|
||||
seen_urls.add(r["url"])
|
||||
unique_results.append(r)
|
||||
|
||||
# Format results for LLM
|
||||
results_text = "\n\n".join(
|
||||
f"**{r['title']}** ({r['url']})\n{r['content']}"
|
||||
for r in unique_results
|
||||
)
|
||||
|
||||
prompt = QUERY_PROMPT.format(
|
||||
client_name=client_config.client_name,
|
||||
sector=scout_config.sector,
|
||||
competitors=", ".join(scout_config.competitors),
|
||||
query=query,
|
||||
search_results=results_text,
|
||||
)
|
||||
|
||||
try:
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a business intelligence scout."},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=1024,
|
||||
)
|
||||
|
||||
result = response.choices[0].message.content.strip()
|
||||
|
||||
if result == "NO_RELEVANT_NEWS":
|
||||
return None
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Scout query failed: {e}")
|
||||
return None
|
||||
Reference in New Issue
Block a user