Source code for kavalai.agents.run_context
"""
Copyright 2026 OÜ KAVAL AI (registry code 17393877)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from loguru import logger
import re
from typing import Optional, Any, Dict
from uuid import UUID
from pydantic import BaseModel, ConfigDict
from kavalai.agents.resolvers import resolve_path
from kavalai.agents.workflow_model import ArgumentInfo
[docs]
class RunContext(BaseModel):
"""Runtime data for a single interaction."""
model_config = ConfigDict(arbitrary_types_allowed=True)
agent_id: Optional[UUID] = None
session_id: Optional[UUID] = None
run_id: Optional[UUID] = None
data: dict = {}
templates: Dict[str, str] = {}
agent_service: Optional[Any] = None
[docs]
def resolve_context_value(self, path: str):
"""Resolve a dotted path like 'input.user_message' from context data."""
return resolve_path(self.data, path)
[docs]
async def resolve_history_value(self, path: str):
"""Resolve a value from session history."""
if not self.agent_service or not self.session_id:
logger.error(
f"Cannot load from history for {path}: agent_service or session_id not set"
)
return None
return await self.agent_service.get_history_value(self.session_id, str(path))
[docs]
async def resolve_template_value(self, name: str):
"""Resolve a template value by name."""
return self.templates.get(name)
[docs]
async def render_prompt(self, prompt: str) -> str:
"""
Render a prompt string by replacing {{ templates.NAME }}, {{ context.PATH }},
and {{ history.PATH }} with their resolved values.
"""
pattern = re.compile(r"\{\{\s*(templates|context|history)\.(.+?)\s*\}\}")
async def replace_match(match):
prefix = match.group(1)
path = match.group(2).strip()
if prefix == "templates":
val = await self.resolve_template_value(path)
elif prefix == "context":
val = self.resolve_context_value(path)
elif prefix == "history":
val = await self.resolve_history_value(path)
else:
val = None
if val is None:
raise ValueError(f"Could not resolve {prefix}.{path}")
if isinstance(val, (dict, list, BaseModel)):
from kavalai.agents.utils import to_plain
import json
try:
plain = to_plain(val)
return json.dumps(plain, ensure_ascii=False)
except Exception as e:
logger.warning(
f"Error serializing template value {path}: {e}", exc_info=True
)
return str(val)
return str(val)
# Since re.sub doesn't support async, we do it manually
last_pos = 0
pieces = []
for match in pattern.finditer(prompt):
pieces.append(prompt[last_pos : match.start()])
pieces.append(await replace_match(match))
last_pos = match.end()
pieces.append(prompt[last_pos:])
return "".join(pieces)
[docs]
async def resolve_input_info(self, info: ArgumentInfo):
"""Resolve a TypeInputInfo to its actual value."""
if info.type == "literal":
return info.value
if info.type == "history":
path = info.value or info.name
if not self.agent_service or not self.session_id:
logger.error(
f"Cannot load from history for {path}: agent_service or session_id not set"
)
return None
return await self.agent_service.get_history_value(
self.session_id, str(path)
)
# For context type, use info.value (the path) or info.name
path = info.value or info.name
if path:
return self.resolve_context_value(str(path))
return None
[docs]
async def prepare_tool_inputs(self, task: Any) -> dict:
"""Resolve a task/node's ``inputs`` mapping into plain values."""
inputs = {}
for name, info in task.inputs.items():
if info.value is None and info.name is None:
info = info.model_copy(update={"value": name})
value = await self.resolve_input_info(info)
if isinstance(value, BaseModel):
value = value.model_dump()
inputs[name] = value
return inputs