Files
CIAgent/apps/api/app/services/report_markdown.py
T
saksham 1a4c80958f 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).
2026-08-05 10:48:20 -04:00

118 lines
4.0 KiB
Python

"""Renders a ReportContent (structured, from the LLM) into the Markdown
document the API/UI serve alongside the JSON - see spec section 6G for the
16-section layout this follows.
"""
from __future__ import annotations
from app.prompts.report_generation import Finding, InferredProject, ReportContent
def _render_findings(findings: list[Finding]) -> str:
if not findings:
return "_No findings for this section from the current evidence._\n"
lines = []
for f in findings:
lines.append(f"- **{f.headline}** _(confidence: {f.confidence.value.replace('_', ' ')})_")
lines.append(f" {f.summary}")
if f.date:
lines.append(f" _Date: {f.date}_")
return "\n".join(lines) + "\n"
def _render_projects(projects: list[InferredProject]) -> str:
if not projects:
return "_No inferred strategic projects from the current evidence._\n"
lines = []
for p in projects:
lines.append(
f"- **{p.project_name}** _({p.status.value.replace('_', ' ')}, confidence {p.confidence:.0%})_"
)
lines.append(f" {p.summary}")
if p.alternative_explanations:
lines.append(f" Alternative explanations: {'; '.join(p.alternative_explanations)}")
return "\n".join(lines) + "\n"
def _render_list(items: list[str]) -> str:
if not items:
return "_None noted._\n"
return "\n".join(f"- {item}" for item in items) + "\n"
def render_report_markdown(
content: ReportContent,
*,
company_name: str,
generated_at: str,
model_provider: str,
model_name: str,
sources: list[dict],
) -> str:
parts = [
f"# Competitive Intelligence Report: {company_name}",
f"_Generated {generated_at} · {model_provider}/{model_name}_",
"",
"## 1. Executive Summary",
content.executive_summary,
"",
"## 2. Company Overview",
content.company_overview,
"",
"## 3. Products and Service Landscape",
_render_findings(content.products_and_services),
"## 4. Recent Developments",
_render_findings(content.recent_developments),
"## 5. Strategic Initiatives",
_render_findings(content.strategic_initiatives),
"## 6. Key Project Signals",
_render_projects(content.key_inferred_projects),
"## 7. Competitive Positioning",
content.market_positioning,
"",
content.competitor_comparison,
"",
"## 8. SWOT Analysis",
"**Strengths**",
_render_list(content.swot.strengths),
"**Weaknesses**",
_render_list(content.swot.weaknesses),
"**Opportunities**",
_render_list(content.swot.opportunities),
"**Threats**",
_render_list(content.swot.threats),
"## 9. Hiring Signals",
_render_findings(content.hiring_signals),
"## 10. Product and Technology Signals",
_render_findings(content.technology_signals + content.patent_signals),
"## 11. Customer Sentiment",
content.customer_sentiment,
"",
"## 12. Financial and Regulatory Signals",
_render_findings(content.financial_signals + content.regulatory_and_legal_signals),
"## 13. Risks and Opportunities",
"**Risks**",
_render_list(content.risks),
"**Opportunities**",
_render_list(content.opportunities),
"## 14. Important Unknowns",
_render_list(content.unknowns_and_missing_data),
"## 15. Sources",
_render_sources(sources),
"## 16. Methodology and Limitations",
content.methodology,
"",
content.limitations,
]
return "\n".join(str(p) for p in parts)
def _render_sources(sources: list[dict]) -> str:
if not sources:
return "_No sources recorded for this report._\n"
lines = [
f"- [{s.get('title') or s.get('url')}]({s.get('url')}) — retrieved {s.get('retrieved_date')}"
for s in sources
]
return "\n".join(lines) + "\n"