feat(api): add context injection and Jinja2 support to Agent V2 node

Agent V2 now fully covers all LLM node capabilities:
- Context injection: {{#context#}} placeholder replaced with upstream
  knowledge retrieval results via _build_context_string()
- Jinja2 template rendering via _render_jinja2() with variable pool
- Multi-variable references across upstream nodes

Compatibility verified (7/7):
- T1: Context injection ({{#context#}})
- T2: Variable template resolution ({{#start.var#}})
- T3: Multi-upstream variable refs
- T4: Old Chat app with opening_statement
- T5: Old app sensitive_word_avoidance
- T6: Old app more_like_this
- T7: Old Completion app with variable substitution

Made-with: Cursor
This commit is contained in:
Yansong Zhang 2026-04-10 17:05:48 +08:00
parent bbed99a4cb
commit e04f00d29b

View File

@ -308,31 +308,39 @@ class AgentV2Node(Node[AgentV2NodeData]):
def _build_prompt_messages(self, dify_ctx: DifyRunContext) -> list[PromptMessage]:
"""Build prompt messages from the node's prompt_template, resolving variables.
If the node has memory config and a conversation_id exists, conversation
history is loaded and inserted between system and user messages.
Handles: variable references ({{#node.var#}}), context injection ({{#context#}}),
Jinja2 templates, and memory (conversation history).
"""
variable_pool = self.graph_runtime_state.variable_pool
messages: list[PromptMessage] = []
context_str = self._build_context_string(variable_pool)
template = self.node_data.prompt_template
if isinstance(template, Sequence) and not isinstance(template, str):
for msg_template in template:
role = msg_template.role.value if hasattr(msg_template.role, "value") else str(msg_template.role)
text = msg_template.text or ""
jinja2_text = getattr(msg_template, "jinja2_text", None)
content = jinja2_text or text
resolved = self._resolve_variable_template(content, variable_pool)
if jinja2_text:
content = self._render_jinja2(jinja2_text, variable_pool, context_str)
else:
content = self._resolve_variable_template(text, variable_pool)
if context_str:
content = content.replace("{{#context#}}", context_str)
if role == "system":
messages.append(SystemPromptMessage(content=resolved))
messages.append(SystemPromptMessage(content=content))
elif role == "user":
messages.append(UserPromptMessage(content=resolved))
messages.append(UserPromptMessage(content=content))
elif role == "assistant":
messages.append(AssistantPromptMessage(content=resolved))
messages.append(AssistantPromptMessage(content=content))
else:
text_content = getattr(template, "text", "") or ""
resolved = self._resolve_variable_template(text_content, variable_pool)
if context_str:
resolved = resolved.replace("{{#context#}}", context_str)
messages.append(UserPromptMessage(content=resolved))
if self._memory is not None:
@ -400,6 +408,60 @@ class AgentV2Node(Node[AgentV2NodeData]):
logger.warning("Failed to load memory for agent-v2 node", exc_info=True)
return []
def _build_context_string(self, variable_pool: Any) -> str:
"""Build context string from knowledge retrieval node output."""
ctx_config = self.node_data.context
if not ctx_config or not ctx_config.enabled:
return ""
selector = getattr(ctx_config, "variable_selector", None)
if not selector:
return ""
try:
value = variable_pool.get(selector)
if value is None:
return ""
raw = value.value if hasattr(value, "value") else value
if isinstance(raw, str):
return raw
if isinstance(raw, list):
parts = []
for item in raw:
if isinstance(item, str):
parts.append(item)
elif isinstance(item, dict):
if "content" in item:
parts.append(item["content"])
elif "text" in item:
parts.append(item["text"])
return "\n".join(parts)
return str(raw)
except Exception:
logger.warning("Failed to build context string", exc_info=True)
return ""
@staticmethod
def _render_jinja2(template: str, variable_pool: Any, context_str: str = "") -> str:
"""Render a Jinja2 template with variables from the pool."""
try:
from jinja2 import Environment, BaseLoader
env = Environment(loader=BaseLoader(), autoescape=False)
tpl = env.from_string(template)
parser = VariableTemplateParser(template)
selectors = parser.extract_variable_selectors()
variables: dict[str, Any] = {}
for selector in selectors:
value = variable_pool.get(selector.value_selector)
if value is not None:
variables[selector.variable] = value.text if hasattr(value, "text") else str(value)
else:
variables[selector.variable] = ""
variables["context"] = context_str
return tpl.render(**variables)
except Exception:
logger.warning("Jinja2 rendering failed, falling back to plain text", exc_info=True)
return template
@staticmethod
def _resolve_variable_template(template: str, variable_pool: Any) -> str:
"""Resolve {{#node.var#}} references in a template string using the variable pool."""