fix(langfuse): use compile() instead of .format() for prompt variable injection
Langfuse uses {{variable}} syntax in its prompt management UI, while the
hardcoded fallbacks use {variable} (Python str.format). The previous code
always called .format() which silently failed/errored when a real Langfuse
prompt was fetched.
- langfuse_client.py: add compile_prompt(template, prompt_obj, **vars)
→ uses prompt_obj.compile(**vars) when Langfuse is available
→ falls back to template.format(**vars) when using the hardcoded fallback
- agent_runner.py: replace .format() with compile_prompt() for
unified_processing (V2 local) and batch_cloud_processing (cloud agent)
- agent_setup.py: replace .format() with compile_prompt() for journey_system
deep_agent.py prompts have no variables, so no change needed there.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@ from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, Tool
|
|||||||
|
|
||||||
from app.agents.filesystem_agent import FILESYSTEM_TOOLS
|
from app.agents.filesystem_agent import FILESYSTEM_TOOLS
|
||||||
from app.config.settings import settings
|
from app.config.settings import settings
|
||||||
from app.core.langfuse_client import extract_usage, get_langfuse, get_prompt_or_fallback
|
from app.core.langfuse_client import compile_prompt, extract_usage, get_langfuse, get_prompt_or_fallback
|
||||||
from app.core.llm import get_llm
|
from app.core.llm import get_llm
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -160,7 +160,9 @@ def _build_system_prompt(
|
|||||||
template, prompt_obj = get_prompt_or_fallback(
|
template, prompt_obj = get_prompt_or_fallback(
|
||||||
"journey_system", _JOURNEY_SYSTEM_PROMPT
|
"journey_system", _JOURNEY_SYSTEM_PROMPT
|
||||||
)
|
)
|
||||||
compiled = template.format(
|
compiled = compile_prompt(
|
||||||
|
template,
|
||||||
|
prompt_obj,
|
||||||
directory=directory,
|
directory=directory,
|
||||||
data_types=", ".join(data_types),
|
data_types=", ".join(data_types),
|
||||||
template_start=_TEMPLATE_START,
|
template_start=_TEMPLATE_START,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ from app.agents.task_agent import TASK_TOOLS
|
|||||||
from app.agents.timeline_agent import TIMELINE_TOOLS
|
from app.agents.timeline_agent import TIMELINE_TOOLS
|
||||||
from app.config.settings import settings
|
from app.config.settings import settings
|
||||||
from app.core.device_manager import DeviceConnectionManager
|
from app.core.device_manager import DeviceConnectionManager
|
||||||
from app.core.langfuse_client import extract_usage, get_langfuse, get_prompt_or_fallback
|
from app.core.langfuse_client import compile_prompt, extract_usage, get_langfuse, get_prompt_or_fallback
|
||||||
from app.core.llm import get_llm
|
from app.core.llm import get_llm
|
||||||
from app.core.preprocessors import detect_content_type, preprocess
|
from app.core.preprocessors import detect_content_type, preprocess
|
||||||
from app.core.ws_context import clear_client_executor, execute_on_client, set_client_executor
|
from app.core.ws_context import clear_client_executor, execute_on_client, set_client_executor
|
||||||
@@ -661,7 +661,9 @@ async def run_local_agent(
|
|||||||
)
|
)
|
||||||
metadata_section = _format_metadata(preprocessed.metadata)
|
metadata_section = _format_metadata(preprocessed.metadata)
|
||||||
|
|
||||||
system_prompt = unified_template.format(
|
system_prompt = compile_prompt(
|
||||||
|
unified_template,
|
||||||
|
prompt_obj,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
metadata_section=metadata_section,
|
metadata_section=metadata_section,
|
||||||
projects_list=projects_block,
|
projects_list=projects_block,
|
||||||
@@ -893,7 +895,9 @@ async def run_cloud_agent(
|
|||||||
cloud_template, cloud_prompt_obj = get_prompt_or_fallback(
|
cloud_template, cloud_prompt_obj = get_prompt_or_fallback(
|
||||||
"batch_cloud_processing", _BATCH_CLOUD_PROCESSING_PROMPT
|
"batch_cloud_processing", _BATCH_CLOUD_PROCESSING_PROMPT
|
||||||
)
|
)
|
||||||
processing_prompt = cloud_template.format(
|
processing_prompt = compile_prompt(
|
||||||
|
cloud_template,
|
||||||
|
cloud_prompt_obj,
|
||||||
data_types=", ".join(config.data_types),
|
data_types=", ".join(config.data_types),
|
||||||
project_context="Determine the appropriate project from the message context.",
|
project_context="Determine the appropriate project from the message context.",
|
||||||
file_list=f"Message from {config.provider} (id: {msg.id})",
|
file_list=f"Message from {config.provider} (id: {msg.id})",
|
||||||
|
|||||||
@@ -80,10 +80,11 @@ def get_langfuse() -> Any | None:
|
|||||||
def get_prompt_or_fallback(name: str, fallback: str) -> tuple[str, Any]:
|
def get_prompt_or_fallback(name: str, fallback: str) -> tuple[str, Any]:
|
||||||
"""Fetch a text prompt from Langfuse; fall back to ``fallback`` on any error.
|
"""Fetch a text prompt from Langfuse; fall back to ``fallback`` on any error.
|
||||||
|
|
||||||
Returns ``(prompt_text, prompt_obj_or_None)``.
|
Returns ``(raw_template, prompt_obj_or_None)``.
|
||||||
|
|
||||||
* ``prompt_text`` — the raw template string (variables not yet substituted).
|
* ``raw_template`` — the uncompiled template string. Do NOT call ``.format()``
|
||||||
Callers perform variable substitution with Python's ``.format()``.
|
on it directly; use :func:`compile_prompt` instead so the correct variable
|
||||||
|
syntax is applied (``{{var}}`` for Langfuse, ``{var}`` for the fallback).
|
||||||
* ``prompt_obj`` — the Langfuse prompt object, or ``None`` when Langfuse is
|
* ``prompt_obj`` — the Langfuse prompt object, or ``None`` when Langfuse is
|
||||||
unavailable / the fetch failed. Pass this to generation observations so
|
unavailable / the fetch failed. Pass this to generation observations so
|
||||||
Langfuse links the generation to the exact prompt version in the UI.
|
Langfuse links the generation to the exact prompt version in the UI.
|
||||||
@@ -102,6 +103,38 @@ def get_prompt_or_fallback(name: str, fallback: str) -> tuple[str, Any]:
|
|||||||
return fallback, None
|
return fallback, None
|
||||||
|
|
||||||
|
|
||||||
|
def compile_prompt(template: str, prompt_obj: Any, **variables: Any) -> str:
|
||||||
|
"""Compile *template* with *variables*, choosing the right syntax.
|
||||||
|
|
||||||
|
* When *prompt_obj* is a real Langfuse prompt object, calls
|
||||||
|
``prompt_obj.compile(**variables)`` which handles ``{{variable}}``
|
||||||
|
substitution as defined in the Langfuse UI.
|
||||||
|
* When *prompt_obj* is ``None`` (Langfuse unavailable or fetch failed),
|
||||||
|
falls back to ``template.format(**variables)`` which handles the
|
||||||
|
``{variable}`` syntax used in the hardcoded fallback strings.
|
||||||
|
|
||||||
|
This keeps callers oblivious to which syntax is in use.
|
||||||
|
"""
|
||||||
|
if prompt_obj is not None:
|
||||||
|
try:
|
||||||
|
compiled = prompt_obj.compile(**variables)
|
||||||
|
# compile() returns a string for text prompts.
|
||||||
|
if isinstance(compiled, str):
|
||||||
|
return compiled
|
||||||
|
# Chat prompts return a list of dicts — join text parts.
|
||||||
|
if isinstance(compiled, list):
|
||||||
|
return "\n".join(
|
||||||
|
m.get("content", "") for m in compiled if isinstance(m, dict)
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
"langfuse: compile failed for prompt %r: %s — falling back to .format()",
|
||||||
|
getattr(prompt_obj, "name", "?"),
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
return template.format(**variables)
|
||||||
|
|
||||||
|
|
||||||
def extract_usage(response: Any) -> dict[str, int]:
|
def extract_usage(response: Any) -> dict[str, int]:
|
||||||
"""Extract token usage from a LangChain AI message into Langfuse format."""
|
"""Extract token usage from a LangChain AI message into Langfuse format."""
|
||||||
meta = getattr(response, "usage_metadata", None)
|
meta = getattr(response, "usage_metadata", None)
|
||||||
|
|||||||
Reference in New Issue
Block a user