Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/sdk/server-ai/src/ldai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
AIAgentConfig, AIAgentConfigDefault, AIAgentConfigRequest,
AIAgentGraphConfig, AIAgents, AICompletionConfig,
AICompletionConfigDefault, AIConfig, AIJudgeConfig, AIJudgeConfigDefault,
Edge, JudgeConfiguration, LDAIAgent, LDAIAgentConfig, LDAIAgentDefaults,
LDMessage, ModelConfig, ProviderConfig)
AITool, Edge, JudgeConfiguration, LDAIAgent, LDAIAgentConfig,
LDAIAgentDefaults, LDMessage, ModelConfig, ProviderConfig)
from ldai.providers.types import EvalScore, JudgeResponse
from ldai.tracker import AIGraphTracker

Expand All @@ -23,6 +23,7 @@
'AIAgents',
'AIAgentGraphConfig',
'AIGraphTracker',
'AITool',
'Edge',
'AICompletionConfig',
'AICompletionConfigDefault',
Expand Down
38 changes: 35 additions & 3 deletions packages/sdk/server-ai/src/ldai/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ldai.models import (AIAgentConfig, AIAgentConfigDefault,
AIAgentConfigRequest, AIAgentGraphConfig, AIAgents,
AICompletionConfig, AICompletionConfigDefault,
AIJudgeConfig, AIJudgeConfigDefault, Edge,
AIJudgeConfig, AIJudgeConfigDefault, AITool, Edge,
JudgeConfiguration, LDMessage, ModelConfig,
ProviderConfig)
from ldai.providers.ai_provider_factory import AIProviderFactory
Expand Down Expand Up @@ -52,15 +52,31 @@ def _completion_config(
default: AICompletionConfigDefault,
variables: Optional[Dict[str, Any]] = None,
) -> AICompletionConfig:
model, provider, messages, instructions, tracker, enabled, judge_configuration, _ = self.__evaluate(
model, provider, messages, instructions, tracker, enabled, judge_configuration, variation = self.__evaluate(
key, context, default.to_dict(), variables
)

# Parse tools from variation data
tools = None
if 'tools' in variation and isinstance(variation['tools'], list):
tools = [
AITool(
key=tool['key'],
version=tool.get('version', 0),
instructions=tool.get('instructions'),
examples=tool.get('examples'),
custom_parameters=tool.get('customParameters'),
)
for tool in variation['tools']
if isinstance(tool, dict) and 'key' in tool
] or None
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated tool-parsing logic across two methods

Low Severity

The tool-parsing block in _completion_config and __evaluate_agent is completely identical — same list comprehension, same AITool construction, same filter, same or None coercion. This duplication means any future bug fix or field addition needs to be applied in both places, risking divergence. Extracting to a shared helper (similar to how __evaluate is shared) would eliminate this risk.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid observation. The duplication is intentional to keep the shared __evaluate method unchanged and limit blast radius. Extracting to a helper is a reasonable follow-up refactor but deferring for now to keep this PR focused on the feature addition.


config = AICompletionConfig(
key=key,
enabled=bool(enabled),
model=model,
messages=messages,
tools=tools if tools is not None else (default.tools if default.tools else None),
provider=provider,
tracker=tracker,
judge_configuration=judge_configuration,
Expand Down Expand Up @@ -706,19 +722,35 @@ def __evaluate_agent(
:param variables: Variables for interpolation.
:return: Configured AIAgentConfig instance.
"""
model, provider, messages, instructions, tracker, enabled, judge_configuration, _ = self.__evaluate(
model, provider, messages, instructions, tracker, enabled, judge_configuration, variation = self.__evaluate(
key, context, default.to_dict(), variables
)

# For agents, prioritize instructions over messages
final_instructions = instructions if instructions is not None else default.instructions

# Parse tools from variation data
tools = None
if 'tools' in variation and isinstance(variation['tools'], list):
tools = [
AITool(
key=tool['key'],
version=tool.get('version', 0),
instructions=tool.get('instructions'),
examples=tool.get('examples'),
custom_parameters=tool.get('customParameters'),
)
for tool in variation['tools']
if isinstance(tool, dict) and 'key' in tool
] or None

return AIAgentConfig(
key=key,
enabled=bool(enabled) if enabled is not None else (default.enabled or False),
model=model or default.model,
provider=provider or default.provider,
instructions=final_instructions,
tools=tools if tools is not None else (default.tools if default.tools else None),
tracker=tracker,
judge_configuration=judge_configuration or default.judge_configuration,
)
Expand Down
44 changes: 44 additions & 0 deletions packages/sdk/server-ai/src/ldai/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,38 @@ def to_dict(self) -> dict:
}


# ============================================================================
# Tool Types
# ============================================================================

@dataclass(frozen=True)
class AITool:
"""
Configuration for an AI tool.
"""
key: str
version: int
instructions: Optional[str] = None
examples: Optional[str] = None
custom_parameters: Optional[Dict[str, Any]] = None

def to_dict(self) -> dict:
"""
Render the tool as a dictionary object.
"""
result: Dict[str, Any] = {
'key': self.key,
'version': self.version,
}
if self.instructions is not None:
result['instructions'] = self.instructions
if self.examples is not None:
result['examples'] = self.examples
if self.custom_parameters is not None:
result['customParameters'] = self.custom_parameters
return result


# ============================================================================
# Base AI Config Types
# ============================================================================
Expand Down Expand Up @@ -207,6 +239,7 @@ class AICompletionConfigDefault(AIConfigDefault):
Default Completion AI Config (default mode).
"""
messages: Optional[List[LDMessage]] = None
tools: Optional[List[AITool]] = None
judge_configuration: Optional[JudgeConfiguration] = None

def to_dict(self) -> dict:
Expand All @@ -215,6 +248,8 @@ def to_dict(self) -> dict:
"""
result = self._base_to_dict()
result['messages'] = [message.to_dict() for message in self.messages] if self.messages else None
if self.tools is not None:
result['tools'] = [tool.to_dict() for tool in self.tools]
if self.judge_configuration is not None:
result['judgeConfiguration'] = self.judge_configuration.to_dict()
return result
Expand All @@ -226,6 +261,7 @@ class AICompletionConfig(AIConfig):
Completion AI Config (default mode).
"""
messages: Optional[List[LDMessage]] = None
tools: Optional[List[AITool]] = None
judge_configuration: Optional[JudgeConfiguration] = None

def to_dict(self) -> dict:
Expand All @@ -234,6 +270,8 @@ def to_dict(self) -> dict:
"""
result = self._base_to_dict()
result['messages'] = [message.to_dict() for message in self.messages] if self.messages else None
if self.tools is not None:
result['tools'] = [tool.to_dict() for tool in self.tools]
if self.judge_configuration is not None:
result['judgeConfiguration'] = self.judge_configuration.to_dict()
return result
Expand All @@ -249,6 +287,7 @@ class AIAgentConfigDefault(AIConfigDefault):
Default Agent-specific AI Config with instructions.
"""
instructions: Optional[str] = None
tools: Optional[List[AITool]] = None
judge_configuration: Optional[JudgeConfiguration] = None

def to_dict(self) -> Dict[str, Any]:
Expand All @@ -258,6 +297,8 @@ def to_dict(self) -> Dict[str, Any]:
result = self._base_to_dict()
if self.instructions is not None:
result['instructions'] = self.instructions
if self.tools is not None:
result['tools'] = [tool.to_dict() for tool in self.tools]
if self.judge_configuration is not None:
result['judgeConfiguration'] = self.judge_configuration.to_dict()
return result
Expand All @@ -269,6 +310,7 @@ class AIAgentConfig(AIConfig):
Agent-specific AI Config with instructions.
"""
instructions: Optional[str] = None
tools: Optional[List[AITool]] = None
judge_configuration: Optional[JudgeConfiguration] = None

def to_dict(self) -> Dict[str, Any]:
Expand All @@ -278,6 +320,8 @@ def to_dict(self) -> Dict[str, Any]:
result = self._base_to_dict()
if self.instructions is not None:
result['instructions'] = self.instructions
if self.tools is not None:
result['tools'] = [tool.to_dict() for tool in self.tools]
if self.judge_configuration is not None:
result['judgeConfiguration'] = self.judge_configuration.to_dict()
return result
Expand Down
Loading
Loading