bugfixes: transaction parameter collector, generic formatting messages, guardrails, prompts. Testing contas

This commit is contained in:
2026-08-20 00:22:49 -03:00
parent 23b477e08f
commit bb0ef019bf
79 changed files with 232 additions and 66 deletions

108
.env
View File

@@ -28,7 +28,7 @@ LLM_TIMEOUT_SECONDS=120
# OCI OpenAI-compatible endpoint
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
OCI_GENAI_MODEL=openai.gpt-4.1
OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
OCI_GENAI_API_KEY=<API_KEY>
OCI_GENAI_PROJECT_OCID=
#OCI_GENAI_BASE_URL=https://pegruagntaiatenddev.pe.inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com
@@ -42,7 +42,7 @@ OCI_AUTH_MODE=config_file
# OCI SDK / signer / profiles
OCI_CONFIG_FILE=~/.oci/config
OCI_PROFILE=LATINOAMERICA-Chicago
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..<COMPARTMENT_OCID>
OCI_REGION=us-chicago-1
###############################################################################
@@ -55,18 +55,18 @@ CHECKPOINT_REPOSITORY_PROVIDER=autonomous
# Autonomous Database
ADB_USER=admin
ADB_PASSWORD=Moniquinha19721972
ADB_DSN=oradb23ai_high
ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai
ADB_WALLET_PASSWORD=Moniquinha1972
ADB_PASSWORD=<PASSWORD>
ADB_DSN=<TNS_ALIAS_OR_CONNECTION_DSN>
ADB_WALLET_LOCATION=/path/to/<REDACTED>
ADB_WALLET_PASSWORD=<PASSWORD>
ADB_TABLE_PREFIX=AGENTFW
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
MONGODB_URI=mongodb://ADMIN:Moniquinha1972@srlrz1eg.adb.sa-saopaulo-1.oraclecloudapps.com:37017/ADMIN?authMechanism=PLAIN&authSource=$external&ssl=true&retryWrites=false&loadBalanced=true
MONGODB_URI=mongodb://<USER>:<PASSWORD>@<HOST>:<PORT>/<DB>?<PARAMS>
MONGODB_DATABASE=agent_platform
# Redis
REDIS_URL=redis://default:devredis@localhost:6379/0
REDIS_URL=redis://<USER>:<PASSWORD>@<HOST>:<PORT>/<DB>
ENABLE_REDIS_CACHE=false
###############################################################################
@@ -90,9 +90,9 @@ LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES=AGA.,NOC., IC.
LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion
LANGFUSE_IGNORE_HEALTHCHECKS=true
LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics
LANGFUSE_PUBLIC_KEY=pk-lf-4a1e3921-5158-4fd3-a16d-7a77549fb312
LANGFUSE_SECRET_KEY=sk-lf-efc6fd59-c5ec-4858-b6ec-4aa129734915
LANGFUSE_HOST=http://localhost:3005
LANGFUSE_PUBLIC_KEY=<PUBLIC_KEY>
LANGFUSE_SECRET_KEY=<SECRET_KEY>
LANGFUSE_HOST=http://<HOST>/<PATH>
ENABLE_OTEL=false
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_SERVICE_NAME=ai-agent-template
@@ -113,7 +113,7 @@ GCP_PROJECT_ID=
GCP_PUBSUB_TOPIC=
GCP_PUBSUB_TIMEOUT_SECONDS=30
# Credencial GCP segue padrão Google:
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
# GOOGLE_APPLICATION_CREDENTIALS=/path/to/<CREDENTIAL_FILE>
###############################################################################
# OCI Streaming
@@ -221,11 +221,11 @@ ENABLE_CONTROL_EVENTS_OTEL_LOGS=true
ENABLE_NOC_OTEL_LOGS=false
ENV_TESTE=123
GUARDRAIL_LLM="20b"
LANGFUSE_HOST=http://localhost:3005
LANGFUSE_HOST=http://<HOST>/<PATH>
MONGODB_COLLECTION=sales_memory
MONGODB_CONNECTION_STRING=mongodb://ADMIN:Moniquinha1972@srlrz1eg.adb.sa-saopaulo-1.oraclecloudapps.com:37017/ADMIN?authMechanism=PLAIN&authSource=$external&ssl=true&retryWrites=false&loadBalanced=true
MONGODB_CONNECTION_STRING=mongodb://<USER>:<PASSWORD>@<HOST>:<PORT>/<DB>?<PARAMS>
OTEL_EXPORTER_OTLP_HOST_HEADER=tim-ai-atend-agnt-opentelemetry
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://10.153.35.23/v1/logs
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://<HOST>/<PATH>
PUBSUB_EXCLUDE_NOC=true
PUBSUB_PAYLOAD_MODE=flat
PUBSUB_SEQUENCE_ENABLED=true
@@ -233,9 +233,9 @@ PUBSUB_SEQUENCE_PROVIDER=mongodb
PUBSUB_SEQUENCE_MEMORY_FALLBACK=false
PUBSUB_SEQUENCE_MONGODB_COLLECTION=observer_sequences
PUBSUB_SEQUENCE_MONGODB_DATABASE=USR_ADB_AGNTATEND_W_DEV
PUBSUB_SEQUENCE_MONGODB_URI=mongodb://ADMIN:Moniquinha1972@srlrz1eg.adb.sa-saopaulo-1.oraclecloudapps.com:37017/ADMIN?authMechanism=PLAIN&authSource=$external&ssl=true&retryWrites=false&loadBalanced=true
PUBSUB_SEQUENCE_MONGODB_URI=mongodb://<USER>:<PASSWORD>@<HOST>:<PORT>/<DB>?<PARAMS>
PUBSUB_SEQUENCE_PROVIDER=mongodb
PUBSUB_SEQUENCE_REDIS_URL=redis://default:devredis@127.0.0.1:6379/0
PUBSUB_SEQUENCE_REDIS_URL=redis://<USER>:<PASSWORD>@<HOST>:<PORT>/<DB>
TIM_AGENT_FRAMEWORK_EVENT_PAYLOAD_LOG_ENABLED=true
TIM_AGENT_FRAMEWORK_LOG_EXPORT_ENABLED=true
TIM_AGENT_ID=contas
@@ -252,88 +252,88 @@ TIM_BLOQUEIO_CLIENT_ID=AIAGENTCR
TIM_BLOQUEIO_OPERATION_TYPE=block
TIM_BLOQUEIO_TIMEOUT=30
TIM_CANCELAMENTO_TIMEOUT=30
TIM_CANCELAMENTO_URL=http://pmidfqa.internal.timbrasil.com.br:8000/access/v1/singleVas
TIM_CANCELAMENTO_URL=http://<HOST>/<PATH>
TIM_CHANNEL_ADAPTER_DEFAULT_CHANNEL=voice
TIM_CHANNEL_ADAPTER_ENABLED=false
TIM_CHANNEL_ADAPTER_SHADOW=false
TIM_COMPLETE_INVOICES_TIMEOUT=30
TIM_COMPLETE_INVOICES_URL=http://pmidfqa.internal.timbrasil.com.br:8000/customers/v1/completeInvoices
TIM_COMPLETE_INVOICES_URL=http://<HOST>/<PATH>
TIM_CONSULTA_TIMEOUT=30
TIM_CONTRACT_VERSION=1.0.0
TIM_CONTRATO_TIMEOUT=30
TIM_CONTRATO_URL=http://pmidfqa.internal.timbrasil.com.br:8000/access/v1/contractInformation
TIM_CONTRATO_URL=http://<HOST>/<PATH>
TIM_CUSTOMER_CONTESTATION_CLIENT_ID=AIAGENTCR
TIM_CUSTOMER_CONTESTATION_TIMEOUT=30
TIM_CUSTOMER_CONTESTATION_URL=http://pmidfqa.internal.timbrasil.com.br:8000/interactions/v1/customerContestation
TIM_CUSTOMER_CONTESTATION_URL=http://<HOST>/<PATH>
TIM_CUSTOMER_CONTESTATION_USER_ID=AIAGENTCR
TIM_DEFAULT_CHANNEL=APP
TIM_DEFAULT_CLIENT_ID=CHAT
TIM_DEFAULT_CSP_ID=740
TIM_DIVERGENCIA_TIMEOUT=120
TIM_DIVERGENCIA_URL=http://pmidfqa.internal.timbrasil.com.br:8000/customers/v1/billingAnalysis
TIM_DIVERGENCIA_URL=http://<HOST>/<PATH>
TIM_DIVERGENCIA_USER=agentecontestacao
TIM_GATEWAY_DEFAULT_TIMEOUT=30
TIM_GATEWAY_MODE=mock
TIM_GATEWAY_RETRY_BACKOFF_FACTOR=0.5
TIM_GATEWAY_RETRY_MAX_RETRIES=3
TIM_INVOICE_RECOVER_CLIENT_ID=AIAGENTCR
TIM_SECURE_PDF_AUTH=Basic test
TIM_SECURE_PDF_AUTH=Basic <REDACTED_TOKEN>
TIM_INVOICE_RECOVER_TIMEOUT=30
TIM_JUDGES_BASELINE_SCORES=
TIM_JUDGES_MIN_SCORES=CSI=0,ALUC=7,RQLT=6,VCTN=8
#TIM_LLM_EXTRA={"auth_type":"RESOURCE_PRINCIPAL","provider":"meta","oci_use_oke_workload_signer":true}
TIM_LLM_OCI_AUTH_FILE_LOCATION=/home/hoshi/.oci/config
TIM_LLM_OCI_AUTH_FILE_LOCATION=/path/to/<REDACTED>
TIM_LLM_EXTRA={"auth_type":"API_KEY","auth_profile":"LATINOAMERICA-Chicago","provider":"meta","oci_use_oke_workload_signer":false}
TIM_LLM_GATEWAY_FALLBACK_PROMPT_CONTENT=Você é um agente para operações VAS TIM. Use tools quando precisar executar ações no backend.
TIM_LLM_GATEWAY_FALLBACK_PROMPT_ID=default
TIM_LLM_GATEWAY_LANGFUSE_CACHE_TTL_SECONDS=60
TIM_LLM_GATEWAY_LANGFUSE_DEFAULT_LABEL=production
TIM_LLM_GATEWAY_LANGFUSE_FETCH_TIMEOUT_SECONDS=5
TIM_LLM_GATEWAY_LANGFUSE_HOST=http://localhost:3005
TIM_LLM_GATEWAY_LANGFUSE_HOST=http://<HOST>/<PATH>
TIM_LLM_GATEWAY_LANGFUSE_MASK_SENSITIVE_DATA=true
TIM_LLM_GATEWAY_LANGFUSE_MAX_RETRIES=3
TIM_LLM_GATEWAY_LANGFUSE_PUBLIC_KEY=pk-lf-4a1e3921-5158-4fd3-a16d-7a77549fb312
TIM_LLM_GATEWAY_LANGFUSE_SECRET_KEY=sk-lf-efc6fd59-c5ec-4858-b6ec-4aa129734915
TIM_LLM_GATEWAY_LANGFUSE_PUBLIC_KEY=<PUBLIC_KEY>
TIM_LLM_GATEWAY_LANGFUSE_SECRET_KEY=<SECRET_KEY>
TIM_LLM_GATEWAY_LANGGRAPH_TIMEOUT=5
TIM_LLM_GATEWAY_LOCAL_DIR=prompts
TIM_LLM_GATEWAY_SOURCES=txt
TIM_LLM_MODEL=openai.gpt-4.1
TIM_LLM_OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
TIM_LLM_OCI_COMPARTMENT_ID=ocid1.compartment.oc1..<COMPARTMENT_OCID>
TIM_LLM_OCI_MODEL_ID=openai.gpt-4.1
TIM_LLM_OCI_MODEL_ID_120B=ocid1.generativeaiendpoint.oc1.sa-saopaulo-1.amaaaaaaaehl73aa2amkvyvpsv3ts6rdbx6tzya43zgwx7vqljtjade2vjya
TIM_LLM_OCI_MODEL_ID_20B=ocid1.generativeaiendpoint.oc1.sa-saopaulo-1.amaaaaaaaehl73aarckk4norgg263as5zleyp36tujnynrssoohshf3nwlhq
TIM_LLM_OCI_MODEL_ID_120B=ocid1.generativeaiendpoint.oc1.<region>.<MODEL_ENDPOINT_OCID>
TIM_LLM_OCI_MODEL_ID_20B=ocid1.generativeaiendpoint.oc1.<region>.<MODEL_ENDPOINT_OCID>
TIM_LLM_OCI_MODEL_KWARGS={"temperature":0.0,"top_p":0.1,"reasoning_effort":"LOW","max_tokens":16384}
TIM_LLM_OCI_MODEL_KWARGS_120B={"temperature":0.0,"top_p":0.1,"reasoning_effort":"LOW","max_tokens":16384}
TIM_LLM_OCI_MODEL_KWARGS_20B={"temperature":0.0,"top_p":0.1,"reasoning_effort":"LOW","max_tokens":5000}
TIM_LLM_OCI_SERVICE_ENDPOINT=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
TIM_LLM_OCI_SERVICE_ENDPOINT=https://<HOST>/<PATH>
TIM_LLM_OCI_VARIANT=120b
TIM_LLM_PROVIDER=oci
TIM_MOCK_FIXTURES_DIR=/mnt/data/tim-ai-contas-agnt-agent-framework-oci-migrado/agente_contas_tim/gateway/mocks/fixtures
TIM_OCI_FINGERPRINT=a6:e6:82:ca:d0:8e:8a:7f:58:14:ce:89:b4:b7:55:18
TIM_OCI_KEY_FILE=/etc/oci/key.pem
TIM_MOCK_FIXTURES_DIR=<REDACTED>
TIM_OCI_FINGERPRINT=<OCI_API_KEY_FINGERPRINT>
TIM_OCI_KEY_FILE=/path/to/<REDACTED>
TIM_OCI_REGION=us-chicago-1
TIM_OCI_TENANCY=ocid1.tenancy.oc1..aaaaaaaayzepbi32gafno3hj23o3d6tuquqetuw3yelxb5y7t2ft2wtm2i7q
TIM_OCI_USER=ocid1.user.oc1..aaaaaaaanzctq4cfq6u7atitnpgb46rzcnewlnbljlipihlbcdnqscy7qmkq
TIM_OCI_TENANCY=ocid1.tenancy.oc1..<TENANCY_OCID>
TIM_OCI_USER=ocid1.user.oc1..<USER_OCID>
TIM_PREFETCH_INVOICE_CONTEXT_USE_CACHE=true
TIM_PROFILE_FULL_URL=http://pmidfqa.internal.timbrasil.com.br:8000/access/v1/info/{msisdn}
TIM_PROTOCOL_URL=http://pmidfqa.internal.timbrasil.com.br:8000/customers/v1/backOfficeSRopening
TIM_RAG_DB_DSN=oradb23ai_high
TIM_PROFILE_FULL_URL=http://<HOST>/<PATH>
TIM_PROTOCOL_URL=http://<HOST>/<PATH>
TIM_RAG_DB_DSN=<TNS_ALIAS_OR_CONNECTION_DSN>
TIM_RAG_DB_USER=ADMIN
TIM_RAG_DB_PASSWORD=Moniquinha19721972
TIM_RAG_DB_CONFIG_DIR=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai
TIM_RAG_DB_PASSWORD=<PASSWORD>
TIM_RAG_DB_CONFIG_DIR=/path/to/<REDACTED>
TIM_RAG_ENABLED=true
TIM_RAG_OCI_AUTH_FILE_LOCATION=/etc/oci/config
TIM_RAG_OCI_SERVICE_ENDPOINT=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
TIM_RAG_OCI_AUTH_FILE_LOCATION=/path/to/<REDACTED>
TIM_RAG_OCI_SERVICE_ENDPOINT=https://<HOST>/<PATH>
TIM_RAG_OCI_USE_OKE_WORKLOAD_IDENTITY=true
TIM_SERVICE_REQUEST_STATUS_TIMEOUT=30
TIM_SERVICE_REQUEST_STATUS_URL=http://pmidfqa.internal.timbrasil.com.br:8000/interactions/v1/statusServiceRequest
TIM_SERVICE_REQUEST_STATUS_URL=http://<HOST>/<PATH>
TIM_SMS_CLIENT_ID=AIAGENTCR
TIM_SMS_SENDER_ADDRESS=324
TIM_SMS_URL=http://pmidfqa.internal.timbrasil.com.br:8000/invoices/v1/smsBarcode
TIM_SMS_URL=http://<HOST>/<PATH>
TIM_STATE_BACKEND=oracle
#TIM_STATE_ORACLE_CONFIG_DIR=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai
#TIM_STATE_ORACLE_DSN=oradb23ai_high
#TIM_STATE_ORACLE_PASSWORD=Moniquinha19721972
#TIM_STATE_ORACLE_CONFIG_DIR=/path/to/<REDACTED>
#TIM_STATE_ORACLE_DSN=<TNS_ALIAS_OR_CONNECTION_DSN>
#TIM_STATE_ORACLE_PASSWORD=<PASSWORD>
TIM_STATE_ORACLE_POOL_ENABLED=true
TIM_STATE_ORACLE_POOL_INCREMENT=1
TIM_STATE_ORACLE_POOL_MAX=8
@@ -344,15 +344,15 @@ TIM_TENANT_ID=tim-brasil
TIM_TRACKING_ACTIVITIES_CHANNEL=AIAGENTCR
TIM_TRACKING_ACTIVITIES_CLIENT_ID=AIAGENTCR
TIM_TRACKING_ACTIVITIES_TIMEOUT=30
TIM_TRACKING_ACTIVITIES_URL=http://pmidfqa.internal.timbrasil.com.br:8000/customers/v1/trackingActivities
TIM_TRACKING_ACTIVITIES_URL=http://<HOST>/<PATH>
TIM_TRACKING_ACTIVITIES_USER_LOGIN=SIEBELPOS_INBOUND
TIM_URL_BLOQUEIO_VAS=http://pmidfqa.internal.timbrasil.com.br:8000/customers/v1/partialServiceBlocking
TIM_URL_CONSULTA_VAS=http://pmidfqa.internal.timbrasil.com.br:8000/access/v1/{msisdn}/partnerServices
TIM_URL_INVOICE_RECOVER=http://pmidfqa.internal.timbrasil.com.br:8000/customers/v1/securePDF
TIM_URL_PERFIL_FATURA=http://pmidfqa.internal.timbrasil.com.br:8000/customers/v1/completeInvoices
TIM_URL_BLOQUEIO_VAS=http://<HOST>/<PATH>
TIM_URL_CONSULTA_VAS=http://<HOST>/<PATH>
TIM_URL_INVOICE_RECOVER=http://<HOST>/<PATH>
TIM_URL_PERFIL_FATURA=http://<HOST>/<PATH>
TIM_USE_MOCK_GATEWAY=true
TIM_VAS_HISTORY_TIMEOUT=30
TIM_VAS_HISTORY_URL=http://pmidfqa.internal.timbrasil.com.br:8000/customers/v1/servicesHistory
TIM_VAS_HISTORY_URL=http://<HOST>/<PATH>
TIM_WORKFLOWS_DIR=workflows
TIM_WORKFLOW_POSTGRES_DSN=
USE_MOCK_LLM=false

View File

@@ -26,11 +26,104 @@ from .checkpoint_repository import create_checkpoint_repository
def _jsonable(value: Any) -> Any:
try:
json.dumps(value, default=str)
"""Convert a value to a JSON-safe tree without collapsing containers to strings.
The previous implementation used ``json.dumps(..., default=str)`` only as a
probe and then returned the *original* object. Because ``default=str`` makes
virtually every object serializable, the fallback branch was never reached.
JSON repositories could therefore stringify arbitrary nested LangGraph objects
later, and restored checkpoints would contain strings where mappings were
required (for example ``metadata``, ``versions_seen`` or ``checkpoint_map``).
Keep dict/list structure durable and stringify only unsupported leaf objects.
"""
if value is None or isinstance(value, (str, int, float, bool)):
return value
if isinstance(value, dict):
return {str(k): _jsonable(v) for k, v in value.items()}
if isinstance(value, (list, tuple, set, frozenset)):
return [_jsonable(v) for v in value]
model_dump = getattr(value, "model_dump", None)
if callable(model_dump):
try:
return _jsonable(model_dump(mode="python"))
except TypeError:
return json.loads(json.dumps(value, default=str))
return _jsonable(model_dump())
except Exception:
pass
# Unsupported runtime objects are not durable. At this point the object is a
# leaf; converting only the leaf to text cannot destroy an enclosing mapping.
return str(value)
def _mapping(value: Any, *, default: dict[str, Any] | None = None) -> dict[str, Any]:
"""Recover a mapping from native or legacy JSON-string values.
Legacy rows written with permissive ``default=str`` serializers may contain a
JSON object encoded as text. Parse that representation when possible; if the
value is an opaque string, return a safe empty/default mapping instead of
letting LangGraph fail with ``'str' object has no attribute 'items'``.
"""
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
decoded = json.loads(value)
except Exception:
decoded = None
if isinstance(decoded, dict):
return decoded
return dict(default or {})
def _normalize_checkpoint(checkpoint: Any) -> dict[str, Any]:
"""Normalize LangGraph checkpoint mapping fields after JSON persistence."""
cp = _mapping(checkpoint)
if not cp:
return {}
cp = dict(cp)
cp["channel_values"] = _mapping(cp.get("channel_values"))
cp["channel_versions"] = _mapping(cp.get("channel_versions"))
versions_seen = _mapping(cp.get("versions_seen"))
cp["versions_seen"] = {
str(node): _mapping(versions) for node, versions in versions_seen.items()
}
pending_sends = cp.get("pending_sends")
if pending_sends is None:
cp["pending_sends"] = []
elif not isinstance(pending_sends, list):
cp["pending_sends"] = [pending_sends]
return cp
def _durable_config(config: dict[str, Any] | None) -> dict[str, Any] | None:
"""Return the persistable subset of a LangGraph RunnableConfig.
LangGraph injects private execution objects under ``configurable`` (notably
``__pregel_runtime``). Those objects are valid only for the current graph
invocation and must never be persisted by a checkpointer. JSON-backed
repositories stringify them; on restore LangGraph then sees that string as
its runtime object and calls ``.override(...)``, producing errors such as
``AttributeError: 'str' object has no attribute 'override'`` before the first
workflow node runs.
Durable checkpoint identity lives in the normal configurable keys
(thread_id/checkpoint_ns/checkpoint_id). User-defined configurable values are
preserved; only LangGraph's private ``__pregel_*`` execution keys are removed.
"""
if config is None:
return None
cleaned = dict(_mapping(config))
configurable = dict(_mapping(cleaned.get("configurable")))
for key in list(configurable):
if str(key).startswith("__pregel_"):
configurable.pop(key, None)
cleaned["configurable"] = configurable
return cleaned
def _thread_id(config: dict[str, Any] | None) -> str:
@@ -85,6 +178,13 @@ class RepositoryCheckpointSaver(BaseCheckpointSaver):
"""Checkpoint saver nativo para LangGraph usando os repositories do framework."""
def __init__(self, settings, repository=None):
# BaseCheckpointSaver initializes LangGraph serializer state in current
# checkpoint releases. Keep compatibility with the lightweight fallback
# class used by framework-only unit tests.
try:
super().__init__()
except TypeError:
pass
self.settings = settings
self.repository = repository or create_checkpoint_repository(settings)
self._loop: asyncio.AbstractEventLoop | None = None
@@ -103,10 +203,10 @@ class RepositoryCheckpointSaver(BaseCheckpointSaver):
def _make_tuple(self, payload: dict[str, Any] | None):
if not payload:
return None
config = payload.get("config") or {"configurable": {"thread_id": payload.get("thread_id")}}
checkpoint = payload.get("checkpoint") or {}
metadata = payload.get("metadata") or {}
parent_config = payload.get("parent_config")
config = _durable_config(payload.get("config")) or {"configurable": {"thread_id": payload.get("thread_id")}}
checkpoint = _normalize_checkpoint(payload.get("checkpoint"))
metadata = _mapping(payload.get("metadata"))
parent_config = _durable_config(payload.get("parent_config"))
pending_writes = _normalize_pending_writes(payload.get("pending_writes") or [])
try:
from langgraph.checkpoint.base import CheckpointTuple
@@ -129,11 +229,13 @@ class RepositoryCheckpointSaver(BaseCheckpointSaver):
async def aput(self, config: dict[str, Any], checkpoint: dict[str, Any], metadata: dict[str, Any] | None = None, new_versions: dict[str, Any] | None = None):
thread_id = _thread_id(config)
checkpoint_id = _checkpoint_id(checkpoint)
base_config = _durable_config(config) or {}
next_config = {
**(config or {}),
**base_config,
"configurable": {
**((config or {}).get("configurable") or {}),
**_mapping(base_config.get("configurable")),
"thread_id": thread_id,
"checkpoint_ns": str(_mapping(base_config.get("configurable")).get("checkpoint_ns") or ""),
"checkpoint_id": checkpoint_id,
},
}

View File

@@ -0,0 +1,64 @@
from __future__ import annotations
import asyncio
from types import SimpleNamespace
from agent_framework.checkpoints.langgraph_saver import (
RepositoryCheckpointSaver,
_durable_config,
)
class _Repo:
def __init__(self) -> None:
self.saved = None
async def put(self, thread_id, checkpoint):
self.saved = (thread_id, checkpoint)
async def get_latest(self, thread_id):
return self.saved[1] if self.saved and self.saved[0] == thread_id else None
def test_private_pregel_runtime_is_not_durable() -> None:
config = {
"tags": ["x"],
"configurable": {
"thread_id": "t1",
"tenant": "default",
"__pregel_runtime": "stringified-runtime",
"__pregel_store": "stringified-store",
},
}
cleaned = _durable_config(config)
assert cleaned == {
"tags": ["x"],
"configurable": {"thread_id": "t1", "tenant": "default"},
}
assert "__pregel_runtime" in config["configurable"]
def test_saver_strips_private_runtime_before_put_and_restore() -> None:
repo = _Repo()
saver = RepositoryCheckpointSaver(SimpleNamespace(), repository=repo)
config = {
"configurable": {
"thread_id": "t1",
"__pregel_runtime": "Runtime(...) persisted by JSON backend",
}
}
checkpoint = {"id": "cp1", "v": 1}
returned = asyncio.run(saver.aput(config, checkpoint, {}, {}))
assert returned["configurable"] == {"thread_id": "t1", "checkpoint_id": "cp1"}
assert repo.saved is not None
persisted = repo.saved[1]
assert "__pregel_runtime" not in persisted["config"]["configurable"]
# Also protects legacy records already stored with a stringified runtime.
persisted["config"]["configurable"]["__pregel_runtime"] = "legacy-string"
restored = saver._make_tuple(persisted)
restored_config = restored.config if hasattr(restored, "config") else restored["config"]
assert "__pregel_runtime" not in restored_config["configurable"]

View File

@@ -11,9 +11,9 @@ input:
output:
- {code: OOS, enabled: true}
- {code: AOFERTA, enabled: true}
# - {code: AOFERTA, enabled: true}
- {code: REVPREC, enabled: true}
- {code: FRASEOLOGIA, enabled: true}
# - {code: FRASEOLOGIA, enabled: true}
- {code: PINJ, enabled: true}
- {code: DLEX_OUT, enabled: true}
- {code: TOXOUT, enabled: true}