Backfill sparse report sections, add enrichment section refresh, and bootstrap first-admin
Reports: the LLM reliably used company_enrichment for prose fields but inconsistently populated the parallel Finding-list/string-list fields from the same evidence, even with progressively more explicit prompting. Add a code-level backfill (products, recent developments, financial signals, strategic initiatives, regulatory signals, risks/opportunities mirrored from SWOT, unknowns, monitoring recommendations) that only ever fills in what the model left empty, never overwrites what it produced. Enrichment tab: reorder sections (Products/Recent updates before Customers/Competitors) and add a per-section "Refresh" button that re-fetches just one of NinjaPear's six independent per-company endpoints when it came back empty - confirmed live that a data-coverage gap (e.g. Amazon returning no products) is real provider behavior, not a bug. Auth: the first account registered on a deployment with zero existing admins is now auto-promoted to admin, closing the chicken-and-egg gap where the only path to admin access was direct DB access. Self-heals if the last admin ever deletes their account. Also bumps nginx's proxy_read_timeout for api.ciagent.org to cover the enrichment refresh's synchronous funding-endpoint call (up to 5 minutes per NinjaPear's docs). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -20,6 +20,7 @@ from datetime import UTC, datetime
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import Settings
|
||||
from app.core.errors import NotFoundError
|
||||
from app.core.logging import get_logger
|
||||
from app.enrichment.base import EnrichmentProvider
|
||||
from app.models.company import Company
|
||||
@@ -174,3 +175,104 @@ async def enrich_company(
|
||||
failed_sections=list(errors.keys()),
|
||||
)
|
||||
return enrichment
|
||||
|
||||
|
||||
# Every entry here maps 1:1 onto one of NinjaPear's independent
|
||||
# per-company endpoints (see app/enrichment/ninjapear.py) - refreshing one
|
||||
# section never re-fetches or touches any of the others.
|
||||
REFRESHABLE_SECTIONS = ("details", "funding", "updates", "competitors", "products", "customers")
|
||||
|
||||
|
||||
async def refresh_section(
|
||||
db: AsyncSession,
|
||||
settings: Settings,
|
||||
provider: EnrichmentProvider,
|
||||
company: Company,
|
||||
section: str,
|
||||
) -> CompanyEnrichment:
|
||||
"""Re-runs a single NinjaPear endpoint for a company that already has
|
||||
an enrichment record but came back empty or failed for just this one
|
||||
section (e.g. `products` returning `[]` while everything else
|
||||
succeeded - a real, honest gap in NinjaPear's own coverage for that
|
||||
company, not a bug in this app). Only ever replaces this section's own
|
||||
slice of `data`/`errors` - every other section's stored data is left
|
||||
exactly as it was."""
|
||||
if section not in REFRESHABLE_SECTIONS:
|
||||
raise ValueError(f"Unknown enrichment section: {section}")
|
||||
|
||||
repo = CompanyEnrichmentRepository(db)
|
||||
existing = await repo.get_for_company(company.id)
|
||||
if existing is None:
|
||||
raise NotFoundError("Company has no enrichment record to refresh")
|
||||
|
||||
website = company.official_website
|
||||
data = dict(existing.data)
|
||||
errors = dict(existing.errors)
|
||||
credits_spent = existing.credits_spent or 0
|
||||
|
||||
try:
|
||||
if section == "details":
|
||||
details = await provider.get_company_details(company.name, website)
|
||||
data["employee_count"] = details.employee_count_range
|
||||
data["description"] = details.description
|
||||
data["industry"] = details.industry
|
||||
data["founded_year"] = details.founded_year
|
||||
data["specialties"] = details.specialties
|
||||
new_leadership = [m.model_dump() for m in details.leadership_team]
|
||||
# A details refresh re-fetches the leadership roster itself,
|
||||
# but not the separate per-leader work_email/person_profile
|
||||
# lookups - preserve those by matching on name so a refresh
|
||||
# never regresses contact info that was already found.
|
||||
old_by_name = {
|
||||
m.get("name"): m for m in (data.get("leadership_team") or []) if m.get("name")
|
||||
}
|
||||
for member in new_leadership:
|
||||
old = old_by_name.get(member.get("name"))
|
||||
if old:
|
||||
member["work_email"] = member.get("work_email") or old.get("work_email")
|
||||
member["profile_url"] = member.get("profile_url") or old.get("profile_url")
|
||||
member["bio"] = member.get("bio") or old.get("bio")
|
||||
data["leadership_team"] = new_leadership
|
||||
elif section == "funding":
|
||||
funding = await provider.get_funding(company.name, website)
|
||||
data["funding"] = funding.model_dump()
|
||||
elif section == "updates":
|
||||
updates = await provider.get_updates(company.name, website)
|
||||
data["recent_updates"] = [u.model_dump() for u in updates]
|
||||
elif section == "competitors":
|
||||
competitors = await provider.get_competitors(company.name, website)
|
||||
data["competitors"] = [c.model_dump() for c in competitors]
|
||||
elif section == "products":
|
||||
products = await provider.get_products(company.name, website)
|
||||
data["products"] = [p.model_dump() for p in products]
|
||||
elif section == "customers":
|
||||
customers = await provider.get_customers(company.name, website)
|
||||
data["customers"] = [c.model_dump() for c in customers]
|
||||
errors.pop(section, None)
|
||||
credits_spent += _CREDIT_COSTS.get(section, 0)
|
||||
except Exception as exc: # noqa: BLE001 - surfaced via `errors`, same as the initial run
|
||||
errors[section] = str(exc)
|
||||
logger.warning(
|
||||
"enrichment_section_refresh_failed",
|
||||
section=section,
|
||||
company_id=str(company.id),
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
new_status = EnrichmentStatus.PARTIAL if errors else EnrichmentStatus.COMPLETE
|
||||
enrichment = await repo.upsert(
|
||||
company.id,
|
||||
status=new_status,
|
||||
data=data,
|
||||
errors=errors,
|
||||
credits_spent=credits_spent,
|
||||
fetched_at=datetime.now(UTC),
|
||||
)
|
||||
await db.commit()
|
||||
logger.info(
|
||||
"company_enrichment_section_refreshed",
|
||||
company_id=str(company.id),
|
||||
section=section,
|
||||
succeeded=section not in errors,
|
||||
)
|
||||
return enrichment
|
||||
|
||||
Reference in New Issue
Block a user