YAML Output Format in Thalabus Agents

Overview

Several agents (AgentPlanner, AgentToolPreparer) emit YAML text instead of JSON. YAML is easier for small LLMs to produce correctly: it avoids mandatory quoting, escape sequences, and bracket matching.


Parsing: get_yaml_from_text()

File: src/core/llm/utils_yaml.py

The function is the single entry point for all YAML parsing. It is robust against common LLM mistakes:

Recovery strategy Trigger
Markdown fence strip Response wrapped in ```yaml … ``` or ``` … ```
JSON fallback yaml.safe_load fails but json.loads succeeds
Colon-repair (_repair_unquoted_colon_values) Plain scalar value contains a bare colon (e.g. key: foo: bar)
Prose-strip (_strip_prose_trailer) LLM appends a "Reasoning:" or numbered-list block after the YAML

If all recovery attempts fail, the function returns None and logs a WARNING.

Lenient YAML subset used in practice

The agents instruct the LLM to emit a lenient YAML subset: - Plain (unquoted) scalars for identifiers, plan names, tool names, short descriptions. - No mandatory quoting for most strings. Quote with "…" only when the value contains a colon (:) or other YAML metacharacter. - Avoid contractions ("does not" not "doesn't") to prevent apostrophe-quoting confusion. - Do NOT wrap output in markdown code fences. - 2-space indentation, list items with - prefix. - Omit null/empty fields entirely — do not include keys whose value would be null or empty.


AgentPlanner YAML format

AgentPlanner outputs a YAML structure describing the macro-plan.

Format

optional_prebuilt_complex_plan_id: pre-built-plan-xxx   # mutually exclusive with optional_complex_plan
optional_complex_plan:
  optional_doc_ids: docid-xxxxxx,docid-yyyyyy           # optional: focus goals on specific document(s)
  goals:
    - goal_sequence_id: Step-1
      goal: First goal description
      dependent_on_sequence_ids: []
      optional_recommended_plan: plan_call_tool_and_answer
      optional_recommended_tools:
        - WeatherInfo
    - goal_sequence_id: Step-2
      goal: Second goal description
      dependent_on_sequence_ids:
        - Step-1
      optional_recommended_plan: plan_simple_answer
optional_require_exact_time: false

Top-level fields reference

Field Type Description
optional_prebuilt_complex_plan_id string ID of a pre-built workflow (e.g. pre-built-plan-CleanDocument). Mutually exclusive with optional_complex_plan — omit the other field entirely when using this.
optional_complex_plan object Custom complex plan. Mutually exclusive with optional_prebuilt_complex_plan_id.
optional_complex_plan.optional_doc_ids string Comma-separated document IDs (e.g. docid-13) that scope all goals in this plan. Must start with docid-.
optional_complex_plan.goals list Ordered list of goal objects (see below).
optional_require_exact_time boolean Set to true only when the user explicitly requires the exact current time. Omit otherwise.

Goal object fields reference

Field Type Description
goal_sequence_id string Unique label for this step (e.g. Step-1). Referenced by dependent steps.
goal string Free-text description of what this goal must accomplish.
dependent_on_sequence_ids list of strings goal_sequence_id values of goals this step depends on. Empty list [] means no dependency (can run in parallel).
optional_recommended_plan string Plan template to use (e.g. plan_call_tool_and_answer, plan_rag_assistant_no_answer).
optional_recommended_tools list of strings Tool names the micro-planner should prefer for this step.

Key rules


AgentToolPreparer YAML format

AgentToolPreparer outputs either the literal string OK (no tool needed) or a YAML tool-call block.

No tool needed

OK

Plain string, not YAML. No surrounding quotes, brackets, or markup.

Single tool call

tool_call:
  - fname: WeatherInfo
    arguments:
      city_name_english: Milan

Multiple tool calls (same or different tools)

tool_call:
  - fname: WeatherInfo
    arguments:
      city_name_english: Rome
  - fname: WeatherInfo
    arguments:
      city_name_english: Paris
  - fname: WeatherInfo
    arguments:
      city_name_english: Berlin

Tool call with nested dict argument

tool_call:
  - fname: KnowledgeGraphEditor
    arguments:
      operation: ENTITY_CREATE_OR_UPDATE
      community_id: DogsAndCats
      label: Animal
      entity_id: bob
      attributes:
        description: A fluffy orange cat named Bob
        age: "3"

Tool call with list argument

tool_call:
  - fname: WebSearch
    arguments:
      query_array:
        - water quality publications
      include_domains:
        - http://example.com

Tool call with a value containing a colon (must be quoted)

tool_call:
  - fname: DatasetCreator
    arguments:
      dataset_name: my_dataset
      cypher_query: "MATCH (e:Entity:ApplicationCollaboration) WHERE e.name_consolidated IN ['CS-EES'] RETURN e"

Tool call with a list-of-objects argument (items)

tool_call:
  - fname: DocumentEditor
    arguments:
      operation: APPEND_TEXT
      doc_or_chapter_id: docid-16-c1
      elementIds: h1-3
      items:
        - new_tag: h2
          text: New Subheading
        - new_tag: p
          text: First paragraph under the new subheading.

Internal-thought pre-call (remains JSON)

The generate_internal_thought() method in AgentToolPreparer uses structured JSON output (enforced by the provider via structured_output schema). This is a separate internal pre-call — not visible to the user — and its format is not changed by the YAML refactoring. See LLM_HINT_FORMAT_STRUCTURED_OUTPUT in AgentToolPreparer.py and the corresponding instruction label reminder_prompt.


Multi-line strings and HTML content

Use the YAML literal block scalar (|) for any argument value that: - spans multiple lines, OR - contains HTML markup.

The | indicator preserves newlines exactly. The content is indented one level beyond the key. get_yaml_from_text() is fully compatible: _repair_unquoted_colon_values explicitly skips lines whose value starts with |, and the indented block-content lines never match the key-value repair pattern.

Avoid plain unquoted single-line HTML strings: if the HTML contains a colon (e.g., href="http://..." or style="color: red"), the colon-repair heuristic will wrap the value in double-quotes and escape interior " — this still parses correctly, but | is unambiguous and easier for small LLMs to produce.

Multi-line string argument

tool_call:
  - fname: DatasetCreateReport
    arguments:
      dataset_name: asylum_cases
      instructions_to_report: |
        For each document, summarize:
        1. Applicant background and country of origin
        2. Key events and timeline of the case
        3. Final outcome of the proceedings
      report_excludes_content: true

yaml.safe_load parses the | block as a single string with embedded \n newlines. Keys that follow the block (report_excludes_content) must be de-indented back to the same level as instructions_to_report.

HTML content argument

tool_call:
  - fname: DocumentEditor
    arguments:
      operation: REPLACE_TEXT
      doc_or_chapter_id: docid-16-c1
      elementIds: p-5
      text: |
        <p><strong>Important notice:</strong> This policy applies to all active cases.</p>
        <ul>
          <li>Filed before 2024-01-01</li>
          <li>Involving applicants from listed countries</li>
          <li>Pending review at cantonal level</li>
        </ul>

The HTML <, > characters are ordinary characters inside a | block — no escaping needed. Colons in HTML attributes (e.g., href="http://...") are also safe because the block-content lines are not key-value pairs and are never touched by the colon-repair heuristic.


Coding pattern

from src.core.llm import get_yaml_from_text

# After receiving a string response from the LLM:
if response is not None and isinstance(response, str) and response != "OK":
    parsed = get_yaml_from_text(response)
    if parsed is not None:
        response = parsed
# response is now either "OK" (str), a dict/list (parsed YAML), or the original string