2525)
2626
2727import jsonpatch # type: ignore[import-untyped]
28+ import jsonpointer # type: ignore[import-untyped]
2829import langsmith as ls
2930from dydantic import create_model_from_schema
3031from langchain_core .language_models import BaseChatModel
4041)
4142from langchain_core .prompt_values import PromptValue
4243from langchain_core .runnables import Runnable , RunnableConfig
44+ from langchain_core .runnables .config import get_executor_for_config
4345from langchain_core .tools import BaseTool , InjectedToolArg , create_schema_from_function
4446from langgraph .constants import Send
4547from langgraph .graph import StateGraph , add_messages
46- from langgraph .prebuilt .tool_validator import ValidationNode , get_executor_for_config
4748from langgraph .types import Command
4849from langgraph .utils .runnable import RunnableCallable
4950from pydantic import (
5859)
5960from typing_extensions import Annotated , TypedDict , get_args , get_origin , is_typeddict
6061
62+ from trustcall ._validation_node import ValidationNode
63+
6164logger = logging .getLogger ("extraction" )
6265
6366
@@ -812,7 +815,7 @@ def _teardown(
812815 ToolCall (
813816 id = tc ["id" ],
814817 name = tool_name ,
815- args = jsonpatch . apply_patch (target , patches ),
818+ args = _apply_patch (target , patches ),
816819 )
817820 )
818821 updated_docs [tc ["id" ]] = str (json_doc_id )
@@ -1221,11 +1224,12 @@ class PatchFunctionErrors(BaseModel):
12211224
12221225 json_doc_id : str = Field (
12231226 ...,
1224- description = "The ID of the function you are patching." ,
1227+ description = "First, identify the json_doc_id of the function you are patching." ,
12251228 )
12261229 planned_edits : str = Field (
12271230 ...,
1228- description = "Write a bullet-point list of each ValidationError you encountered"
1231+ description = "Second, write a bullet-point list of each ValidationError "
1232+ "you encountered"
12291233 " and the corresponding JSONPatch operation needed to heal it."
12301234 " For each operation, write why your initial guess was incorrect, "
12311235 " citing the corresponding types(s) from the JSONSchema"
@@ -1234,8 +1238,8 @@ class PatchFunctionErrors(BaseModel):
12341238 )
12351239 patches : list [JsonPatch ] = Field (
12361240 ...,
1237- description = "A list of JSONPatch operations to be applied to the "
1238- " previous tool call's response arguments. If none are required, return"
1241+ description = "Finally, provide a list of JSONPatch operations to be applied to"
1242+ " the previous tool call's response arguments. If none are required, return"
12391243 " an empty list. This field is REQUIRED."
12401244 " Multiple patches in the list are applied sequentially in the order provided,"
12411245 " with each patch building upon the result of the previous one." ,
@@ -1254,17 +1258,19 @@ class PatchFunctionName(BaseModel):
12541258
12551259 json_doc_id : str = Field (
12561260 ...,
1257- description = "The ID of the function you are patching." ,
1261+ description = "First, identify the json_doc_id of the function"
1262+ " you are patching." ,
12581263 )
12591264 reasoning : list [str ] = Field (
12601265 ...,
1261- description = "At least 2 logical reasons why this action ought to be taken."
1266+ description = "Seconds, provide at least 2 logical reasons why this"
1267+ " action ought to be taken."
12621268 "Cite the specific error(s) mentioned to motivate the fix." ,
12631269 )
12641270 fixed_name : Optional [str ] = Field (
12651271 ...,
1266- description = "If you need to change the name of the function (e.g., "
1267- f'from an "Unrecognized tool name" error), do so here.{ vname } ' ,
1272+ description = "Finally, if you need to change the name of the function (e.g.,"
1273+ f' from an "Unrecognized tool name" error), do so here.{ vname } ' ,
12681274 )
12691275
12701276 return PatchFunctionName
@@ -1276,11 +1282,11 @@ class PatchDoc(BaseModel):
12761282
12771283 json_doc_id : str = Field (
12781284 ...,
1279- description = "The json_doc_id of the document you are patching." ,
1285+ description = "First, identify the json_doc_id of the document you are patching." ,
12801286 )
12811287 planned_edits : str = Field (
12821288 ...,
1283- description = "Think step-by-step, reasoning over each required"
1289+ description = "Seconds, think step-by-step, reasoning over each required"
12841290 " update and the corresponding JSONPatch operation to accomplish it."
12851291 " Cite the fields in the JSONSchema you referenced in developing this plan."
12861292 " Address each path as a group; don't switch between paths.\n "
@@ -1294,8 +1300,8 @@ class PatchDoc(BaseModel):
12941300 )
12951301 patches : list [JsonPatch ] = Field (
12961302 ...,
1297- description = "A list of JSONPatch operations to be applied to the "
1298- " previous tool call's response arguments. If none are required, return"
1303+ description = "Finally, provide a list of JSONPatch operations to be applied to"
1304+ " the previous tool call's response arguments. If none are required, return"
12991305 " an empty list. This field is REQUIRED."
13001306 " Multiple patches in the list are applied sequentially in the order provided,"
13011307 " with each patch building upon the result of the previous one."
@@ -1453,9 +1459,7 @@ def _get_message_op(
14531459 try :
14541460 patches = _ensure_patches (tool_call )
14551461 if patches :
1456- patched_args = jsonpatch .apply_patch (
1457- tc ["args" ], patches
1458- )
1462+ patched_args = _apply_patch (tc ["args" ], patches )
14591463 msg_ops .append (
14601464 {
14611465 "op" : "update_tool_call" ,
@@ -1620,7 +1624,7 @@ def _strip_injected(fn: Callable) -> Callable:
16201624 return _curry (fn , ** {k : None for k in injected })
16211625
16221626
1623- def _ensure_patches (args : dict ) -> list [JsonPatch ]:
1627+ def _ensure_patches (args : dict ) -> list [jsonpatch . JsonPatch ]:
16241628 patches = args .get ("patches" )
16251629 if isinstance (patches , list ):
16261630 return patches
@@ -1656,6 +1660,44 @@ def _ensure_patches(args: dict) -> list[JsonPatch]:
16561660 return []
16571661
16581662
1663+ def _fix_string_concat (
1664+ doc : dict , patch : list [jsonpatch .JsonPatch ]
1665+ ) -> Optional [list [jsonpatch .JsonPatch ]] | None :
1666+ fixed = False
1667+ result = []
1668+ for p in patch :
1669+ if p ["path" ] and p ["path" ].endswith ("/-" ):
1670+ new_path = p ["path" ][:- 2 ]
1671+ pointer = jsonpointer .JsonPointer (new_path )
1672+ existing = pointer .resolve (doc )
1673+ if existing is not None and isinstance (existing , str ):
1674+ fixed = True
1675+ result .append (
1676+ {
1677+ "path" : new_path ,
1678+ "op" : "replace" ,
1679+ "value" : existing + p ["value" ],
1680+ }
1681+ )
1682+ else :
1683+ result .append (p )
1684+ else :
1685+ result .append (p )
1686+ if not fixed :
1687+ return None
1688+ return result
1689+
1690+
1691+ def _apply_patch (doc : dict , patches : list [jsonpatch .JsonPatch ]) -> dict :
1692+ try :
1693+ return jsonpatch .apply_patch (doc , patches )
1694+ except jsonpatch .JsonPatchConflict :
1695+ fixed = _fix_string_concat (doc , patches )
1696+ if fixed is not None :
1697+ return jsonpatch .apply_patch (doc , fixed )
1698+ raise
1699+
1700+
16591701__all__ = [
16601702 "create_extractor" ,
16611703 "ensure_tools" ,
0 commit comments