Skip to content

feat: LLM based prose format conistency workflow - #140

Open
hpnyaggerman wants to merge 20 commits into
OrbFrontend:mainfrom
hpnyaggerman:nyagman-dev-new-prose-format
Open

feat: LLM based prose format conistency workflow#140
hpnyaggerman wants to merge 20 commits into
OrbFrontend:mainfrom
hpnyaggerman:nyagman-dev-new-prose-format

Conversation

@hpnyaggerman

Copy link
Copy Markdown
Contributor

This workflow is an implementation of an idea I described in #55. It is not perfect but it works and it has a shape I believe is viable. Up to you @OrbFrontend whether you want to merge it but I believe no one will be at a loss with this workflow in the default Orb selection.

build_direction_note_prompt opens an [OOC: aside (DIRECTION_NOTE_PREAMBLE) but
never closed it, unlike the other pass builders. Adopt the same close-with-]
convention ahead of merging main's cf6cad7, which adds it to the four
pre-existing OOC builders, so the merged tree stays consistent.
Main upstreamed the inherited direction-notes feature as OrbFrontend#104 (dab5ad2) and
added the Gemma-4 native tool-call parser (ef05ba0). The direction-notes surface
is resolved wholesale to main's OrbFrontend#104: its upstreamed form is ahead of the dev
version this branch inherited (per-fragment director calls, the characterization
seed replacing story_direction, rewritten preambles), so no branch-side
direction-notes is kept. The prose-format workflow is untouched -- main never
references it -- and test_tool_registry keeps both sides' additive edits.
Both sides independently rewrote the same two assertions in
TestBuiltinToolNames and TestStandaloneToolsBaseline once a workflow
started contributing standalone tools, reaching the same set equation by
different spellings. Adopting main's text makes the hunks identical, so
the upcoming merge sees no conflict here at all.
Main replaced the ad-hoc plugin surface with a single facade,
frontend/workflow_api.js, and added scripts/check_frontend_layers.py to
lint.sh to enforce it. A module under frontend/workflows/ may now import
only that facade plus its own relative files, so the deep imports of
state.js, api.js, utils.js and modal.js all become hard errors.

The same check ratchets the number of inline on*= attributes across the
frontend and refuses to let it grow. Twelve of them here move to the
facade's delegated dispatcher: markup carries data-wf-action, and
initConfigPanel registers the handlers through registerAction instead of
planting window.pf* globals.

The global state object S is deliberately absent from the facade, so the
two reads of S.activeConvId go through its getActiveConvId accessor.
Main split off-turn LLM work into a writer lane and an agent lane, gave
OnDemandCtx and RegenCtx an agent_client plus agent_model_name, and added
a model_name argument to forced_tool_call. image_gen's off-turn hooks
already route their tool calls that way; the manual Analyze button now
does the same.

run_analyzer takes the client and model as arguments rather than reading
ctx.client, because only the off-turn contexts resolve a second lane.
PreCtx does not, so the automatic pre-pipeline attempt keeps passing its
single client -- see the note at the on-demand call site for what that
asymmetry costs.
The only conflict is the workflow registry in backend/workflows, where
main registers image_gen and this branch registers prose_format_llm. Both
additions stay: main's block keeps its place and prose_format_llm appends
after it, so the manifest order main established is unchanged. Hook order
is unaffected either way, since the post-pipeline fan-out sorts on the
explicit priority rather than on registration order.
Main raised the Python floor to 3.11 and added ruff's UP rules, which
reject the deprecated typing aliases for Mapping, Sequence and Iterable.
These three modules were written before that and fail ruff as merged.
@OrbFrontend

Copy link
Copy Markdown
Owner

@hpnyaggerman I'll give this a spin later. I actually trained a very small pov/tense classifier here as I said I would - miliseconds on a CPU for 256 tokens: https://huggingface.co/chartreuse-verte/ettin-povtense-17m, it's already being used for auto POV in image gen workflow.

I'll see if it can be integrated into the workflow to assist the agent model.

# Conflicts:
#	backend/workflows/__init__.py
Main now withholds tools and tool_choice from every chat request to an endpoint whose profile sets structured_tool_calls, deriving a strict response_format from the forced tool's parameters and rebuilding the reply from a synthesized tool-call message. Every path in this workflow is a forced tool call, so all three of its schemas ride that rewrite and none of them was exercised through it.

The cases assert what the rewrite can quietly break: strictify_schema closes each array's item object, and every item property has to stay a plain string, because a leaf widened to ["string", "null"] decodes as None and is then dropped by the string guards in violations.py and patching.py -- an empty result rather than a visible failure. They sit in this workflow's own suite because the schemas are its own; test_structured_tool_calls.py already covers the transport itself.
@OrbFrontend OrbFrontend linked an issue Aug 4, 2026 that may be closed by this pull request
@OrbFrontend

OrbFrontend commented Aug 4, 2026

Copy link
Copy Markdown
Owner

@hpnyaggerman

  1. Can you fix the modal UI so it looks presentable?
image
  1. What's the add element function for? Are there other use cases besides narration, speech, inner monologue, quotation that I can't think of?

  2. Incompatibility with existing algormith-based format consistency:

INFO:backend.workflows.format_consistency.hooks:format-consistency: normalized draft (dialogue=quoted, narration=asterisk -> dialogue=bare, narration=asterisk)
INFO:backend.inference.client:LLM complete: model=default, tools=["prose_format_report"], tool_choice={'type': 'function', 'function': {'name': 'prose_format_report'}}
INFO:httpx:HTTP Request: POST http://localhost:5000/v1/chat/completions "HTTP/1.1 200 OK"
INFO:backend.inference.client:LLM complete: assembled keys=['tool_calls', 'finish_reason'], has_tool_calls=True, content_len=null, usage={'completion_tokens': 157, 'prompt_tokens': 556, 'total_tokens': 713, 'prompt_tokens_details': {'cached_tokens': 236}}
INFO:backend.inference.client:LLM complete: model=default, tools=["prose_format_patch"], tool_choice={'type': 'function', 'function': {'name': 'prose_format_patch'}}
INFO:httpx:HTTP Request: POST http://localhost:5000/v1/chat/completions "HTTP/1.1 200 OK"
INFO:backend.inference.client:LLM complete: assembled keys=['tool_calls', 'finish_reason'], has_tool_calls=True, content_len=null, usage={'completion_tokens': 257, 'prompt_tokens': 655, 'total_tokens': 912, 'prompt_tokens_details': {'cached_tokens': 221}}
INFO:backend.inference.client:LLM complete: model=default, tools=["prose_format_report"], tool_choice={'type': 'function', 'function': {'name': 'prose_format_report'}}
INFO:httpx:HTTP Request: POST http://localhost:5000/v1/chat/completions "HTTP/1.1 200 OK"
INFO:backend.inference.client:LLM complete: assembled keys=['tool_calls', 'finish_reason'], has_tool_calls=True, content_len=null, usage={'completion_tokens': 157, 'prompt_tokens': 556, 'total_tokens': 713, 'prompt_tokens_details': {'cached_tokens': 551}}
INFO:backend.inference.kv_tracker:KV cache report  (provider = truth; local = msgs-prefix + tools-match, template-dependent):
  director:direct_scene         provider: cached=2198/2743 tok (80.1%) [prompt_tokens_details.cached_tokens]  |  local: msgs_overlap=6602/8852c (74.6%) vs prev-turn 'director:direct_scene'; tools_MATCH
  writer                        provider: cached=2359/2507 tok (94.1%) [prompt_tokens_details.cached_tokens]  |  local: msgs_overlap=7251/7945c (91.3%) vs 'director:direct_scene'; tools_MATCH
  forced:prose_format_report    provider: cached=236/556 tok (42.4%) [prompt_tokens_details.cached_tokens]  |  local: msgs_overlap=22/1852c (1.2%) vs 'writer'; tools_DIFFER (prev=2414c, this=669c)
  forced:prose_format_patch     provider: cached=221/655 tok (33.7%) [prompt_tokens_details.cached_tokens]  |  local: msgs_overlap=35/2265c (1.5%) vs 'forced:prose_format_report'; tools_DIFFER (prev=669c, this=641c)
  forced:prose_format_report    provider: cached=551/556 tok (99.1%) [prompt_tokens_details.cached_tokens]  |  local: msgs_overlap=35/1852c (1.9%) vs 'forced:prose_format_patch'; tools_DIFFER (prev=641c, this=669c)

Re-judge's prompt is identical to judge, meaning the enforcer emitted 257 tokens that did nothing - because the algorithm-based one already fixed everything. Still the judge flagged it anyway. This is Gemma 4 31B.

KV-cache report above is another eyebrow-raising thing. modern providers have different lanes for different prefix hashes but some might configure their engine to only have a single lane per hash, and the hash can be derived from user id (This is my speculation, I know it's possible to be stingy and "tweak" by disabling reuse and limit allocated memory pages, but I can't say if anyone actually does it). This is acceptable for now, maybe I'll add a kv cache bust warning to the user and leave them a hint what's breaking in the future, and they can switch between minimal and full modes if not compatible.

@hpnyaggerman

Copy link
Copy Markdown
Contributor Author

@OrbFrontend Will look into it tomorrow or slightly later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Prose Format Workflow

2 participants