Packages

A Fair Source multi-agent runtime with deterministic agent scoring and replayable run history.

Retired package: Deprecated - superseded — operator console moved to the Syntropy app

Current section

Files

Jump to
syntropy lib syntropy output_quality.ex
Raw

lib/syntropy/output_quality.ex

defmodule Syntropy.OutputQuality do
@moduledoc """
Deterministic post-processing for model output shown in run envelopes.
This module does not judge whether the recommendation is correct. It makes the
retained synthesis display-safe, extracts the expected decision-review
sections when present, and derives a conservative quality status from existing
execution, inquiry, and citation signals.
"""
@type structured_answer :: %{
recommendation: String.t(),
evidence_backed_findings: [String.t()],
risks: [String.t()],
next_actions: [String.t()],
confidence: String.t(),
needs_more_evidence: boolean(),
needs_more_evidence_reason: String.t() | nil,
raw: String.t()
}
@type output_quality :: %{
status: String.t(),
citation_status: String.t() | nil,
warnings: [String.t()],
scratchpad_removed: boolean(),
missing_sections: [String.t()]
}
@spec assess(String.t(), keyword()) :: %{
structured_answer: structured_answer(),
output_quality: output_quality()
}
def assess(synthesis, opts \\ []) do
raw = to_string(synthesis || "")
{cleaned, scratchpad_removed} = clean_output(raw)
sections = parse_sections(cleaned)
citation_audit = Keyword.get(opts, :citation_audit)
execution_quality = Keyword.get(opts, :execution_quality)
inquiry_quality = Keyword.get(opts, :inquiry_quality)
source_review = Keyword.get(opts, :source_review)
missing_sections = missing_sections(sections)
status =
citation_audit
|> quality_status(execution_quality, inquiry_quality, source_review, cleaned)
|> maybe_downgrade_for_missing_sections(missing_sections, cleaned)
warnings =
quality_warnings(
status,
citation_audit,
execution_quality,
inquiry_quality,
source_review,
missing_sections
)
confidence = confidence(sections, status)
needs_more_evidence_reason =
needs_more_evidence_reason(sections, citation_audit, inquiry_quality, source_review)
%{
structured_answer: %{
recommendation: recommendation(sections, cleaned),
evidence_backed_findings: list_section(sections, :findings),
risks: list_section(sections, :risks),
next_actions: list_section(sections, :next_actions),
confidence: confidence,
needs_more_evidence: not is_nil(needs_more_evidence_reason),
needs_more_evidence_reason: needs_more_evidence_reason,
raw: raw
},
output_quality: %{
status: status,
citation_status: field(citation_audit, :status),
warnings: warnings,
scratchpad_removed: scratchpad_removed,
missing_sections: missing_sections
}
}
end
@spec failed(term()) :: output_quality()
def failed(reason) do
%{
status: "weak",
citation_status: nil,
warnings: ["The run failed before a usable synthesis was retained: #{inspect(reason)}"],
scratchpad_removed: false,
missing_sections: ~w(recommendation evidence_backed_findings risks next_actions)
}
end
defp clean_output(raw) do
text = String.trim(raw)
{from_first_heading, removed_prefix?} = trim_to_first_heading(text)
cleaned_lines =
from_first_heading
|> String.split("\n")
|> Enum.reject(&scratchpad_line?/1)
cleaned = cleaned_lines |> Enum.join("\n") |> String.trim()
{cleaned, removed_prefix? or cleaned != text}
end
defp trim_to_first_heading(text) do
case Regex.run(section_heading_regex(), text, return: :index) do
[{index, _length} | _rest] when index > 0 ->
{binary_part(text, index, byte_size(text) - index), true}
_other ->
{text, false}
end
end
defp scratchpad_line?(line) do
Regex.match?(
~r/^\s*(analysis|thinking|scratchpad|plan|we need|i need|need to|let'?s craft|let us craft)\b/i,
line
)
end
defp parse_sections(""), do: %{}
defp parse_sections(text) do
text
|> String.split("\n")
|> Enum.reduce(%{current: nil, sections: %{}}, &parse_section_line/2)
|> Map.fetch!(:sections)
|> Map.new(fn {key, lines} ->
{key, lines |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == ""))}
end)
end
defp parse_section_line(line, acc) do
case Regex.run(section_heading_regex(), line) do
[_match, heading, rest] ->
section = section_key(heading)
append_section(%{acc | current: section}, section, rest)
_other ->
if acc.current do
append_section(acc, acc.current, line)
else
append_section(%{acc | current: :recommendation}, :recommendation, line)
end
end
end
defp append_section(acc, section, line) do
line = String.trim(line || "")
if line == "" do
acc
else
update_in(acc, [:sections, section], fn lines -> (lines || []) ++ [line] end)
end
end
defp section_heading_regex do
~r/^\s*(Recommendation|Verdict|Evidence-backed findings|Findings|Why|Evidence from selected perspectives|Evidence|Risks|Risk|Next actions|Action|Confidence|Needs more evidence|Insufficient evidence)\s*:\s*(.*)$/i
end
defp section_key(heading) do
case heading |> String.downcase() |> String.trim() do
value when value in ["recommendation", "verdict"] ->
:recommendation
value
when value in [
"evidence-backed findings",
"findings",
"why",
"evidence from selected perspectives",
"evidence"
] ->
:findings
value when value in ["risks", "risk"] ->
:risks
value when value in ["next actions", "action"] ->
:next_actions
"confidence" ->
:confidence
value when value in ["needs more evidence", "insufficient evidence"] ->
:needs_more_evidence
end
end
defp recommendation(sections, cleaned) do
sections
|> Map.get(:recommendation, [])
|> clean_items()
|> Enum.join(" ")
|> empty_as(cleaned)
|> String.trim()
end
defp list_section(sections, key) do
sections
|> Map.get(key, [])
|> clean_items()
|> Enum.take(6)
end
defp clean_items(lines) do
lines
|> Enum.flat_map(&split_inline_items/1)
|> Enum.map(&String.replace(&1, ~r/^\s*(?:[-*]|\d+[.)])\s*/, ""))
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
end
defp split_inline_items(line) do
line
|> String.split(~r/\s+-\s+/, trim: true)
|> case do
[] -> []
values -> values
end
end
defp missing_sections(sections) do
[
{:recommendation, "recommendation"},
{:findings, "evidence_backed_findings"},
{:risks, "risks"},
{:next_actions, "next_actions"}
]
|> Enum.filter(fn {key, _label} -> sections |> Map.get(key, []) |> clean_items() == [] end)
|> Enum.map(fn {_key, label} -> label end)
end
defp confidence(sections, status) do
explicit =
sections
|> Map.get(:confidence, [])
|> Enum.join(" ")
|> normalize_confidence()
explicit
|> fallback(derived_confidence(status))
|> downgrade_confidence(status)
end
defp normalize_confidence(value) do
value = value |> to_string() |> String.downcase()
cond do
String.contains?(value, "high") -> "high"
String.contains?(value, "medium") -> "medium"
String.contains?(value, "low") -> "low"
true -> nil
end
end
defp derived_confidence("usable"), do: "high"
defp derived_confidence("needs_review"), do: "medium"
defp derived_confidence(_status), do: "low"
defp downgrade_confidence("high", "needs_review"), do: "medium"
defp downgrade_confidence(_confidence, "weak"), do: "low"
defp downgrade_confidence(confidence, _status), do: confidence
defp needs_more_evidence_reason(sections, citation_audit, inquiry_quality, source_review) do
explicit = explicit_evidence_reason(sections)
cond do
explicit ->
explicit
no_source_evidence?(citation_audit) ->
"No source evidence was available for citation."
ignored_source_evidence?(citation_audit) ->
"The answer did not cite the supplied source evidence."
incomplete_source_inquiry?(source_review, inquiry_quality) ->
"The run did not complete expanded source inquiry."
true ->
nil
end
end
defp explicit_evidence_reason(sections) do
explicit =
sections
|> Map.get(:needs_more_evidence, [])
|> clean_items()
|> Enum.join(" ")
|> empty_to_nil()
if is_binary(explicit) and not negative_evidence_need?(explicit) do
explicit
else
nil
end
end
defp no_source_evidence?(citation_audit) do
field(citation_audit, :valid_citation_count) == 0 and
field(citation_audit, :supplied_evidence_count) in [nil, 0]
end
defp ignored_source_evidence?(citation_audit) do
field(citation_audit, :valid_citation_count) == 0 and
is_integer(field(citation_audit, :supplied_evidence_count))
end
defp incomplete_source_inquiry?(source_review, inquiry_quality) do
source_inquiry_required?(source_review) and
field(inquiry_quality, :status) in ["static_only", "static_fallback", "failed"]
end
defp negative_evidence_need?(value) do
value
|> String.downcase()
|> String.trim()
|> then(&(&1 in ["no", "none", "not needed", "no."]))
end
defp quality_status(
_citation_audit,
execution_quality,
_inquiry_quality,
_source_review,
cleaned
)
when cleaned == "" do
if field(execution_quality, :status) == "running", do: "needs_review", else: "weak"
end
defp quality_status(citation_audit, execution_quality, inquiry_quality, source_review, _cleaned) do
cond do
weak_quality?(citation_audit, execution_quality, source_review) ->
"weak"
needs_review_quality?(citation_audit, execution_quality, inquiry_quality, source_review) ->
"needs_review"
true ->
"usable"
end
end
defp weak_quality?(citation_audit, execution_quality, source_review) do
field(execution_quality, :status) == "failed" or
(is_map(source_review) and field(citation_audit, :unknown_evidence_count) |> positive?()) or
(is_map(source_review) and field(citation_audit, :valid_citation_count) == 0)
end
defp needs_review_quality?(citation_audit, execution_quality, inquiry_quality, source_review) do
field(execution_quality, :status) == "partial" or
(source_inquiry_required?(source_review) and
field(inquiry_quality, :status) in [
"partial",
"static_only",
"static_fallback",
"failed"
]) or
blocking_unsupported_claim_count(citation_audit) |> positive?() or
field(citation_audit, :missing_citation_count) |> positive?() or
field(citation_audit, :status) == "warning"
end
defp maybe_downgrade_for_missing_sections("usable", missing_sections, cleaned)
when missing_sections != [] and cleaned != "",
do: "needs_review"
defp maybe_downgrade_for_missing_sections(status, _missing_sections, _cleaned), do: status
defp quality_warnings(
status,
citation_audit,
execution_quality,
inquiry_quality,
source_review,
missing_sections
) do
[]
|> maybe_warning(
status == "weak",
"Output quality is weak; inspect evidence and raw output before acting."
)
|> maybe_warning(
status == "needs_review",
"Output needs review before it is treated as a final recommendation."
)
|> maybe_warning(
field(execution_quality, :status) == "partial",
"Only some selected agents contributed to this run."
)
|> maybe_warning(
source_inquiry_required?(source_review) and
field(inquiry_quality, :status) in ["static_only", "static_fallback", "failed"],
"Expanded source inquiry was incomplete."
)
|> add_warnings(field(citation_audit, :warnings) || [])
|> maybe_warning(
missing_sections != [],
"The model did not return every requested answer section."
)
|> Enum.uniq()
end
defp maybe_warning(warnings, true, warning), do: warnings ++ [warning]
defp maybe_warning(warnings, false, _warning), do: warnings
defp add_warnings(warnings, extra) when is_list(extra), do: warnings ++ extra
defp add_warnings(warnings, _extra), do: warnings
defp source_inquiry_required?(nil), do: false
defp source_inquiry_required?(source_review) when is_map(source_review) do
source_review
|> field(:engine)
|> case do
"github" -> false
_other -> true
end
end
defp source_inquiry_required?(_source_review), do: false
defp blocking_unsupported_claim_count(citation_audit) do
case field(citation_audit, :blocking_unsupported_claim_count) do
value when is_integer(value) or is_float(value) -> value
_other -> field(citation_audit, :unsupported_claim_count)
end
end
defp field(nil, _key), do: nil
defp field(map, key) when is_map(map) and is_atom(key) do
Map.get(map, key) || Map.get(map, Atom.to_string(key))
end
defp field(map, key) when is_map(map), do: Map.get(map, key)
defp field(_value, _key), do: nil
defp positive?(value) when is_integer(value) or is_float(value), do: value > 0
defp positive?(_value), do: false
defp fallback(nil, fallback), do: fallback
defp fallback("", fallback), do: fallback
defp fallback(value, _fallback), do: value
defp empty_as("", fallback), do: fallback
defp empty_as(value, _fallback), do: value
defp empty_to_nil(""), do: nil
defp empty_to_nil(value), do: value
end