Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""Company-profile discovery extraction. Used once per company, at
|
||||
onboarding time (see app/services/discovery_service.py) - never asked
|
||||
"tell me everything about this company," only "given this fetched
|
||||
homepage text and these search snippets, extract what's actually
|
||||
supported." Leaves a field unset rather than guessing when the evidence
|
||||
doesn't support it; the wizard's Review step shows unset fields as
|
||||
"not found" for the user to fill in themselves.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.analysis.llm.base import LLMProvider
|
||||
from app.prompts.base import build_user_prompt
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"You are a competitive intelligence analyst building an initial company profile from "
|
||||
"search results and a fetched homepage. Extract only what the evidence actually states or "
|
||||
"clearly implies - never use outside knowledge, never guess. Leave a field null (or an "
|
||||
"empty list) rather than filling it with a plausible-sounding guess. Aliases means other "
|
||||
"names the company is or was known by (former names, common abbreviations, brand names) - "
|
||||
"not synonyms or descriptions. Competitors means other named companies the evidence "
|
||||
"explicitly identifies as competing in the same space."
|
||||
)
|
||||
|
||||
|
||||
class PublicIdentifier(BaseModel):
|
||||
key: str = Field(description="e.g. 'ticker', 'linkedin_url', 'cik'")
|
||||
value: str
|
||||
|
||||
|
||||
class CompanyProfileExtraction(BaseModel):
|
||||
description: str | None = Field(
|
||||
default=None,
|
||||
description="A short (1-3 sentence) factual summary of what the company does, drawn "
|
||||
"strictly from the evidence - not a marketing tagline.",
|
||||
)
|
||||
industry: str | None = None
|
||||
country: str | None = None
|
||||
region: str | None = None
|
||||
headquarters: str | None = None
|
||||
aliases: list[str] = Field(default_factory=list)
|
||||
competitors: list[str] = Field(default_factory=list)
|
||||
public_identifiers: list[PublicIdentifier] = Field(
|
||||
default_factory=list,
|
||||
description="Best-effort key/value pairs, e.g. ticker or linkedin_url - empty if none found. "
|
||||
"A list of {key, value} pairs rather than a free-form object, since the Gemini Developer "
|
||||
"API's structured-output mode rejects open-ended (additionalProperties) JSON schemas.",
|
||||
)
|
||||
|
||||
|
||||
async def extract_company_profile(
|
||||
llm: LLMProvider,
|
||||
*,
|
||||
company_name: str,
|
||||
homepage_url: str | None,
|
||||
homepage_text: str | None,
|
||||
search_results: list[dict],
|
||||
) -> CompanyProfileExtraction:
|
||||
evidence = {
|
||||
"company_name": company_name,
|
||||
"homepage_url": homepage_url,
|
||||
"homepage_text": (homepage_text or "")[:4000],
|
||||
"search_results": search_results[:15],
|
||||
}
|
||||
user_prompt = build_user_prompt(
|
||||
"Build an initial company profile (description, industry, country, region, "
|
||||
"headquarters, aliases, competitors, public identifiers) strictly from this evidence.",
|
||||
evidence,
|
||||
)
|
||||
return await llm.generate_structured(SYSTEM_PROMPT, user_prompt, CompanyProfileExtraction)
|
||||
Reference in New Issue
Block a user