feat(templates): add invoicing collections agent#6335
feat(templates): add invoicing collections agent#6335Ttian18 wants to merge 8 commits intoaden-hive:mainfrom
Conversation
PR Requirements WarningThis PR does not meet the contribution requirements. Missing: No linked issue found. To fix:
Exception: To bypass this requirement, you can:
Micro-fix requirements (must meet ALL):
Why is this required? See #472 for details. |
Config, sample data, and node definitions for the invoicing & collections agent template (issue aden-hive#5181). Includes COLLECTION_CONFIG with aging buckets/thresholds, 15-row sample CSV, and 6 node specs (scan, classify, remind, judge, HITL escalate, report).
- Add agent.py stub (imports only, goal/edges/class TBD) - Set classify-overdue max_node_visits to 3 for bounded feedback loop
- Add goal, edges, and agent class to agent.py - Add __init__.py with all module-level exports - Add __main__.py CLI entry point - Add mcp_servers.json for tool server config - Judge node uses CONDITIONAL edges for feedback loop - Escalation review is client-facing for human-in-the-loop
- Pass COLLECTION_CONFIG as initial input so classify, send-reminders, and judge nodes can access it at runtime - Switch __main__.py to @cli.command() pattern matching other templates
- Update sample_invoices.csv with dates covering all aging buckets (current, 30-day, 60-day, 90+) and high-value escalation cases - Default CLI to bundled sample data when no --file is provided
6eb28b2 to
53dcb22
Compare
📝 WalkthroughWalkthroughIntroduces a new runnable invoicing & collections agent template at Changes
Sequence DiagramsequenceDiagram
participant CLI as CLI/User
participant Runtime as Agent Runtime
participant ScanN as scan-invoices<br/>Node
participant ClassN as classify-overdue<br/>Node
participant SendN as send-reminders<br/>Node
participant JudgeN as judge-reminders<br/>Node
participant EscN as escalate-review<br/>Node (HITL)
participant ReportN as generate-report<br/>Node
participant Tools as Tools<br/>(CSV, Email)
CLI->>Runtime: run(invoice_file, context)
activate Runtime
Runtime->>ScanN: execute (invoice_file_path)
activate ScanN
ScanN->>Tools: csv_read, csv_sql
Tools-->>ScanN: all_invoices, invoice_count
ScanN-->>Runtime: scan_summary
deactivate ScanN
Runtime->>ClassN: execute (all_invoices, collection_config)
activate ClassN
ClassN-->>Runtime: classified_invoices, reminder_accounts, escalation_accounts
deactivate ClassN
Runtime->>SendN: execute (reminder_accounts, collection_config)
activate SendN
SendN->>Tools: send_email (reminder templates)
Tools-->>SendN: reminders_sent
SendN-->>Runtime: reminder_summary
deactivate SendN
Runtime->>JudgeN: execute (classified, reminders_sent, config)
activate JudgeN
JudgeN-->>Runtime: judgment_passed or needs_reclassification
deactivate JudgeN
alt judgment_passed
Runtime->>EscN: execute (escalation_accounts, classified_invoices)
activate EscN
Note over EscN: Human reviews & approves<br/>escalation accounts (HITL)
EscN-->>Runtime: approval_result, approved_accounts
deactivate EscN
else needs_reclassification
Runtime->>ClassN: loop back to classify-overdue
end
Runtime->>ReportN: execute (classified, reminders_sent, escalation_accounts)
activate ReportN
ReportN->>Tools: csv_write (AR aging report)
Tools-->>ReportN: report_file_path, ar_aging_totals
ReportN-->>Runtime: report_summary
deactivate ReportN
Runtime-->>CLI: ExecutionResult {success, output}
deactivate Runtime
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (13)
examples/templates/invoicing_collections_agent/__main__.py (4)
13-20: Add type hints to function signature.Per coding guidelines, all function signatures should have type hints.
Suggested fix
-def setup_logging(verbose=False, debug=False): +def setup_logging(verbose: bool = False, debug: bool = False) -> None: if debug: level, fmt = logging.DEBUG, "%(asctime)s %(name)s: %(message)s" elif verbose: level, fmt = logging.INFO, "%(message)s" else: level, fmt = logging.WARNING, "%(levelname)s: %(message)s" logging.basicConfig(level=level, format=fmt, stream=sys.stderr)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/__main__.py` around lines 13 - 20, Add type hints to the setup_logging function signature: annotate the parameters verbose and debug as bool and the return type as None (i.e., def setup_logging(verbose: bool = False, debug: bool = False) -> None). No other changes to behavior; keep existing default values and body (logging.basicConfig call). Update any necessary imports if your linter requires typing imports (none required for bool/None).
38-56: Add type hints to command function.Per coding guidelines, function signatures should include type hints.
Suggested fix
-def run(invoice_file, verbose): +def run(invoice_file: str | None, verbose: bool) -> None: """Execute the agent."""🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/__main__.py` around lines 38 - 56, The run function lacks type hints; update the signature of run to include types (e.g., invoice_file: Optional[str], verbose: bool) and a return type (-> None), import Optional from typing, and ensure any local usages that depend on the type (like passing invoice_file or "") remain valid; specifically modify the def run(invoice_file, verbose): declaration to include the type annotations and add the necessary typing import at the top of the module.
59-67: Add return type hint.Suggested fix
`@cli.command`() -def info(): +def info() -> None: """Show agent info."""🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/__main__.py` around lines 59 - 67, The info() CLI command is missing a return type hint; update its signature to include -> None to explicitly indicate it returns nothing. Locate the function def info(): in the CLI/register area (the function that calls default_agent.info() and uses click.echo) and change its declaration to def info() -> None: so the return type is explicit and matches typical click command handlers.
70-80: Add return type hint.Suggested fix
`@cli.command`() -def validate(): +def validate() -> None: """Validate agent structure."""🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/__main__.py` around lines 70 - 80, The validate CLI function is missing a return type hint; update the function signature for validate (the `@cli.command`() function named validate) to include an explicit return type (e.g., def validate() -> None:) so the function's return type is clear and matches its use of sys.exit rather than returning a value; leave the body unchanged and keep references to default_agent.validate() as-is.examples/templates/invoicing_collections_agent/agent.py (9)
267-276: Add return type hints to async methods.Suggested fix
- async def start(self): + async def start(self) -> None: if self._agent_runtime is None: self._setup() if not self._agent_runtime.is_running: await self._agent_runtime.start() - async def stop(self): + async def stop(self) -> None: if self._agent_runtime and self._agent_runtime.is_running: await self._agent_runtime.stop() self._agent_runtime = None🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/agent.py` around lines 267 - 276, Annotate the asynchronous lifecycle methods with explicit return type hints: add "-> None" to the async def start(self) and async def stop(self) methods in the class (methods named start and stop) so their signatures become async def start(self) -> None and async def stop(self) -> None; this makes the intent explicit and satisfies type checkers without changing runtime behavior.
3-22: Addfrom __future__ import annotationsfor modern type syntax.The file uses union type syntax (
|) in some type positions. Adding the future import ensures consistent behavior across Python versions.Suggested fix
"""Agent graph construction for Invoicing & Collections Agent.""" +from __future__ import annotations + from pathlib import Path🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/agent.py` around lines 3 - 22, Add "from __future__ import annotations" at the top of the module so the file uses postponed evaluation of annotations and supports modern union type syntax (|) consistently; insert this import as the very first line of examples/templates/invoicing_collections_agent/agent.py above the existing imports (before the pathlib and framework imports) to ensure symbols like EdgeSpec, GraphSpec, ExecutionResult and the node functions (scan_invoices_node, classify_overdue_node, send_reminders_node, judge_reminders_node, escalate_review_node, generate_report_node) benefit from the forward-reference behavior.
208-224: Add return type hint.Suggested fix
- def _build_graph(self): + def _build_graph(self) -> GraphSpec: return GraphSpec(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/agent.py` around lines 208 - 224, The _build_graph method lacks a return type annotation; add an explicit return type of GraphSpec to the def _build_graph signature so it reads def _build_graph(self) -> GraphSpec: and ensure GraphSpec is imported or referenced (if needed) to satisfy typing; update any related type imports at top of the file if GraphSpec isn't already in scope.
293-303: Add type hints torunmethod.Suggested fix
- async def run(self, context, session_state=None): + async def run( + self, context: dict, session_state: dict | None = None + ) -> ExecutionResult: # Inject collection_config so downstream nodes can access it🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/agent.py` around lines 293 - 303, Add Python type hints to the async run method signature: annotate the method name run with types for context (e.g., Dict[str, Any] or a specific Context type), session_state (Optional[SessionState] or Optional[Dict[str, Any]]), and the return type as Awaitable[ExecutionResult] or Async return of ExecutionResult; update any imports (from typing import Optional, Dict, Any, Awaitable) and reference the ExecutionResult type used in the body so the signature becomes fully typed while keeping the implementation intact (symbols to edit: run, context, session_state, ExecutionResult).
226-265: Add return type hint.Suggested fix
- def _setup(self): + def _setup(self) -> None: self._storage_path = (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/agent.py` around lines 226 - 265, The _setup method in the Invoicing Collections agent is missing a return type hint; update the method signature for def _setup(self) to include an explicit return type (None) so it becomes def _setup(self) -> None: and ensure any type imports are present if needed; locate the method by name "_setup" in agent.py and update the signature accordingly.
278-291: Add type hints totrigger_and_waitmethod.Suggested fix
async def trigger_and_wait( self, - entry_point="default", - input_data=None, - timeout=None, - session_state=None, - ): + entry_point: str = "default", + input_data: dict | None = None, + timeout: float | None = None, + session_state: dict | None = None, + ) -> ExecutionResult: if self._agent_runtime is None: raise RuntimeError("Agent not started. Call start() first.")Note: The
timeoutparameter is defined but not used in the method body. Consider either using it or removing it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/agent.py` around lines 278 - 291, The trigger_and_wait method lacks type annotations and declares an unused timeout parameter; update the signature of trigger_and_wait to include proper type hints (e.g., entry_point: str = "default", input_data: Optional[Dict[str, Any]] = None, timeout: Optional[float] = None, session_state: Optional[Dict[str, Any]] = None) and an appropriate return type (e.g., -> Any or a concrete response type), import typing symbols as needed, and then either (A) pass timeout through to self._agent_runtime.trigger_and_wait if that backend accepts a timeout parameter, or (B) remove the timeout parameter from the signature if it is not supported by _agent_runtime.trigger_and_wait; ensure references to _agent_runtime and trigger_and_wait remain intact.
193-206: Add type hints to class methods.Per coding guidelines, function signatures should include type hints. The
__init__method and instance variables would benefit from type annotations.Suggested fix
+from framework.config import RuntimeConfig +from framework.graph import GraphSpec as GraphSpecType +from framework.runtime.agent_runtime import AgentRuntime + class InvoicingCollectionsAgent: - def __init__(self, config=None): + def __init__(self, config: RuntimeConfig | None = None) -> None: self.config = config or default_config self.goal = goal self.nodes = nodes self.edges = edges self.entry_node = entry_node self.entry_points = entry_points self.pause_nodes = pause_nodes self.terminal_nodes = terminal_nodes - self._graph = None - self._agent_runtime = None - self._tool_registry = None - self._storage_path = None + self._graph: GraphSpecType | None = None + self._agent_runtime: AgentRuntime | None = None + self._tool_registry: ToolRegistry | None = None + self._storage_path: Path | None = None🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/agent.py` around lines 193 - 206, Add explicit type hints to the InvoicingCollectionsAgent __init__ signature and its instance attributes: annotate the parameter config (e.g., Optional[Dict[str, Any]] or the specific config type), and add types for goal (str), nodes (List[Node] or List[Dict[str, Any]]), edges (List[Edge]), entry_node (str), entry_points (List[str]), pause_nodes (Set[str] or List[str]), terminal_nodes (Set[str] or List[str]), and internal attributes _graph, _agent_runtime, _tool_registry, _storage_path (use Optional[...] with the appropriate classes or Any). Import necessary typing symbols (Optional, List, Dict, Any, Set) and any domain types (Node, Edge) and update the __init__ signature and assignments accordingly so instance variables are type-annotated.
322-338: Add return type hint and note unused variable.The
warningslist is initialized but never populated.Suggested fix
- def validate(self): - errors, warnings = [], [] + def validate(self) -> dict: + errors: list[str] = [] + warnings: list[str] = [] # Reserved for future use node_ids = {n.id for n in self.nodes}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/agent.py` around lines 322 - 338, The validate method lacks a return type hint and initializes a warnings list that is never used; update the signature of validate to include an explicit return type (e.g. def validate(self) -> dict[str, object]:) and remove the unused local variable warnings (or replace its initialization with directly returning an empty list in the return dict), ensuring you still return the "warnings" key (e.g. "warnings": []) alongside "errors" and "valid"; reference symbols: validate, warnings, errors.
305-320: Add return type hint.Suggested fix
- def info(self): + def info(self) -> dict: return {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/templates/invoicing_collections_agent/agent.py` around lines 305 - 320, The info method lacks a return type hint; update the signature of def info(self) to include an explicit type (e.g. def info(self) -> Dict[str, Any]: or -> Mapping[str, Any]:) and import the corresponding typing symbols (Dict and Any or Mapping and Any) at the top of the file, ensuring the method still returns the same dict built from metadata, self.goal, self.nodes, self.edges, entry_node, entry_points, terminal_nodes, and client_facing_nodes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/templates/invoicing_collections_agent/agent.py`:
- Around line 165-171: The EdgeSpec with id "report-to-scan" creates a loop from
node "generate-report" back to "scan-invoices", causing the graph to never
terminate when terminal_nodes is empty; to fix this for single-pass execution,
add "generate-report" to the agent's terminal_nodes list (or alternatively
update README/docs to state the loop is intentional for continuous/daemon
processing with dynamic invoice sources) so the workflow ends after report
generation.
In `@examples/templates/invoicing_collections_agent/nodes/__init__.py`:
- Around line 277-299: The system_prompt string in escalate_review_node contains
unreplaced template placeholders (e.g., {escalation_accounts},
{classified_invoices}, {judgment_details}, {collection_config.cfo_email}) but
the code never calls .format() and input_keys only lists
["escalation_accounts","classified_invoices","judgment_details"], so those
values (and collection_config) will be sent literally to the LLM; fix by either
removing placeholders from system_prompt and moving the data into
_build_initial_message() / the initial user message, or implement interpolation
before constructing the NodeConversation (e.g., call
system_prompt.format(**ctx.input_data) or otherwise resolve
collection_config.cfo_email into a flat key and add it to input_keys) so that
system_prompt contains real values when passed to NodeConversation.
---
Nitpick comments:
In `@examples/templates/invoicing_collections_agent/__main__.py`:
- Around line 13-20: Add type hints to the setup_logging function signature:
annotate the parameters verbose and debug as bool and the return type as None
(i.e., def setup_logging(verbose: bool = False, debug: bool = False) -> None).
No other changes to behavior; keep existing default values and body
(logging.basicConfig call). Update any necessary imports if your linter requires
typing imports (none required for bool/None).
- Around line 38-56: The run function lacks type hints; update the signature of
run to include types (e.g., invoice_file: Optional[str], verbose: bool) and a
return type (-> None), import Optional from typing, and ensure any local usages
that depend on the type (like passing invoice_file or "") remain valid;
specifically modify the def run(invoice_file, verbose): declaration to include
the type annotations and add the necessary typing import at the top of the
module.
- Around line 59-67: The info() CLI command is missing a return type hint;
update its signature to include -> None to explicitly indicate it returns
nothing. Locate the function def info(): in the CLI/register area (the function
that calls default_agent.info() and uses click.echo) and change its declaration
to def info() -> None: so the return type is explicit and matches typical click
command handlers.
- Around line 70-80: The validate CLI function is missing a return type hint;
update the function signature for validate (the `@cli.command`() function named
validate) to include an explicit return type (e.g., def validate() -> None:) so
the function's return type is clear and matches its use of sys.exit rather than
returning a value; leave the body unchanged and keep references to
default_agent.validate() as-is.
In `@examples/templates/invoicing_collections_agent/agent.py`:
- Around line 267-276: Annotate the asynchronous lifecycle methods with explicit
return type hints: add "-> None" to the async def start(self) and async def
stop(self) methods in the class (methods named start and stop) so their
signatures become async def start(self) -> None and async def stop(self) ->
None; this makes the intent explicit and satisfies type checkers without
changing runtime behavior.
- Around line 3-22: Add "from __future__ import annotations" at the top of the
module so the file uses postponed evaluation of annotations and supports modern
union type syntax (|) consistently; insert this import as the very first line of
examples/templates/invoicing_collections_agent/agent.py above the existing
imports (before the pathlib and framework imports) to ensure symbols like
EdgeSpec, GraphSpec, ExecutionResult and the node functions (scan_invoices_node,
classify_overdue_node, send_reminders_node, judge_reminders_node,
escalate_review_node, generate_report_node) benefit from the forward-reference
behavior.
- Around line 208-224: The _build_graph method lacks a return type annotation;
add an explicit return type of GraphSpec to the def _build_graph signature so it
reads def _build_graph(self) -> GraphSpec: and ensure GraphSpec is imported or
referenced (if needed) to satisfy typing; update any related type imports at top
of the file if GraphSpec isn't already in scope.
- Around line 293-303: Add Python type hints to the async run method signature:
annotate the method name run with types for context (e.g., Dict[str, Any] or a
specific Context type), session_state (Optional[SessionState] or
Optional[Dict[str, Any]]), and the return type as Awaitable[ExecutionResult] or
Async return of ExecutionResult; update any imports (from typing import
Optional, Dict, Any, Awaitable) and reference the ExecutionResult type used in
the body so the signature becomes fully typed while keeping the implementation
intact (symbols to edit: run, context, session_state, ExecutionResult).
- Around line 226-265: The _setup method in the Invoicing Collections agent is
missing a return type hint; update the method signature for def _setup(self) to
include an explicit return type (None) so it becomes def _setup(self) -> None:
and ensure any type imports are present if needed; locate the method by name
"_setup" in agent.py and update the signature accordingly.
- Around line 278-291: The trigger_and_wait method lacks type annotations and
declares an unused timeout parameter; update the signature of trigger_and_wait
to include proper type hints (e.g., entry_point: str = "default", input_data:
Optional[Dict[str, Any]] = None, timeout: Optional[float] = None, session_state:
Optional[Dict[str, Any]] = None) and an appropriate return type (e.g., -> Any or
a concrete response type), import typing symbols as needed, and then either (A)
pass timeout through to self._agent_runtime.trigger_and_wait if that backend
accepts a timeout parameter, or (B) remove the timeout parameter from the
signature if it is not supported by _agent_runtime.trigger_and_wait; ensure
references to _agent_runtime and trigger_and_wait remain intact.
- Around line 193-206: Add explicit type hints to the InvoicingCollectionsAgent
__init__ signature and its instance attributes: annotate the parameter config
(e.g., Optional[Dict[str, Any]] or the specific config type), and add types for
goal (str), nodes (List[Node] or List[Dict[str, Any]]), edges (List[Edge]),
entry_node (str), entry_points (List[str]), pause_nodes (Set[str] or List[str]),
terminal_nodes (Set[str] or List[str]), and internal attributes _graph,
_agent_runtime, _tool_registry, _storage_path (use Optional[...] with the
appropriate classes or Any). Import necessary typing symbols (Optional, List,
Dict, Any, Set) and any domain types (Node, Edge) and update the __init__
signature and assignments accordingly so instance variables are type-annotated.
- Around line 322-338: The validate method lacks a return type hint and
initializes a warnings list that is never used; update the signature of validate
to include an explicit return type (e.g. def validate(self) -> dict[str,
object]:) and remove the unused local variable warnings (or replace its
initialization with directly returning an empty list in the return dict),
ensuring you still return the "warnings" key (e.g. "warnings": []) alongside
"errors" and "valid"; reference symbols: validate, warnings, errors.
- Around line 305-320: The info method lacks a return type hint; update the
signature of def info(self) to include an explicit type (e.g. def info(self) ->
Dict[str, Any]: or -> Mapping[str, Any]:) and import the corresponding typing
symbols (Dict and Any or Mapping and Any) at the top of the file, ensuring the
method still returns the same dict built from metadata, self.goal, self.nodes,
self.edges, entry_node, entry_points, terminal_nodes, and client_facing_nodes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e3494263-d2b7-4d9d-ae9a-863a94b3621e
⛔ Files ignored due to path filters (1)
examples/templates/invoicing_collections_agent/data/sample_invoices.csvis excluded by!**/*.csv
📒 Files selected for processing (6)
examples/templates/invoicing_collections_agent/__init__.pyexamples/templates/invoicing_collections_agent/__main__.pyexamples/templates/invoicing_collections_agent/agent.pyexamples/templates/invoicing_collections_agent/config.pyexamples/templates/invoicing_collections_agent/mcp_servers.jsonexamples/templates/invoicing_collections_agent/nodes/__init__.py
| EdgeSpec( | ||
| id="report-to-scan", | ||
| source="generate-report", | ||
| target="scan-invoices", | ||
| condition=EdgeCondition.ON_SUCCESS, | ||
| priority=1, | ||
| ), |
There was a problem hiding this comment.
Potential infinite loop: report loops back to scan.
The report-to-scan edge creates a continuous loop that re-scans the same invoice file after report generation. With terminal_nodes=[], the graph never terminates naturally.
If the input CSV is static (e.g., the bundled sample data), this will process the same invoices repeatedly. Consider either:
- Adding
generate-reporttoterminal_nodesfor single-pass execution - Documenting that this design is intentional for batch/daemon processing with dynamic invoice sources
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/templates/invoicing_collections_agent/agent.py` around lines 165 -
171, The EdgeSpec with id "report-to-scan" creates a loop from node
"generate-report" back to "scan-invoices", causing the graph to never terminate
when terminal_nodes is empty; to fix this for single-pass execution, add
"generate-report" to the agent's terminal_nodes list (or alternatively update
README/docs to state the loop is intentional for continuous/daemon processing
with dynamic invoice sources) so the workflow ends after report generation.
| system_prompt="""\ | ||
| The following invoices require human review before further collection action. | ||
| They have been flagged for escalation due to high value (>$10,000) or being | ||
| 30+ days past due. | ||
|
|
||
| Escalation accounts: {escalation_accounts} | ||
| Classification details: {classified_invoices} | ||
| Judgment details: {judgment_details} | ||
|
|
||
| **Pending actions for approved accounts:** | ||
| - Client software access will be restricted via API | ||
| - The CFO ({collection_config.cfo_email}) will be notified | ||
| - A final notice email will be sent to the client | ||
|
|
||
| Please review each account and decide: | ||
| - "approved" — proceed with access restriction, CFO notification, and final notice | ||
| - "rejected" — do not escalate; no further action | ||
| - "deferred" — revisit later | ||
|
|
||
| Provide any notes for the collections team. | ||
| """, | ||
| tools=[], | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how system_prompt templating works in the framework
rg -n "system_prompt" --type=py -A 10 -B 2 | head -100
# Look for format or template methods that process system_prompt
ast-grep --pattern 'system_prompt.format($$$)'
ast-grep --pattern 'format($$$system_prompt$$$)'Repository: aden-hive/hive
Length of output: 9263
🏁 Script executed:
# First, find and read the specific file
find . -path "*/invoicing_collections_agent/nodes/__init__.py" -type fRepository: aden-hive/hive
Length of output: 124
🏁 Script executed:
# Look at the NodeSpec class definition to understand how system_prompt is used
rg -n "class NodeSpec" --type=py -A 20Repository: aden-hive/hive
Length of output: 1384
🏁 Script executed:
# Search for how system_prompt is formatted or processed in the framework
rg -n "\.format\(" --type=py | grep -i "prompt\|system" | head -20Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Look for the framework's graph module to understand NodeSpec
find . -path "*/framework/graph*" -type fRepository: aden-hive/hive
Length of output: 1160
🏁 Script executed:
# Read the specific file around lines 277-299
sed -n '270,305p' ./examples/templates/invoicing_collections_agent/nodes/__init__.pyRepository: aden-hive/hive
Length of output: 1290
🏁 Script executed:
# Read the prompt_composer.py to see how prompts are templated
cat ./core/framework/graph/prompt_composer.pyRepository: aden-hive/hive
Length of output: 14430
🏁 Script executed:
# Also check if there's any formatting logic in the event loop
rg -n "system_prompt" ./core/framework/graph/event_loop/ -A 5 -B 2 --type=pyRepository: aden-hive/hive
Length of output: 5056
🏁 Script executed:
# Search for where system_prompt is actually formatted with context
rg -n "\.format\(" ./core/framework/graph/event_loop/ --type=py -B 3 -A 3 | head -80Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Look for conversation.system_prompt or where it's built
rg -n "system_prompt" ./core/framework/graph/conversation.py --type=py -A 5 -B 2Repository: aden-hive/hive
Length of output: 2672
🏁 Script executed:
# Search for how context/input variables are prepared before passing to LLM
rg -n "input_keys\|context\|prepare" ./core/framework/graph/event_loop_node.py --type=py -A 3 -B 1 | head -100Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Check if there's a template formatter used
rg -n "format\|template\|interpolate" ./core/framework/graph/ --type=py | grep -i "prompt\|system" | head -30Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Find where the conversation.system_prompt is actually sent to an LLM
rg -n "system_prompt\|\.complete\|\.chat\|llm" ./core/framework/graph/event_loop/event_publishing.py -A 5 -B 2 | head -100Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Check the executor to see how prompts are built and passed
cat ./core/framework/graph/executor.py | head -200Repository: aden-hive/hive
Length of output: 7541
🏁 Script executed:
# Look for judge_pipeline which might process prompts
rg -n "system_prompt" ./core/framework/graph/event_loop/judge_pipeline.py -A 5 -B 2Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Search more broadly for any .format calls on prompts
rg -n "\.format\(" ./core/framework/graph/ --type=py -A 2 -B 2 | grep -i "prompt" -A 3 -B 3Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Search for where node context is prepared - look for input_keys processing
rg -n "input_keys" ./core/framework/graph/ --type=py -A 3 -B 1 | head -150Repository: aden-hive/hive
Length of output: 6006
🏁 Script executed:
# Look for where context dict is built from input_keys
rg -n "context\|Context" ./core/framework/graph/event_loop_node.py --type=py -A 2 -B 2 | head -100Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Search for "collection_config" in examples to understand data structure
rg -n "collection_config" ./examples/templates/invoicing_collections_agent/ --type=py -A 3 -B 3Repository: aden-hive/hive
Length of output: 8033
🏁 Script executed:
# Look for how prompt context is built from input - any format or template calls
rg -n "\.format\(|f\"|str\.format" ./core/framework/graph/event_loop_node.py --type=py -A 5 -B 2Repository: aden-hive/hive
Length of output: 16050
🏁 Script executed:
# Look more carefully at where the system_prompt from NodeSpec is used in the event loop
rg -n "ctx.node_spec.system_prompt\|node_spec.system_prompt" ./core/framework/graph/event_loop_node.py -A 5 -B 2Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Check how the LLM call is made - what parameters are passed
rg -n "await.*llm\|self.llm\|\.complete\|\.chat" ./core/framework/graph/event_loop_node.py -A 3 -B 1 | head -100Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Look at conversation initialization and how it uses system_prompt
rg -n "Conversation\(" ./core/framework/graph/event_loop_node.py -A 10 -B 2 | head -80Repository: aden-hive/hive
Length of output: 711
🏁 Script executed:
# Check if there's any string formatting happening before conversation initialization
rg -n "compose_system_prompt\|\.format\(" ./core/framework/graph/event_loop_node.py -A 5 -B 2 | head -100Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Find where system_prompt is assigned before being passed to NodeConversation
sed -n '420,510p' ./core/framework/graph/event_loop_node.pyRepository: aden-hive/hive
Length of output: 4947
🏁 Script executed:
# Check if LLM provider or conversation handles template interpolation
rg -n "\.complete\|\.chat\|system_prompt" ./core/framework/graph/event_loop_node.py -A 3 -B 1 | sed -n '1,200p'Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Look at the LLM provider to see if it does template formatting
find ./core/framework/llm -name "*.py" -type fRepository: aden-hive/hive
Length of output: 329
🏁 Script executed:
# Verify the exact input_keys for the escalation review node
sed -n '260,310p' ./examples/templates/invoicing_collections_agent/nodes/__init__.pyRepository: aden-hive/hive
Length of output: 1726
🏁 Script executed:
# Check the LLM provider to see if it does string formatting on prompts
rg -n "system_prompt\|\.format\|\.complete" ./core/framework/llm/provider.py -A 3 -B 1 | head -80Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Check if there's any post-processing of system_prompt before LLM call
rg -n "def.*complete\|def.*chat" ./core/framework/llm/provider.py -A 20 | head -100Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# See if Conversation or any middleware formats the prompt
rg -n "def complete\|async def complete" ./core/framework/ --type=py -A 10 | head -100Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Confirm collection_config is passed in input_data
sed -n '290,310p' ./examples/templates/invoicing_collections_agent/agent.pyRepository: aden-hive/hive
Length of output: 786
🏁 Script executed:
# Check the Anthropic LLM provider to see if it formats prompts
cat ./core/framework/llm/anthropic.py | head -300Repository: aden-hive/hive
Length of output: 3312
🏁 Script executed:
# Look for the complete method signature in provider base class
rg -n "class LLMProvider\|def complete\|async def complete" ./core/framework/llm/provider.py -A 30 | head -150Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Get the LLMProvider base class definition with complete method
sed -n '1,500p' ./core/framework/llm/provider.py | grep -A 40 "def complete\|class LLMProvider"Repository: aden-hive/hive
Length of output: 1835
🏁 Script executed:
# Check if LiteLLMProvider does any formatting
rg -n "def complete\|def acomplete" ./core/framework/llm/litellm.py -A 30 | head -150Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Search for any .format() calls on system prompts in the entire codebase
rg -n "system.*\.format\|\.format.*system" ./core/framework/ --type=pyRepository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Check NodeContext to see what data is passed to nodes
rg -n "class NodeContext\|input_data\|memory" ./core/framework/graph/node.py -A 5 | head -100Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Look at the full LiteLLMProvider complete method
sed -n '1,300p' ./core/framework/llm/litellm.pyRepository: aden-hive/hive
Length of output: 11580
🏁 Script executed:
# Check if system prompt is formatted anywhere before being sent to LLM
rg -n "messages\|system" ./core/framework/llm/litellm.py -A 5 -B 2 | head -150Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Search if there's any user message injection that formats the prompt
rg -n "add_user_message\|format\|template" ./core/framework/graph/event_loop_node.py -A 3 -B 1 | grep -i "prompt\|format\|template" | head -40Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Check if there's middleware that processes prompts
rg -n "system_prompt.*format\|format.*prompt" ./core/framework/graph/conversation.py -A 5 -B 2Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Verify the exact input_keys for the escalate_review_node and check if collection_config is there
rg -n "escalate_review_node\|input_keys" ./examples/templates/invoicing_collections_agent/nodes/__init__.py -A 3 | grep -A 20 "escalate_review_node"Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Check how input data is passed to the node - look at how ctx.input_data is constructed
rg -n "ctx.input_data\|NodeContext" ./core/framework/graph/executor.py -B 2 -A 5 | head -100Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Verify if the system_prompt is ever formatted with context before being used
rg -n "system_prompt.*format\|format.*system_prompt\|\.format\(" ./core/framework/graph/event_loop_node.py | head -20Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Let me verify the exact node definition one more time to be absolutely sure
sed -n '260,305p' ./examples/templates/invoicing_collections_agent/nodes/__init__.pyRepository: aden-hive/hive
Length of output: 1554
🏁 Script executed:
# Check if there's any formatting happening in the initial prompt composition
sed -n '425,440p' ./core/framework/graph/event_loop_node.pyRepository: aden-hive/hive
Length of output: 874
🏁 Script executed:
# Verify collection_config is in the input_data by checking execution flow
rg -n "def execute_node\|input_data" ./core/framework/graph/executor.py -B 2 -A 10 | head -150Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Check if there's any final formatting or injection of input data into the system prompt
rg -n "system_prompt\|inject\|add_user_message" ./core/framework/graph/event_loop_node.py -A 3 -B 1 | sed -n '1,200p'Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Verify once more if any string substitution happens on system_prompt
rg -n "\.format\(|f\"|str\(.*{" ./core/framework/graph/event_loop_node.py | grep -i promptRepository: aden-hive/hive
Length of output: 165
🏁 Script executed:
# Check if the LLM call directly gets the unformatted system_prompt
rg -n "await.*complete\|acomplete" ./core/framework/graph/event_loop_node.py -A 10 | head -80Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Check what input_data is actually passed to the node - look at the executor's node call
rg -n "execute_node\|NodeContext\(" ./core/framework/graph/executor.py -A 20 | head -200Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Verify if collection_config is even accessible to the escalate_review_node
rg -n "input_keys\|input_data\|memory" ./core/framework/graph/event_loop_node.py -A 3 | sed -n '1,120p'Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Final check: confirm that system_prompt is sent to LLM without any formatting
sed -n '495,550p' ./core/framework/graph/event_loop_node.py | grep -A 30 "NodeConversation"Repository: aden-hive/hive
Length of output: 1644
🏁 Script executed:
# Check if Conversation.system_prompt is ever formatted
rg -n "def __init__\|system_prompt" ./core/framework/graph/conversation.py -A 5 | sed -n '1,100p'Repository: aden-hive/hive
Length of output: 40
🏁 Script executed:
# Confirm the ultimate fate of system_prompt when sent to LLM
sed -n '1700,1850p' ./core/framework/graph/event_loop_node.py | grep -B 10 -A 5 "complete\|acomplete"Repository: aden-hive/hive
Length of output: 1067
Remove template placeholders from system_prompt or implement format string substitution.
The system_prompt contains uninterpolated template placeholders: {escalation_accounts}, {classified_invoices}, {judgment_details}, and {collection_config.cfo_email}. These will be sent to the LLM literally with curly braces, not replaced with actual values.
The escalate_review_node declares only ["escalation_accounts", "classified_invoices", "judgment_details"] as input_keys, excluding collection_config. More critically, the framework never calls .format() on the system_prompt before passing it to the LLM—the template is concatenated with preambles and sent unchanged.
Two solutions:
- Remove placeholders and rely on the initial user message — move the data context into
_build_initial_message()instead. - Implement format string interpolation — call
system_prompt.format(**ctx.input_data)or similar before initializing NodeConversation, ensuring all required keys (includingcollection_config) are available in input_keys.
Option 1 is simpler and avoids the need for multi-level attribute access (collection_config.cfo_email).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/templates/invoicing_collections_agent/nodes/__init__.py` around
lines 277 - 299, The system_prompt string in escalate_review_node contains
unreplaced template placeholders (e.g., {escalation_accounts},
{classified_invoices}, {judgment_details}, {collection_config.cfo_email}) but
the code never calls .format() and input_keys only lists
["escalation_accounts","classified_invoices","judgment_details"], so those
values (and collection_config) will be sent literally to the LLM; fix by either
removing placeholders from system_prompt and moving the data into
_build_initial_message() / the initial user message, or implement interpolation
before constructing the NodeConversation (e.g., call
system_prompt.format(**ctx.input_data) or otherwise resolve
collection_config.cfo_email into a flat key and add it to input_keys) so that
system_prompt contains real values when passed to NodeConversation.
Closes #5181
Related: #6755
Description
New template agent that automates invoice follow-up and collections with
human-in-the-loop escalation. Based on use case #76 (Polite Accounts
Receivable Clerk) from the sample prompts.
6-node graph: scan invoices → classify overdue → send reminders → judge
classification → escalation review → generate report.
Type of Change
Related Issues
Demo
https://drive.google.com/file/d/1JDQp-sa0Niw3GmkN5YbRU5GD1s4avPBa/view?usp=sharing
Full end-to-end run via
hive openshowing all 6 nodes:Changes Made
examples/templates/invoicing_collections_agent/__init__.py— agent package init, exportsdefault_agentexamples/templates/invoicing_collections_agent/__main__.py— CLI withrun,info,validatecommandsexamples/templates/invoicing_collections_agent/agent.py— goal, edges, graph spec,InvoicingCollectionsAgentclassexamples/templates/invoicing_collections_agent/config.py—COLLECTION_CONFIG(aging buckets, thresholds, email templates)examples/templates/invoicing_collections_agent/nodes/__init__.py— 6 node definitions with system promptsexamples/templates/invoicing_collections_agent/data/sample_invoices.csv— sample invoice data for offline testingexamples/templates/invoicing_collections_agent/mcp_servers.json— MCP server configTesting
ruff checkandruff format --checkpassdefault_agent.validate()passesGraphSpec.validate()passes with no errorsAgentRunner.load()succeeds (same path ashive open)collection_configinjected into graph state viarun()hive open --agent examples/templates/invoicing_collections_agentloads correctlyChecklist