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).
73 lines
3.0 KiB
Python
73 lines
3.0 KiB
Python
"""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)
|