Files
lda-wf/examples/lda_report_workflow/report_source.py
T

274 lines
7.9 KiB
Python

from __future__ import annotations
from pydantic import BaseModel, Field
from wf_authoring import node
from .document_source import DocumentText
class AnalyzeDocumentsInput(BaseModel):
documents: list[DocumentText]
class Finding(BaseModel):
source: str
summary: str
risks: list[str] = Field(default_factory=list)
actions: list[str] = Field(default_factory=list)
class AnalyzeDocumentsOutput(BaseModel):
analysis: list[Finding]
class BuildReportInput(BaseModel):
analysis: list[Finding]
class ReadinessReport(BaseModel):
title: str
summary: str
achievements: list[str]
risks: list[str]
next_actions: list[str]
class BuildReportOutput(BaseModel):
report: ReadinessReport
markdown: str
class CreateIssueDraftsInput(BaseModel):
report: ReadinessReport
class ProposedIssue(BaseModel):
id: str
title: str
body: str
severity: str = "medium"
class CreateIssueDraftsOutput(BaseModel):
issues: list[ProposedIssue]
class CreatedIssue(BaseModel):
id: str
title: str
url: str
class FinaliseReportInput(BaseModel):
report: ReadinessReport
created_issues: list[CreatedIssue] = Field(default_factory=list)
approved: bool
selected_issue_ids: list[str] = Field(default_factory=list)
comment: str | None = None
class FinalReportOutput(BaseModel):
approved: bool
markdown: str
created_issues: list[CreatedIssue]
selected_issue_ids: list[str]
comment: str | None = None
class RecordRevisionRequestInput(BaseModel):
comment: str | None = None
@node(
name="analyze_documents",
description="Extract deterministic findings from lda.chat project documents.",
)
def analyze_documents(payload: AnalyzeDocumentsInput) -> AnalyzeDocumentsOutput:
return _analyze_documents(payload)
@node(
name="build_report",
description="Build a typed lda.chat readiness report from document findings.",
)
def build_report(payload: BuildReportInput) -> BuildReportOutput:
return _build_report(payload)
@node(
name="create_issue_drafts",
description="Create proposed local issue drafts from report risks and next actions.",
)
def create_issue_drafts(payload: CreateIssueDraftsInput) -> CreateIssueDraftsOutput:
return _create_issue_drafts(payload)
@node(
name="finalise_report",
description="Render the approved report and include created issue references.",
)
def finalise_report(payload: FinaliseReportInput) -> FinalReportOutput:
return _finalise_report(payload)
@node(
name="record_revision_request",
description="Return a cancelled report result when the human asks for revision.",
)
def record_revision_request(payload: RecordRevisionRequestInput) -> FinalReportOutput:
return _record_revision_request(payload)
def _analyze_documents(payload: AnalyzeDocumentsInput) -> AnalyzeDocumentsOutput:
findings: list[Finding] = []
for document in payload.documents:
text = document.text.lower()
risks = _lines_after(document.text, "Material risks:")
actions = _lines_after(document.text, "Near-term:") or _lines_after(
document.text, "Mitigations:"
)
if "workflow substrate" in text:
summary = (
"lda.chat is positioned as a workflow substrate for external agents."
)
elif "evaluation" in text:
summary = "Evaluation evidence is bounded and should be presented as operational evidence."
elif "risk" in text:
summary = "The current risk register emphasizes framing, evaluation, and storage limits."
elif "roadmap" in text:
summary = "Near-term roadmap focuses on typed interrupts, deterministic demos, and a console."
else:
summary = document.text.splitlines()[0].lstrip("# ").strip()
findings.append(
Finding(
source=document.name,
summary=summary,
risks=risks[:3],
actions=actions[:3],
)
)
return AnalyzeDocumentsOutput(analysis=findings)
def _build_report(payload: BuildReportInput) -> BuildReportOutput:
achievements = [
finding.summary
for finding in payload.analysis
if "risk register" not in finding.summary
]
risks = _unique(item for finding in payload.analysis for item in finding.risks)
next_actions = _unique(
item for finding in payload.analysis for item in finding.actions
)
report = ReadinessReport(
title="lda.chat Thesis And Project Readiness Report",
summary=(
"lda.chat is a typed workflow substrate with lifecycle records, "
"source-provider boundaries, and agent-operable CLI/RPC surfaces."
),
achievements=achievements[:6],
risks=risks[:6],
next_actions=next_actions[:6],
)
return BuildReportOutput(report=report, markdown=_render_report(report))
def _create_issue_drafts(payload: CreateIssueDraftsInput) -> CreateIssueDraftsOutput:
issues: list[ProposedIssue] = []
for index, risk in enumerate(payload.report.risks[:4], start=1):
issue_id = f"risk-{index}"
issues.append(
ProposedIssue(
id=issue_id,
title=risk.rstrip("."),
body=f"Track mitigation for: {risk}",
severity="high" if "title" in risk.lower() else "medium",
)
)
if not issues:
issues.append(
ProposedIssue(
id="follow-up-1",
title="Review thesis demo readiness",
body="Confirm the prepared workflow and replay are ready for defense.",
severity="medium",
)
)
return CreateIssueDraftsOutput(issues=issues)
def _finalise_report(payload: FinaliseReportInput) -> FinalReportOutput:
markdown = _render_report(payload.report)
if payload.created_issues:
markdown += "\n\nCreated issues:\n"
markdown += "\n".join(
f"- {issue.id}: {issue.title} ({issue.url})"
for issue in payload.created_issues
)
if payload.comment:
markdown += f"\n\nApproval comment: {payload.comment}"
return FinalReportOutput(
approved=payload.approved,
markdown=markdown,
created_issues=payload.created_issues,
selected_issue_ids=payload.selected_issue_ids,
comment=payload.comment,
)
def _record_revision_request(payload: RecordRevisionRequestInput) -> FinalReportOutput:
comment = payload.comment or "Revision requested."
return FinalReportOutput(
approved=False,
markdown=f"# Revision Requested\n\n{comment}",
created_issues=[],
selected_issue_ids=[],
comment=comment,
)
def _render_report(report: ReadinessReport) -> str:
lines = [f"# {report.title}", "", "Summary:", report.summary, ""]
lines.append("Achievements:")
lines.extend(f"- {item}" for item in report.achievements)
lines.extend(["", "Risks:"])
lines.extend(f"- {item}" for item in report.risks)
lines.extend(["", "Next actions:"])
lines.extend(f"- {item}" for item in report.next_actions)
return "\n".join(lines)
def _lines_after(text: str, heading: str) -> list[str]:
lines: list[str] = []
active = False
for raw in text.splitlines():
line = raw.strip()
if line == heading:
active = True
continue
if active and line.endswith(":"):
break
if active and line.startswith("- "):
lines.append(line.removeprefix("- ").strip())
return lines
def _unique(items: object) -> list[str]:
seen: set[str] = set()
values: list[str] = []
for item in items: # type: ignore[union-attr]
if isinstance(item, str) and item and item not in seen:
seen.add(item)
values.append(item)
return values
registry = [
analyze_documents,
build_report,
create_issue_drafts,
finalise_report,
record_revision_request,
]