Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 57 additions & 12 deletions app/agent/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
from app.agent.state import AnalysisState
from app.llm import get_llm_client
from app.prompts import render_prompt
from app.schemas import AnalysisRenderResponse
from app.schemas import AnalysisRenderResponse, ApprovedClaim
from app.utils.logging import get_logger

_ANALYSIS_RENDER_ATTEMPTS = 2
logger = get_logger(__name__)


def _build_analysis_render_prompt(
Expand All @@ -26,18 +28,62 @@ def _build_analysis_render_prompt(
)


def _render_fallback_analysis(claims: list[ApprovedClaim], answer_status: str) -> str:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this fallback for?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_render_fallback_analysis(...) is a deterministic safety path used when the normal LLM rendering step cannot produce a validated final answer. In the standard flow, we build approved claims from validated evidence and then ask the model to turn those claims into user-facing analysis. If that rendering step fails validation, returns unusable output, or if the workflow only has incomplete or caveat-level evidence, this function generates the response directly on the backend instead. It is not a single fixed fallback message; it still uses the approved claims that were already grounded in validated evidence, so it preserves whatever the system has actually established, such as a contradicted premise, a partial answer, or unresolved caveats. The purpose is to stay as close as possible to a reliable grounded conclusion while avoiding any unvalidated LLM wording or internal validator/orchestration details in the final user response.`

substantive = [claim for claim in claims if claim.kind != "caveat"]
caveats = [claim for claim in claims if claim.kind == "caveat"]

if answer_status == "contradicted_premise" and substantive:
lines = [substantive[0].statement]
lines.extend(f"- {claim.statement}" for claim in substantive[1:3])
if caveats:
lines.append("")
lines.append("Some requested breakdowns remain unresolved:")
lines.extend(f"- {claim.statement}" for claim in caveats[:2])
return "\n".join(lines)

if answer_status == "partial_answer" and substantive:
lines = ["The available evidence establishes part of the answer, but not the full requested breakdown."]
lines.extend(f"- {claim.statement}" for claim in substantive[:3])
if caveats:
lines.append("")
lines.append("Unresolved parts:")
lines.extend(f"- {claim.statement}" for claim in caveats[:2])
return "\n".join(lines)

if answer_status == "conflicting_evidence":
return "The available evidence is internally inconsistent, so Planera cannot validate a reliable conclusion."

if substantive:
lines = [substantive[0].statement]
lines.extend(f"- {claim.statement}" for claim in substantive[1:3])
return "\n".join(lines)

if caveats:
lines = [caveats[0].statement]
if len(caveats) > 1:
lines.extend(f"- {claim.statement}" for claim in caveats[1:3])
return "\n".join(lines)

return "The workflow could not validate a reliable comparison from the available results."


def run_analysis_narrative(state: AnalysisState) -> AnalysisState:
"""Produce markdown-friendly analysis from query, objective, and step outputs."""

workflow = state.get("workflow_status", "")
if workflow in ("planner_failed", "execution_failed"):
state["analysis"] = "The available evidence is insufficient because the workflow did not complete successfully."
return state

evidence = build_analysis_evidence(state)
approved_claims, expected_status = build_approved_claims(evidence)
caveat_steps = [
step
for step in state.get("executed_steps") or []
if step.get("status") in {"failed", "invalid"}
or (step.get("status") == "success" and step.get("validation_status") == "partial")
]
approved_claims, expected_status = build_approved_claims(evidence, unresolved_steps=caveat_steps)
state["answer_status"] = expected_status
if not approved_claims:
state["analysis"] = "The approved claims are insufficient to answer the question with the available evidence."
state["analysis"] = _render_fallback_analysis([], expected_status)
return state
if all(claim.kind == "caveat" for claim in approved_claims):
state["analysis"] = _render_fallback_analysis(approved_claims, expected_status)
return state

approved_claims_json = json.dumps([claim.model_dump() for claim in approved_claims], indent=2)
Expand All @@ -56,11 +102,10 @@ def run_analysis_narrative(state: AnalysisState) -> AnalysisState:
raise
continue

state["answer_status"] = parsed.answer_status
state["analysis"] = parsed.analysis_markdown.strip() or "No analysis text was returned."
return state
except Exception as exc: # pragma: no cover - defensive
state["analysis"] = (
f"The analysis step could not complete ({exc!s}). "
"Review the executed steps and trace for raw outputs."
)
logger.warning("Analysis rendering fell back to deterministic summary: %s", exc, exc_info=True)
state["analysis"] = _render_fallback_analysis(approved_claims, expected_status)
return state
Loading
Loading