fix(compiler): thread actor-level system_prompt into graph node metadata #7
@@ -41,11 +41,28 @@ host application.
|
||||
|
||||
| Actor Node Type | LangGraph NodeType | Notes |
|
||||
|---|---|---|
|
||||
| `agent` | `AGENT` | LLM invocation node; resolved at runtime via `ProviderRegistryPort`. Graph-level `provider` and `model` are propagated as defaults into each AGENT node's metadata; per-node `config.provider`/`config.model` override them. |
|
||||
| `agent` | `AGENT` | LLM invocation node; resolved at runtime via `ProviderRegistryPort`. Graph-level `provider`, `model`, and `system_prompt` act as per-node defaults (see below). |
|
||||
| `tool` | `TOOL` | Tool execution node; reference verified at load time via `ToolRegistryPort` |
|
||||
| `conditional` | `CONDITIONAL` | Routing node |
|
||||
| `subgraph` | `SUBGRAPH` | Nested actor reference |
|
||||
|
||||
#### AGENT node defaults
|
||||
|
||||
Graph-level `provider`, `model`, and `system_prompt` are propagated as
|
||||
fallback defaults into each AGENT node's `NodeConfig.metadata` via
|
||||
`setdefault`. Per-node `config.provider` / `config.model` /
|
||||
`config.system_prompt` override them. The `system_prompt` key is only
|
||||
set in metadata when the actor defines a top-level `system_prompt`
|
||||
value (including empty string); actors without the field do not carry
|
||||
the key.
|
||||
|
||||
At runtime, `Node._execute_agent()` injects
|
||||
`metadata["system_prompt"]` (when present) into the context dict
|
||||
passed to `agent.process_message()`, so host agents can read it
|
||||
without a direct reference to the raw `ActorConfigSchema`. Per-
|
||||
execution `state.metadata["system_prompt"]` overrides the compiled
|
||||
default.
|
||||
|
||||
LSP bindings are declared per-node in the `config.lsp_bindings` list:
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
Feature: Compiler threads actor-level system_prompt into AGENT node metadata
|
||||
|
||||
The actor-level ``system_prompt`` field for graph actors must be
|
||||
threaded into each AGENT node's ``NodeConfig.metadata`` by the
|
||||
compiler, exactly as ``provider`` and ``model`` already are, via
|
||||
``setdefault`` semantics so per-node overrides take precedence.
|
||||
|
||||
``Node._execute_agent()`` must also inject the node's
|
||||
``metadata["system_prompt"]`` into the context dict that reaches
|
||||
``agent.process_message()``, allowing host agents to read the prompt
|
||||
without a direct reference to the raw ``ActorConfigSchema``.
|
||||
|
||||
Scenario: Actor-level system_prompt is threaded into each AGENT node metadata
|
||||
Given a graph actor with system_prompt "You are a helpful assistant"
|
||||
When the actor configuration is compiled
|
||||
Then each AGENT node metadata contains system_prompt "You are a helpful assistant"
|
||||
|
||||
Scenario: Per-node system_prompt takes precedence over actor-level default
|
||||
Given a graph actor with actor-level system_prompt "Actor default" and planner node system_prompt "Planner override"
|
||||
When the actor configuration is compiled
|
||||
Then the planner AGENT node metadata contains system_prompt "Planner override"
|
||||
And the executor AGENT node metadata contains system_prompt "Actor default"
|
||||
|
||||
Scenario: process_message receives system_prompt from node metadata in context
|
||||
Given an AGENT NodeConfig with system_prompt "You are an expert assistant" in metadata
|
||||
And a provider registry stub that returns a capturing agent for "openai" and "gpt-4"
|
||||
When the AGENT node is executed with empty state metadata
|
||||
Then the capturing agent received system_prompt "You are an expert assistant" in context
|
||||
|
||||
Scenario: state metadata system_prompt overrides compiled node metadata at runtime
|
||||
Given an AGENT NodeConfig with system_prompt "Compiled default" in metadata
|
||||
And a provider registry stub that returns a capturing agent for "openai" and "gpt-4"
|
||||
When the AGENT node is executed with state metadata containing system_prompt "Runtime override"
|
||||
Then the capturing agent received system_prompt "Runtime override" in context
|
||||
|
||||
Scenario: state metadata system_prompt None overrides compiled default at runtime
|
||||
Given an AGENT NodeConfig with system_prompt "Compiled default" in metadata
|
||||
And a provider registry stub that returns a capturing agent for "openai" and "gpt-4"
|
||||
When the AGENT node is executed with state metadata containing system_prompt None
|
||||
Then the capturing agent received system_prompt None in context
|
||||
|
||||
Scenario: Empty-string system_prompt is threaded into AGENT node metadata
|
||||
Given a graph actor with an empty system_prompt
|
||||
When the actor configuration is compiled
|
||||
Then each AGENT node metadata contains an empty system_prompt
|
||||
|
||||
Scenario: Empty-string system_prompt is passed into runtime context
|
||||
Given an AGENT NodeConfig with an empty system_prompt in metadata
|
||||
And a provider registry stub that returns a capturing agent for "openai" and "gpt-4"
|
||||
When the AGENT node is executed with empty state metadata
|
||||
Then the capturing agent received an empty system_prompt in context
|
||||
|
||||
Scenario: Non-AGENT nodes do not receive system_prompt in compiled metadata
|
||||
Given a mixed graph actor with AGENT, TOOL, CONDITIONAL, and SUBGRAPH nodes, with system_prompt "ForAgentsOnly"
|
||||
When the actor configuration is compiled
|
||||
Then the agent node "worker" metadata contains system_prompt "ForAgentsOnly"
|
||||
And the tool node "fetcher" metadata does not contain a system_prompt key
|
||||
And the conditional node "check" metadata does not contain a system_prompt key
|
||||
And the subgraph node "nested" metadata does not contain a system_prompt key
|
||||
|
||||
Scenario: Actor without system_prompt does not add system_prompt key to AGENT node metadata
|
||||
Given a graph actor without a system_prompt
|
||||
When the actor configuration is compiled
|
||||
Then each AGENT node metadata does not contain a system_prompt key
|
||||
|
||||
Scenario: Absent system_prompt is excluded from runtime context
|
||||
Given an AGENT NodeConfig without system_prompt in metadata
|
||||
And a provider registry stub that returns a capturing agent for "openai" and "gpt-4"
|
||||
When the AGENT node is executed with empty state metadata
|
||||
Then the capturing agent context dict does not contain a system_prompt key
|
||||
@@ -0,0 +1,459 @@
|
||||
"""Behave step definitions for the compiler system_prompt threading feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveractors.agents.base import Agent
|
||||
from cleveractors.langgraph.nodes import Node, NodeConfig, NodeType
|
||||
from cleveractors.langgraph.state import GraphState
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared actor-config helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _graph_actor_with_system_prompt(system_prompt: str | None) -> dict[str, Any]:
|
||||
"""Return a minimal two-AGENT-node graph actor dict, with optional system_prompt."""
|
||||
raw: dict[str, Any] = {
|
||||
"name": "local/sys-prompt-test",
|
||||
"type": "graph",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"description": "system_prompt threading test actor",
|
||||
"route": {
|
||||
"entry_node": "planner",
|
||||
"exit_nodes": ["executor"],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "planner",
|
||||
"name": "Planner",
|
||||
"type": "agent",
|
||||
"description": "planning node",
|
||||
},
|
||||
{
|
||||
"id": "executor",
|
||||
"name": "Executor",
|
||||
"type": "agent",
|
||||
"description": "execution node",
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{"from_node": "planner", "to_node": "executor"},
|
||||
],
|
||||
},
|
||||
}
|
||||
if system_prompt is not None:
|
||||
raw["system_prompt"] = system_prompt
|
||||
return raw
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Capturing agent stub for context-inspection tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _CapturingAgent(Agent):
|
||||
"""Agent stub that records the context dict passed to process_message."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(name="capturing-agent")
|
||||
self.received_context: dict[str, Any] | None = None
|
||||
|
||||
async def process_message(
|
||||
self, message: Any, context: dict[str, Any] | None = None
|
||||
) -> Any:
|
||||
self.received_context = dict(context) if context is not None else None
|
||||
return "captured response"
|
||||
|
||||
def get_capabilities(self) -> list[str]:
|
||||
return ["capture"]
|
||||
|
||||
|
||||
class _CapturingProviderRegistry:
|
||||
"""Registry stub that always returns the same _CapturingAgent instance."""
|
||||
|
||||
def __init__(self, provider: str, model: str) -> None:
|
||||
self._provider = provider
|
||||
self._model = model
|
||||
self.agent: _CapturingAgent = _CapturingAgent()
|
||||
|
||||
def get(self, provider: str, model: str) -> Agent | None:
|
||||
if provider == self._provider and model == self._model:
|
||||
return self.agent
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Given steps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given('a graph actor with system_prompt "{system_prompt}"')
|
||||
def given_graph_actor_with_system_prompt(context: Any, system_prompt: str) -> None:
|
||||
context.actor_raw = _graph_actor_with_system_prompt(system_prompt)
|
||||
|
||||
|
||||
@given(
|
||||
'a graph actor with actor-level system_prompt "{actor_prompt}"'
|
||||
' and planner node system_prompt "{node_prompt}"'
|
||||
)
|
||||
def given_graph_actor_with_planner_override(
|
||||
context: Any, actor_prompt: str, node_prompt: str
|
||||
) -> None:
|
||||
raw = _graph_actor_with_system_prompt(actor_prompt)
|
||||
# Inject per-node system_prompt into the planner node's config block.
|
||||
for node in raw["route"]["nodes"]:
|
||||
if node["id"] == "planner":
|
||||
node["config"] = {"system_prompt": node_prompt}
|
||||
break
|
||||
context.actor_raw = raw
|
||||
|
||||
|
||||
@given('an AGENT NodeConfig with system_prompt "{system_prompt}" in metadata')
|
||||
def given_agent_nodeconfig_with_system_prompt(context: Any, system_prompt: str) -> None:
|
||||
context.node_config = NodeConfig(
|
||||
name="worker",
|
||||
type=NodeType.AGENT,
|
||||
agent=None,
|
||||
metadata={
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"system_prompt": system_prompt,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@given("a graph actor with an empty system_prompt")
|
||||
def given_graph_actor_with_empty_system_prompt(context: Any) -> None:
|
||||
context.actor_raw = _graph_actor_with_system_prompt("")
|
||||
|
||||
|
||||
@given("an AGENT NodeConfig with an empty system_prompt in metadata")
|
||||
def given_agent_nodeconfig_with_empty_system_prompt(context: Any) -> None:
|
||||
context.node_config = NodeConfig(
|
||||
name="worker",
|
||||
type=NodeType.AGENT,
|
||||
agent=None,
|
||||
metadata={
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"system_prompt": "",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@given("an AGENT NodeConfig without system_prompt in metadata")
|
||||
def given_agent_nodeconfig_without_system_prompt(context: Any) -> None:
|
||||
context.node_config = NodeConfig(
|
||||
name="worker",
|
||||
type=NodeType.AGENT,
|
||||
agent=None,
|
||||
metadata={
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@given(
|
||||
'a provider registry stub that returns a capturing agent for "{provider}" and "{model}"'
|
||||
)
|
||||
def given_capturing_provider_registry(context: Any, provider: str, model: str) -> None:
|
||||
context.capturing_registry = _CapturingProviderRegistry(provider, model)
|
||||
|
||||
|
||||
@given("a graph actor without a system_prompt")
|
||||
def given_graph_actor_without_system_prompt(context: Any) -> None:
|
||||
context.actor_raw = _graph_actor_with_system_prompt(None)
|
||||
|
||||
|
||||
@given(
|
||||
"a mixed graph actor with AGENT, TOOL, CONDITIONAL, and SUBGRAPH nodes,"
|
||||
' with system_prompt "{system_prompt}"'
|
||||
)
|
||||
def given_mixed_graph_actor_all_node_types(context: Any, system_prompt: str) -> None:
|
||||
raw: dict[str, Any] = {
|
||||
"name": "local/mixed-test",
|
||||
"type": "graph",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"description": "mixed node type test actor",
|
||||
"system_prompt": system_prompt,
|
||||
"route": {
|
||||
"entry_node": "worker",
|
||||
"exit_nodes": ["nested"],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "worker",
|
||||
"name": "Worker",
|
||||
"type": "agent",
|
||||
"description": "agent node",
|
||||
},
|
||||
{
|
||||
"id": "fetcher",
|
||||
"name": "Fetcher",
|
||||
"type": "tool",
|
||||
"description": "tool node",
|
||||
"config": {"tools": ["fetch_url"]},
|
||||
},
|
||||
{
|
||||
"id": "check",
|
||||
"name": "Checker",
|
||||
"type": "conditional",
|
||||
"description": "conditional routing node",
|
||||
"config": {
|
||||
"function": "check_result",
|
||||
"condition": {"field": "status", "equals": "ok"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "nested",
|
||||
"name": "Nested",
|
||||
"type": "subgraph",
|
||||
"description": "subgraph reference node",
|
||||
"actor_ref": "local/nested-actor",
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{"from_node": "worker", "to_node": "fetcher"},
|
||||
{"from_node": "fetcher", "to_node": "check"},
|
||||
{"from_node": "check", "to_node": "nested"},
|
||||
],
|
||||
},
|
||||
}
|
||||
context.actor_raw = raw
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# When steps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("the AGENT node is executed with empty state metadata")
|
||||
def when_agent_node_executed_empty_metadata(context: Any) -> None:
|
||||
node = Node(
|
||||
config=context.node_config,
|
||||
agents={},
|
||||
provider_registry=context.capturing_registry,
|
||||
)
|
||||
state = GraphState(
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
metadata={},
|
||||
)
|
||||
context.node_result = asyncio.run(node.execute(state))
|
||||
|
||||
|
||||
@when(
|
||||
'the AGENT node is executed with state metadata containing system_prompt "{prompt}"'
|
||||
)
|
||||
def when_agent_node_executed_with_state_metadata_system_prompt(
|
||||
context: Any, prompt: str
|
||||
) -> None:
|
||||
node = Node(
|
||||
config=context.node_config,
|
||||
agents={},
|
||||
provider_registry=context.capturing_registry,
|
||||
)
|
||||
state = GraphState(
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
metadata={"system_prompt": prompt},
|
||||
)
|
||||
context.node_result = asyncio.run(node.execute(state))
|
||||
|
||||
|
||||
@when("the AGENT node is executed with state metadata containing system_prompt None")
|
||||
def when_agent_node_executed_with_state_metadata_system_prompt_none(
|
||||
context: Any,
|
||||
) -> None:
|
||||
node = Node(
|
||||
config=context.node_config,
|
||||
agents={},
|
||||
provider_registry=context.capturing_registry,
|
||||
)
|
||||
state = GraphState(
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
metadata={"system_prompt": None},
|
||||
)
|
||||
context.node_result = asyncio.run(node.execute(state))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Then steps — compilation assertions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("each AGENT node metadata contains an empty system_prompt")
|
||||
def then_each_agent_node_has_empty_system_prompt(context: Any) -> None:
|
||||
compiled = context.compiled
|
||||
agent_nodes_found = 0
|
||||
for node_id, node_cfg in compiled.nodes.items():
|
||||
if node_cfg.type == NodeType.AGENT:
|
||||
agent_nodes_found += 1
|
||||
actual = node_cfg.metadata.get("system_prompt")
|
||||
assert actual == "", (
|
||||
f"Node '{node_id}': expected empty system_prompt, got {actual!r}"
|
||||
)
|
||||
assert agent_nodes_found > 0, "no AGENT nodes found in compiled graph"
|
||||
|
||||
|
||||
@then('each AGENT node metadata contains system_prompt "{expected}"')
|
||||
def then_each_agent_node_has_system_prompt(context: Any, expected: str) -> None:
|
||||
compiled = context.compiled
|
||||
agent_nodes_found = 0
|
||||
for node_id, node_cfg in compiled.nodes.items():
|
||||
if node_cfg.type == NodeType.AGENT:
|
||||
agent_nodes_found += 1
|
||||
actual = node_cfg.metadata.get("system_prompt")
|
||||
assert actual == expected, (
|
||||
f"Node '{node_id}': expected metadata system_prompt={expected!r}, "
|
||||
f"got {actual!r}"
|
||||
)
|
||||
assert agent_nodes_found > 0, "no AGENT nodes found in compiled graph"
|
||||
|
||||
|
||||
@then('the planner AGENT node metadata contains system_prompt "{expected}"')
|
||||
def then_planner_node_has_system_prompt(context: Any, expected: str) -> None:
|
||||
compiled = context.compiled
|
||||
node_cfg = compiled.nodes["planner"]
|
||||
actual = node_cfg.metadata.get("system_prompt")
|
||||
assert actual == expected, (
|
||||
f"planner node: expected metadata system_prompt={expected!r}, got {actual!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the executor AGENT node metadata contains system_prompt "{expected}"')
|
||||
def then_executor_node_has_system_prompt(context: Any, expected: str) -> None:
|
||||
compiled = context.compiled
|
||||
node_cfg = compiled.nodes["executor"]
|
||||
actual = node_cfg.metadata.get("system_prompt")
|
||||
assert actual == expected, (
|
||||
f"executor node: expected metadata system_prompt={expected!r}, got {actual!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("each AGENT node metadata does not contain a system_prompt key")
|
||||
def then_each_agent_node_metadata_lacks_system_prompt(context: Any) -> None:
|
||||
compiled = context.compiled
|
||||
agent_nodes_found = 0
|
||||
for node_id, node_cfg in compiled.nodes.items():
|
||||
if node_cfg.type == NodeType.AGENT:
|
||||
agent_nodes_found += 1
|
||||
assert "system_prompt" not in node_cfg.metadata, (
|
||||
f"node '{node_id}': expected no 'system_prompt' key in metadata, "
|
||||
f"but found {node_cfg.metadata.get('system_prompt')!r}"
|
||||
)
|
||||
assert agent_nodes_found > 0, "no AGENT nodes found in compiled graph"
|
||||
|
||||
|
||||
@then('the agent node "{node_id}" metadata contains system_prompt "{expected}"')
|
||||
def then_agent_node_metadata_has_system_prompt(
|
||||
context: Any, node_id: str, expected: str
|
||||
) -> None:
|
||||
compiled = context.compiled
|
||||
assert node_id in compiled.nodes, (
|
||||
f"node '{node_id}' not found in compiled nodes: {list(compiled.nodes.keys())}"
|
||||
)
|
||||
node_cfg = compiled.nodes[node_id]
|
||||
actual = node_cfg.metadata.get("system_prompt")
|
||||
assert actual == expected, (
|
||||
f"node '{node_id}': expected metadata system_prompt={expected!r}, "
|
||||
f"got {actual!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the tool node "{node_id}" metadata does not contain a system_prompt key')
|
||||
def then_tool_node_metadata_lacks_system_prompt(context: Any, node_id: str) -> None:
|
||||
compiled = context.compiled
|
||||
assert node_id in compiled.nodes, (
|
||||
f"node '{node_id}' not found in compiled nodes: {list(compiled.nodes.keys())}"
|
||||
)
|
||||
node_cfg = compiled.nodes[node_id]
|
||||
assert "system_prompt" not in node_cfg.metadata, (
|
||||
f"node '{node_id}' unexpectedly contains system_prompt in metadata: "
|
||||
f"{node_cfg.metadata.get('system_prompt')!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the conditional node "{node_id}" metadata does not contain a system_prompt key')
|
||||
def then_conditional_node_metadata_lacks_system_prompt(
|
||||
context: Any, node_id: str
|
||||
) -> None:
|
||||
compiled = context.compiled
|
||||
assert node_id in compiled.nodes, (
|
||||
f"node '{node_id}' not found in compiled nodes: {list(compiled.nodes.keys())}"
|
||||
)
|
||||
node_cfg = compiled.nodes[node_id]
|
||||
assert "system_prompt" not in node_cfg.metadata, (
|
||||
f"node '{node_id}' unexpectedly contains system_prompt in metadata: "
|
||||
f"{node_cfg.metadata.get('system_prompt')!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the subgraph node "{node_id}" metadata does not contain a system_prompt key')
|
||||
def then_subgraph_node_metadata_lacks_system_prompt(context: Any, node_id: str) -> None:
|
||||
compiled = context.compiled
|
||||
assert node_id in compiled.nodes, (
|
||||
f"node '{node_id}' not found in compiled nodes: {list(compiled.nodes.keys())}"
|
||||
)
|
||||
node_cfg = compiled.nodes[node_id]
|
||||
assert "system_prompt" not in node_cfg.metadata, (
|
||||
f"node '{node_id}' unexpectedly contains system_prompt in metadata: "
|
||||
f"{node_cfg.metadata.get('system_prompt')!r}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Then steps — runtime assertions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the capturing agent received an empty system_prompt in context")
|
||||
def then_capturing_agent_received_empty_system_prompt(context: Any) -> None:
|
||||
agent = context.capturing_registry.agent
|
||||
assert agent.received_context is not None, (
|
||||
"capturing agent process_message was never called"
|
||||
)
|
||||
actual = agent.received_context.get("system_prompt")
|
||||
assert actual == "", f"context system_prompt: expected '', got {actual!r}"
|
||||
|
||||
|
||||
@then('the capturing agent received system_prompt "{expected}" in context')
|
||||
def then_capturing_agent_received_system_prompt(context: Any, expected: str) -> None:
|
||||
agent = context.capturing_registry.agent
|
||||
assert agent.received_context is not None, (
|
||||
"capturing agent process_message was never called"
|
||||
)
|
||||
actual = agent.received_context.get("system_prompt")
|
||||
assert actual == expected, (
|
||||
f"context system_prompt: expected {expected!r}, got {actual!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the capturing agent received system_prompt None in context")
|
||||
def then_capturing_agent_received_system_prompt_none(context: Any) -> None:
|
||||
agent = context.capturing_registry.agent
|
||||
assert agent.received_context is not None, (
|
||||
"capturing agent process_message was never called"
|
||||
)
|
||||
assert "system_prompt" in agent.received_context, (
|
||||
"expected 'system_prompt' key in context, but it is missing"
|
||||
)
|
||||
actual = agent.received_context["system_prompt"]
|
||||
assert actual is None, f"context system_prompt: expected None, got {actual!r}"
|
||||
|
||||
|
||||
@then("the capturing agent context dict does not contain a system_prompt key")
|
||||
def then_capturing_agent_context_lacks_system_prompt(context: Any) -> None:
|
||||
agent = context.capturing_registry.agent
|
||||
assert agent.received_context is not None, (
|
||||
"capturing agent process_message was never called"
|
||||
)
|
||||
assert "system_prompt" not in agent.received_context, (
|
||||
f"context dict unexpectedly contains system_prompt: "
|
||||
f"{agent.received_context.get('system_prompt')!r}"
|
||||
)
|
||||
@@ -141,6 +141,7 @@ def _map_node(
|
||||
node: NodeDefinition,
|
||||
actor_provider: str | None = None,
|
||||
actor_model: str | None = None,
|
||||
actor_system_prompt: str | None = None,
|
||||
) -> lg_nodes.NodeConfig:
|
||||
"""Map an actor schema node to a LangGraph ``NodeConfig``.
|
||||
|
||||
@@ -152,17 +153,23 @@ def _map_node(
|
||||
actor_model: The actor-level model (e.g. ``"gpt-4"``).
|
||||
Used as a fallback for AGENT nodes that do not declare their
|
||||
own ``model`` in their per-node config block.
|
||||
actor_system_prompt: The actor-level system prompt.
|
||||
Used as a fallback for AGENT nodes that do not declare their
|
||||
own ``system_prompt`` in their per-node config block.
|
||||
"""
|
||||
lg_type = _NODE_TYPE_MAP.get(node.type, lg_nodes.NodeType.FUNCTION)
|
||||
config = node.config
|
||||
|
||||
# For AGENT nodes, merge the actor-level provider/model as defaults so
|
||||
# that Node._execute_agent can resolve the agent via ProviderRegistryPort
|
||||
# even when the node's own config block omits them.
|
||||
# For AGENT nodes, merge the actor-level provider/model/system_prompt as
|
||||
# defaults so that Node._execute_agent can resolve the agent via
|
||||
# ProviderRegistryPort and expose the system prompt even when the node's
|
||||
# own config block omits them.
|
||||
merged_meta: dict[str, Any] = dict(config)
|
||||
if node.type == NodeType.AGENT:
|
||||
merged_meta.setdefault("provider", actor_provider)
|
||||
merged_meta.setdefault("model", actor_model)
|
||||
if actor_system_prompt is not None:
|
||||
merged_meta.setdefault("system_prompt", actor_system_prompt)
|
||||
|
||||
return lg_nodes.NodeConfig(
|
||||
name=node.id,
|
||||
@@ -323,6 +330,7 @@ def compile_actor(
|
||||
node_def,
|
||||
actor_provider=config.provider,
|
||||
actor_model=config.model,
|
||||
actor_system_prompt=config.system_prompt,
|
||||
)
|
||||
|
||||
if node_def.type == NodeType.TOOL:
|
||||
|
||||
@@ -237,6 +237,16 @@ class Node: # pylint: disable=too-many-instance-attributes
|
||||
if history_truncated:
|
||||
context["_history_truncated"] = True
|
||||
context["_history_original_length"] = len(state.messages)
|
||||
# Expose the node's compiled system_prompt as a context default so
|
||||
# that host agents can read it via context.get("system_prompt")
|
||||
# without requiring a direct reference to the raw ActorConfigSchema.
|
||||
# Applied before state.metadata so per-execution overrides take
|
||||
# precedence over the compile-time default.
|
||||
node_system_prompt: str | None = self.config.metadata.get("system_prompt")
|
||||
# Guard against non-string values (e.g. int, list) injected by
|
||||
# manually constructed NodeConfig; only string/none pass through.
|
||||
if isinstance(node_system_prompt, str):
|
||||
context["system_prompt"] = node_system_prompt
|
||||
if state.metadata:
|
||||
context.update(state.metadata)
|
||||
nested_context = context.get("context")
|
||||
@@ -272,6 +282,7 @@ class Node: # pylint: disable=too-many-instance-attributes
|
||||
"full_context",
|
||||
"_history_truncated",
|
||||
"_history_original_length",
|
||||
"system_prompt",
|
||||
}
|
||||
snapshot_keys = set(context_snapshot.keys()) if context_snapshot else set()
|
||||
current_keys = set(context.keys())
|
||||
|
||||
Reference in New Issue
Block a user