fix(block): Remove Python.format & Jinja templating format backward compatibility (#9229)

Python format uses `{Variable}` as the variable placeholder, while Jinja
uses `{{Variable}}` as its default.
Jinja is used as the main templating engine on the system, but the
Python format version is still maintained for backward compatibility.

However, the backward compatibility support can cause a side effect
while passing JSON string value into the block that uses it:
https://github.com/Significant-Gravitas/AutoGPT/issues/9194

### Changes 🏗️

* Use `{{Variable}}` place holder format and removed `{Variable}`
support in these blocks:
 - '363ae599-353e-4804-937e-b2ee3cef3da4', -- AgentOutputBlock
 - 'db7d8f02-2f44-4c55-ab7a-eae0941f0c30', -- FillTextTemplateBlock
 - '1f292d4a-41a4-4977-9684-7c8d560b9f91', -- AITextGeneratorBlock
- 'ed55ac19-356e-4243-a6cb-bc599e9b716f' --
AIStructuredResponseGeneratorBlock
* Add Jinja templating support on `AITextGeneratorBlock` &
`AIStructuredResponseGeneratorBlock`
* Migrated the existing database content to prevent breaking changes.

### Checklist 📋

#### For code changes:
- [ ] I have clearly listed my changes in the PR description
- [ ] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [ ] ...

<details>
  <summary>Example test plan</summary>
  
  - [ ] Create from scratch and execute an agent with at least 3 blocks
- [ ] Import an agent from file upload, and confirm it executes
correctly
  - [ ] Upload agent to marketplace
- [ ] Import an agent from marketplace and confirm it executes correctly
  - [ ] Edit an agent from monitor, and confirm it executes correctly
</details>

#### For configuration changes:
- [ ] `.env.example` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>
This commit is contained in:
Zamil Majdy 2025-01-09 23:29:16 +07:00 committed by GitHub
parent a1889e6212
commit 1670579a61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 94 additions and 15 deletions

View File

@ -241,7 +241,7 @@ class AgentOutputBlock(Block):
advanced=True, advanced=True,
) )
format: str = SchemaField( format: str = SchemaField(
description="The format string to be used to format the recorded_value.", description="The format string to be used to format the recorded_value. Use Jinja2 syntax.",
default="", default="",
advanced=True, advanced=True,
) )

View File

@ -26,8 +26,10 @@ from backend.data.model import (
) )
from backend.util import json from backend.util import json
from backend.util.settings import BehaveAs, Settings from backend.util.settings import BehaveAs, Settings
from backend.util.text import TextFormatter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
fmt = TextFormatter()
LLMProviderName = Literal[ LLMProviderName = Literal[
ProviderName.ANTHROPIC, ProviderName.ANTHROPIC,
@ -236,7 +238,9 @@ class AIStructuredResponseGeneratorBlock(Block):
description="Number of times to retry the LLM call if the response does not match the expected format.", description="Number of times to retry the LLM call if the response does not match the expected format.",
) )
prompt_values: dict[str, str] = SchemaField( prompt_values: dict[str, str] = SchemaField(
advanced=False, default={}, description="Values used to fill in the prompt." advanced=False,
default={},
description="Values used to fill in the prompt. The values can be used in the prompt by putting them in a double curly braces, e.g. {{variable_name}}.",
) )
max_tokens: int | None = SchemaField( max_tokens: int | None = SchemaField(
advanced=True, advanced=True,
@ -450,8 +454,8 @@ class AIStructuredResponseGeneratorBlock(Block):
values = input_data.prompt_values values = input_data.prompt_values
if values: if values:
input_data.prompt = input_data.prompt.format(**values) input_data.prompt = fmt.format_string(input_data.prompt, values)
input_data.sys_prompt = input_data.sys_prompt.format(**values) input_data.sys_prompt = fmt.format_string(input_data.sys_prompt, values)
if input_data.sys_prompt: if input_data.sys_prompt:
prompt.append({"role": "system", "content": input_data.sys_prompt}) prompt.append({"role": "system", "content": input_data.sys_prompt})
@ -578,7 +582,9 @@ class AITextGeneratorBlock(Block):
description="Number of times to retry the LLM call if the response does not match the expected format.", description="Number of times to retry the LLM call if the response does not match the expected format.",
) )
prompt_values: dict[str, str] = SchemaField( prompt_values: dict[str, str] = SchemaField(
advanced=False, default={}, description="Values used to fill in the prompt." advanced=False,
default={},
description="Values used to fill in the prompt. The values can be used in the prompt by putting them in a double curly braces, e.g. {{variable_name}}.",
) )
ollama_host: str = SchemaField( ollama_host: str = SchemaField(
advanced=True, advanced=True,

View File

@ -141,10 +141,10 @@ class ExtractTextInformationBlock(Block):
class FillTextTemplateBlock(Block): class FillTextTemplateBlock(Block):
class Input(BlockSchema): class Input(BlockSchema):
values: dict[str, Any] = SchemaField( values: dict[str, Any] = SchemaField(
description="Values (dict) to be used in format" description="Values (dict) to be used in format. These values can be used by putting them in double curly braces in the format template. e.g. {{value_name}}.",
) )
format: str = SchemaField( format: str = SchemaField(
description="Template to format the text using `values`" description="Template to format the text using `values`. Use Jinja2 syntax."
) )
class Output(BlockSchema): class Output(BlockSchema):
@ -160,7 +160,7 @@ class FillTextTemplateBlock(Block):
test_input=[ test_input=[
{ {
"values": {"name": "Alice", "hello": "Hello", "world": "World!"}, "values": {"name": "Alice", "hello": "Hello", "world": "World!"},
"format": "{hello}, {world} {{name}}", "format": "{{hello}}, {{ world }} {{name}}",
}, },
{ {
"values": {"list": ["Hello", " World!"]}, "values": {"list": ["Hello", " World!"]},

View File

@ -38,7 +38,7 @@ def create_test_graph() -> graph.Graph:
graph.Node( graph.Node(
block_id=FillTextTemplateBlock().id, block_id=FillTextTemplateBlock().id,
input_default={ input_default={
"format": "{a}, {b}{c}", "format": "{{a}}, {{b}}{{c}}",
"values_#_c": "!!!", "values_#_c": "!!!",
}, },
), ),

View File

@ -1,5 +1,3 @@
import re
from jinja2 import BaseLoader from jinja2 import BaseLoader
from jinja2.sandbox import SandboxedEnvironment from jinja2.sandbox import SandboxedEnvironment
@ -15,8 +13,5 @@ class TextFormatter:
self.env.globals.clear() self.env.globals.clear()
def format_string(self, template_str: str, values=None, **kwargs) -> str: def format_string(self, template_str: str, values=None, **kwargs) -> str:
# For python.format compatibility: replace all {...} with {{..}}.
# But avoid replacing {{...}} to {{{...}}}.
template_str = re.sub(r"(?<!{){[ a-zA-Z0-9_]+}", r"{\g<0>}", template_str)
template = self.env.from_string(template_str) template = self.env.from_string(template_str)
return template.render(values or {}, **kwargs) return template.render(values or {}, **kwargs)

View File

@ -0,0 +1,78 @@
/*
Warnings:
- You are about replace a single brace string input format for the following blocks:
- AgentOutputBlock
- FillTextTemplateBlock
- AITextGeneratorBlock
- AIStructuredResponseGeneratorBlock
with a double brace format.
*/
WITH to_update AS (
SELECT
"agentBlockId",
"constantInput"::jsonb AS j
FROM "AgentNode"
WHERE
"agentBlockId" IN (
'363ae599-353e-4804-937e-b2ee3cef3da4', -- AgentOutputBlock
'db7d8f02-2f44-4c55-ab7a-eae0941f0c30', -- FillTextTemplateBlock
'1f292d4a-41a4-4977-9684-7c8d560b9f91', -- AITextGeneratorBlock
'ed55ac19-356e-4243-a6cb-bc599e9b716f' -- AIStructuredResponseGeneratorBlock
)
AND (
"constantInput"::jsonb->>'format' ~ '(?<!\{)\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}(?!\})'
OR "constantInput"::jsonb->>'prompt' ~ '(?<!\{)\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}(?!\})'
OR "constantInput"::jsonb->>'sys_prompt' ~ '(?<!\{)\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}(?!\})'
)
),
updated_rows AS (
SELECT
"agentBlockId",
(
j
-- Update "format" if it has a single-brace placeholder
|| CASE WHEN j->>'format' ~ '(?<!\{)\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}(?!\})'
THEN jsonb_build_object(
'format',
regexp_replace(
j->>'format',
'(?<!\{)\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}(?!\})',
'{{\1}}',
'g'
)
)
ELSE '{}'::jsonb
END
-- Update "prompt" if it has a single-brace placeholder
|| CASE WHEN j->>'prompt' ~ '(?<!\{)\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}(?!\})'
THEN jsonb_build_object(
'prompt',
regexp_replace(
j->>'prompt',
'(?<!\{)\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}(?!\})',
'{{\1}}',
'g'
)
)
ELSE '{}'::jsonb
END
-- Update "sys_prompt" if it has a single-brace placeholder
|| CASE WHEN j->>'sys_prompt' ~ '(?<!\{)\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}(?!\})'
THEN jsonb_build_object(
'sys_prompt',
regexp_replace(
j->>'sys_prompt',
'(?<!\{)\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}(?!\})',
'{{\1}}',
'g'
)
)
ELSE '{}'::jsonb
END
)::text AS "newConstantInput"
FROM to_update
)
UPDATE "AgentNode" AS an
SET "constantInput" = ur."newConstantInput"
FROM updated_rows ur
WHERE an."agentBlockId" = ur."agentBlockId";

View File

@ -102,7 +102,7 @@ async def assert_sample_graph_executions(
assert exec.graph_exec_id == graph_exec_id assert exec.graph_exec_id == graph_exec_id
assert exec.output_data == {"output": ["Hello, World!!!"]} assert exec.output_data == {"output": ["Hello, World!!!"]}
assert exec.input_data == { assert exec.input_data == {
"format": "{a}, {b}{c}", "format": "{{a}}, {{b}}{{c}}",
"values": {"a": "Hello", "b": "World", "c": "!!!"}, "values": {"a": "Hello", "b": "World", "c": "!!!"},
"values_#_a": "Hello", "values_#_a": "Hello",
"values_#_b": "World", "values_#_b": "World",