sofias-sdk-lite 0.4.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/PKG-INFO +1 -1
  2. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/pyproject.toml +4 -1
  3. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/pyproject.toml.orig +8 -1
  4. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/llm_node.py +43 -8
  5. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/prompt_assembler.py +65 -1
  6. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/LICENSE +0 -0
  7. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/README.md +0 -0
  8. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/__init__.py +0 -0
  9. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/__init__.py +0 -0
  10. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/agent.py +0 -0
  11. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/agent_builder.py +0 -0
  12. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/agent_config.py +0 -0
  13. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/execution_context.py +0 -0
  14. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/middleware.py +0 -0
  15. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/response_workflow.py +0 -0
  16. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/streaming.py +0 -0
  17. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/__init__.py +0 -0
  18. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/agent_settings.py +0 -0
  19. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/defaults.py +0 -0
  20. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/runtime_config.py +0 -0
  21. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/sdk_config.py +0 -0
  22. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/source.py +0 -0
  23. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/__init__.py +0 -0
  24. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/agent_contracts.py +0 -0
  25. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/base_contracts.py +0 -0
  26. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/node_contracts.py +0 -0
  27. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/strict_contract.py +0 -0
  28. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/errors/__init__.py +0 -0
  29. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/errors/circuit_breaker.py +0 -0
  30. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/errors/error_handler.py +0 -0
  31. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/errors/exceptions.py +0 -0
  32. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/files.py +0 -0
  33. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/__init__.py +0 -0
  34. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/factory.py +0 -0
  35. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/gateway.py +0 -0
  36. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/openai_compatible.py +0 -0
  37. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/protocol.py +0 -0
  38. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/tokens.py +0 -0
  39. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/tools.py +0 -0
  40. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/mcp.py +0 -0
  41. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/messaging/__init__.py +0 -0
  42. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/messaging/models.py +0 -0
  43. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/__init__.py +0 -0
  44. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/aggregator_config.py +0 -0
  45. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/aggregator_node.py +0 -0
  46. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/base_node.py +0 -0
  47. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/delegation_config.py +0 -0
  48. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/delegation_node.py +0 -0
  49. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/delegation_transport.py +0 -0
  50. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/function_node.py +0 -0
  51. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/llm_node_config.py +0 -0
  52. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/plan_executor.py +0 -0
  53. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/planner_node.py +0 -0
  54. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/planning_models.py +0 -0
  55. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/__init__.py +0 -0
  56. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/_log.py +0 -0
  57. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/events.py +0 -0
  58. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/tool_events.py +0 -0
  59. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/trace_context.py +0 -0
  60. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/tracing.py +0 -0
  61. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/py.typed +0 -0
  62. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/__init__.py +0 -0
  63. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/client.py +0 -0
  64. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/config.py +0 -0
  65. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/consumer.py +0 -0
  66. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/delegation_transport.py +0 -0
  67. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/publisher.py +0 -0
  68. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/rpc_client.py +0 -0
  69. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/types.py +0 -0
  70. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/routing/__init__.py +0 -0
  71. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/routing/router.py +0 -0
  72. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/routing/strategies.py +0 -0
  73. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/runner/__init__.py +0 -0
  74. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/runner/config.py +0 -0
  75. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/runner/runner.py +0 -0
  76. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/state/__init__.py +0 -0
  77. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/state/conversation_state.py +0 -0
  78. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/state/history.py +0 -0
  79. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/state/memory.py +0 -0
  80. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/tools.py +0 -0
  81. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/workflows/__init__.py +0 -0
  82. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/workflows/chat.py +0 -0
  83. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/workflows/null.py +0 -0
  84. {sofias_sdk_lite-0.4.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/workflows/streaming_chat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sofias-sdk-lite
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Build agents on the Sofias agent graph and run them over RabbitMQ.
5
5
  Keywords: agents,ai,llm,rabbitmq,sofias
6
6
  Author: Sofias Tech, S.L.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sofias-sdk-lite"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "Build agents on the Sofias agent graph and run them over RabbitMQ."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -68,6 +68,9 @@ markers = ["integration: requires a running RabbitMQ broker"]
68
68
  line-length = 100
69
69
  target-version = "py311"
70
70
 
71
+ [tool.ruff.lint]
72
+ extend-select = ["TC004"]
73
+
71
74
  [tool.pyright]
72
75
  include = ["src"]
73
76
  typeCheckingMode = "strict"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sofias-sdk-lite"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "Build agents on the Sofias agent graph and run them over RabbitMQ."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -75,6 +75,13 @@ markers = ["integration: requires a running RabbitMQ broker"]
75
75
  line-length = 100
76
76
  target-version = "py311"
77
77
 
78
+ [tool.ruff.lint]
79
+ # TC004 catches a name imported only under ``if TYPE_CHECKING:`` but used at
80
+ # runtime. That shipped in 0.4.0 as a NameError the moment a node executed a
81
+ # caller-owned tool (TKT-067). Neither pyright strict nor mypy flag it: to them
82
+ # the symbol exists. Ruff is the one checker that does.
83
+ extend-select = ["TC004"]
84
+
78
85
  [tool.pyright]
79
86
  include = ["src"]
80
87
  typeCheckingMode = "strict"
@@ -600,6 +600,7 @@ class LLMNode(BaseNode):
600
600
  base_prompt: str,
601
601
  prompt: str,
602
602
  runtime_vars: dict[str, Any],
603
+ input_data: dict[str, Any],
603
604
  ) -> tuple[int, list[ToolSpec] | None, str]:
604
605
  """Re-read the tools when the catalog changed mid-turn.
605
606
 
@@ -616,6 +617,7 @@ class LLMNode(BaseNode):
616
617
  would duplicate the schemas inside the text.
617
618
  prompt: Current prompt.
618
619
  runtime_vars: Template variables for this turn.
620
+ input_data: The node's validated input for this turn.
619
621
 
620
622
  Returns:
621
623
  Updated ``(revision, specs, prompt)``, or the ones received when
@@ -635,14 +637,33 @@ class LLMNode(BaseNode):
635
637
  return (
636
638
  revision,
637
639
  new_specs,
638
- self._prompt_with_registry_tools(base_prompt, new_specs, runtime_vars),
640
+ self._prompt_with_registry_tools(base_prompt, new_specs, runtime_vars, input_data),
639
641
  )
640
642
 
643
+ def _assemble_prompt(self, runtime_vars: dict[str, Any], input_data: dict[str, Any]) -> str:
644
+ """Assemble this turn's prompt with the node's assembler.
645
+
646
+ ``input_data`` only reaches an assembler we built ourselves. A
647
+ caller-supplied one was written against ``assemble(runtime_vars=...)``
648
+ and would reject the extra keyword, so it keeps the signature it knows.
649
+
650
+ Args:
651
+ runtime_vars: Template variables for this turn (settings + input).
652
+ input_data: The node's validated input on its own.
653
+
654
+ Returns:
655
+ The assembled prompt.
656
+ """
657
+ if not self._owns_prompt_assembler:
658
+ return self._prompt_assembler.assemble(runtime_vars=runtime_vars)
659
+ return self._prompt_assembler.assemble(runtime_vars=runtime_vars, input_data=input_data)
660
+
641
661
  def _prompt_with_registry_tools(
642
662
  self,
643
663
  prompt: str,
644
664
  tool_specs: list[ToolSpec] | None,
645
665
  runtime_vars: dict[str, Any],
666
+ input_data: dict[str, Any],
646
667
  ) -> str:
647
668
  """Re-assemble the prompt including the registry's tools.
648
669
 
@@ -656,6 +677,7 @@ class LLMNode(BaseNode):
656
677
  prompt: Prompt already assembled with the node's assembler.
657
678
  tool_specs: Static plus registry tools, or None.
658
679
  runtime_vars: Template variables for this turn.
680
+ input_data: The node's validated input for this turn.
659
681
 
660
682
  Returns:
661
683
  The re-assembled prompt, or the one received when no rebuild applies.
@@ -671,7 +693,7 @@ class LLMNode(BaseNode):
671
693
  output_contract=self._config.output_contract,
672
694
  raw_text=self._raw_text_field is not None,
673
695
  )
674
- return assembler.assemble(runtime_vars=runtime_vars)
696
+ return assembler.assemble(runtime_vars=runtime_vars, input_data=input_data)
675
697
 
676
698
  async def _execute_registry_tool(self, tool_call: ToolCall, tool_start: float) -> ToolResult:
677
699
  """Execute a tool call via the ToolRegistry.
@@ -779,6 +801,12 @@ class LLMNode(BaseNode):
779
801
  if modified_input is not None:
780
802
  tool_input = modified_input
781
803
 
804
+ # ToolContext is a TYPE_CHECKING-only name in this module, and this is
805
+ # the one place that BUILDS one instead of just annotating with it, so
806
+ # it needs the real import at runtime. Same local-import idiom already
807
+ # used for DelegationNode, FanOutStrategy and StreamingResponseWorkflow.
808
+ from sofias_sdk_lite.tools import ToolContext
809
+
782
810
  # Execute the tool using BaseTool.execute() in a worker thread (async, non-blocking)
783
811
  try:
784
812
  context = ToolContext(
@@ -999,7 +1027,7 @@ class LLMNode(BaseNode):
999
1027
 
1000
1028
  runtime_vars = self._get_runtime_vars()
1001
1029
  runtime_vars.update(input_data)
1002
- prompt = self._prompt_assembler.assemble(runtime_vars=runtime_vars)
1030
+ prompt = self._assemble_prompt(runtime_vars, input_data)
1003
1031
 
1004
1032
  max_iterations = self._get_max_iterations()
1005
1033
  parallel_tool_calls = self._get_parallel_tool_calls()
@@ -1007,7 +1035,7 @@ class LLMNode(BaseNode):
1007
1035
  tool_catalog_revision = self._tool_catalog_revision()
1008
1036
 
1009
1037
  base_prompt = prompt
1010
- prompt = self._prompt_with_registry_tools(prompt, tool_specs, runtime_vars)
1038
+ prompt = self._prompt_with_registry_tools(prompt, tool_specs, runtime_vars, input_data)
1011
1039
 
1012
1040
  history: list[tuple[LLMResponse, list[ToolResult]]] = []
1013
1041
  iteration = 0
@@ -1019,7 +1047,7 @@ class LLMNode(BaseNode):
1019
1047
 
1020
1048
  while iteration < max_iterations:
1021
1049
  tool_catalog_revision, tool_specs, prompt = await self._refresh_tool_catalog(
1022
- tool_catalog_revision, tool_specs, base_prompt, prompt, runtime_vars
1050
+ tool_catalog_revision, tool_specs, base_prompt, prompt, runtime_vars, input_data
1023
1051
  )
1024
1052
 
1025
1053
  if token_budget and history:
@@ -1493,7 +1521,7 @@ class LLMNode(BaseNode):
1493
1521
  runtime_vars = self._get_runtime_vars()
1494
1522
  # Include validated input in runtime vars for prompt templates
1495
1523
  runtime_vars.update(input_data)
1496
- prompt = self._prompt_assembler.assemble(runtime_vars=runtime_vars)
1524
+ prompt = self._assemble_prompt(runtime_vars, input_data)
1497
1525
 
1498
1526
  # Tool loop
1499
1527
  max_iterations = self._get_max_iterations()
@@ -1502,7 +1530,9 @@ class LLMNode(BaseNode):
1502
1530
  tool_catalog_revision = self._tool_catalog_revision()
1503
1531
 
1504
1532
  base_prompt = prompt
1505
- prompt = self._prompt_with_registry_tools(prompt, tool_specs, runtime_vars)
1533
+ prompt = self._prompt_with_registry_tools(
1534
+ prompt, tool_specs, runtime_vars, input_data
1535
+ )
1506
1536
 
1507
1537
  history: list[tuple[LLMResponse, list[ToolResult]]] = []
1508
1538
  iteration = 0
@@ -1525,7 +1555,12 @@ class LLMNode(BaseNode):
1525
1555
 
1526
1556
  while iteration < max_iterations:
1527
1557
  tool_catalog_revision, tool_specs, prompt = await self._refresh_tool_catalog(
1528
- tool_catalog_revision, tool_specs, base_prompt, prompt, runtime_vars
1558
+ tool_catalog_revision,
1559
+ tool_specs,
1560
+ base_prompt,
1561
+ prompt,
1562
+ runtime_vars,
1563
+ input_data,
1529
1564
  )
1530
1565
 
1531
1566
  if token_budget and history:
@@ -13,6 +13,7 @@ from pathlib import Path
13
13
  from typing import TYPE_CHECKING, Any
14
14
 
15
15
  from sofias_sdk_lite.llm import ToolSpec
16
+ from sofias_sdk_lite.observability._log import get_logger
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from sofias_sdk_lite.contracts import StrictContract
@@ -21,6 +22,8 @@ if TYPE_CHECKING:
21
22
  # Re-export so existing importers keep working
22
23
  __all__ = ["PromptAssembler", "ToolSpec"]
23
24
 
25
+ logger = get_logger("nodes.prompt_assembler")
26
+
24
27
 
25
28
  class PromptAssembler:
26
29
  """Assembles the final prompt for LLM calls.
@@ -230,7 +233,44 @@ class PromptAssembler:
230
233
  """
231
234
  return ""
232
235
 
233
- def assemble(self, runtime_vars: dict[str, Any] | None = None) -> str:
236
+ @staticmethod
237
+ def _keys_used_by_template(template: str, input_data: dict[str, Any]) -> list[str]:
238
+ """Which input keys the template actually references as {placeholders}.
239
+
240
+ Args:
241
+ template: The fixed prompt, before variable resolution.
242
+ input_data: The node's validated input.
243
+
244
+ Returns:
245
+ The referenced keys, empty when the template ignores the input.
246
+ """
247
+ return [key for key in input_data if "{" + key + "}" in template]
248
+
249
+ def _format_input_section(self, input_data: dict[str, Any]) -> str:
250
+ """Format the node's input as its own section.
251
+
252
+ The input reaches the model ONLY through {placeholders} in the fixed
253
+ prompt: there is no separate "user message" channel. A node configured
254
+ without ``NodePromptConfig`` therefore sent the model the tool catalogue
255
+ and nothing else, dropping the user's message in silence (TKT-066).
256
+
257
+ Args:
258
+ input_data: The node's validated input.
259
+
260
+ Returns:
261
+ Formatted input section, or empty string when there is no input.
262
+ """
263
+ if not input_data:
264
+ return ""
265
+
266
+ body = json.dumps(input_data, indent=2, ensure_ascii=False, default=str)
267
+ return f"## Input\n\n```json\n{body}\n```"
268
+
269
+ def assemble(
270
+ self,
271
+ runtime_vars: dict[str, Any] | None = None,
272
+ input_data: dict[str, Any] | None = None,
273
+ ) -> str:
234
274
  """Assemble the final prompt.
235
275
 
236
276
  Combines the fixed prompt with variable sections (tools, output format)
@@ -238,6 +278,11 @@ class PromptAssembler:
238
278
 
239
279
  Args:
240
280
  runtime_vars: Optional runtime variables for template resolution.
281
+ input_data: The node's validated input, when the caller can tell it
282
+ apart from ``runtime_vars`` (which also carries agent settings).
283
+ Used to render the input for a node that has no prompt template,
284
+ and to warn when a template ignores it. Optional, so a
285
+ caller-supplied assembler keeps working without it.
241
286
 
242
287
  Returns:
243
288
  The complete assembled prompt ready for LLM invocation.
@@ -264,5 +309,24 @@ class PromptAssembler:
264
309
  if output_section:
265
310
  sections.append(output_section)
266
311
 
312
+ if input_data:
313
+ if not fixed_prompt:
314
+ # No template at all: without this the input never reaches the
315
+ # model. Appended last, after the tool schemas, so the task sits
316
+ # closest to where the model answers.
317
+ input_section = self._format_input_section(input_data)
318
+ if input_section:
319
+ sections.append(input_section)
320
+ elif not self._keys_used_by_template(fixed_prompt, input_data):
321
+ # A template that names none of the input keys drops the input
322
+ # just as silently, but it may be deliberate (a node whose input
323
+ # only passes through to the output), so this only warns.
324
+ logger.warning(
325
+ "Prompt template references none of the node's input: the "
326
+ "model will not see it. Reference it as {placeholder}.",
327
+ node_name=self._config.name,
328
+ input_keys=sorted(input_data),
329
+ )
330
+
267
331
  # Join sections with clear separation
268
332
  return "\n\n".join(sections).strip()
File without changes