Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c6431b7
docs: add tool configuration model refactor spec
Davsooonowy May 14, 2026
38923cb
docs: add tool configuration model refactor implementation plan
Davsooonowy May 14, 2026
fd9f4eb
feat: replace positional tool config lookup with named lookup in set_…
Davsooonowy May 15, 2026
85fe991
fix: address code review feedback on set_runtime_arguments
Davsooonowy May 15, 2026
fee52f8
feat: build tool_config dict in agent preconfiguration service
Davsooonowy May 15, 2026
98444b7
fix: add missing tool_config field tests, fix views.py filter, fix er…
Davsooonowy May 15, 2026
0e8401e
fix: address code quality issues in tool_config refactor
Davsooonowy May 15, 2026
0dd4e39
fix: align test fixtures to use CONFIGURATION_ARGS attribute name
Davsooonowy May 15, 2026
da6df71
fix: use FunctionToolConfig wrapper in test_fields fixtures
Davsooonowy May 15, 2026
3724e72
feat: update AgentConfig and AgentChoice types for tool_config dict s…
Davsooonowy May 15, 2026
1244c48
feat: update frontend to handle tool_config dict in agent configurati…
Davsooonowy May 15, 2026
c15e098
fix: correct hasConfigFields logic and add tool label in tool_config …
Davsooonowy May 15, 2026
14e6d00
fix: preserve JSON values in tool_config flatten and fix error parsin…
Davsooonowy May 15, 2026
56a9c4f
fix: rename prompt_inputs to prompt_input, remove dead PydanticModelT…
Davsooonowy May 15, 2026
42c178b
fix: add test for TypeError in verifyagents and fix blank line in fie…
Davsooonowy May 15, 2026
fd2ca8b
fix: use instance-based assertion in non_dict_config_value test
Davsooonowy May 15, 2026
0ca93d6
docs: add tool config refactor spec to specs/, remove docs/superpower…
Davsooonowy May 15, 2026
a3c2cce
fix: add missing newline in test_runtime_arguments.py
Davsooonowy May 15, 2026
f8161db
fix: restore trailing whitespace stripped by editor in form-utils and…
Davsooonowy May 15, 2026
d8b83f8
fix: correct error format, tool_config prefix, and AttributeError guard
Davsooonowy May 18, 2026
82fe0cb
refactor: update tool configuration model field names for clarity
Davsooonowy May 19, 2026
5481d39
fix: align AgentChoice tool_config type and accept tools without CONF…
Davsooonowy May 19, 2026
753cdf7
chore: remove dead code
Davsooonowy May 19, 2026
08f42fe
chore: refactor
Davsooonowy May 19, 2026
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
85 changes: 42 additions & 43 deletions frontend/src/components/agents-table/agent-configuration-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { DynamicConfigInput } from "./dynamic-config-input";
import { TypeInfo } from "@/lib/types";

interface AgentConfigurationFormProps {
agentConfigSections: Record<string, Record<string, unknown> | Record<string, unknown>[]>;
agentConfigSections: Record<string, Record<string, unknown>>;
openSections: Record<string, boolean>;
setOpenSections: (sections: Record<string, boolean>) => void;
config: Record<string, string | number | boolean>;
Expand All @@ -27,18 +27,13 @@ export function AgentConfigurationForm({
};

useEffect(() => {
const checkForSectionErrors = (section: string, fields: Record<string, unknown> | Record<string, unknown>[]) => {
if (Array.isArray(fields)) {
return fields.some(obj => {
if (obj && typeof obj === 'object') {
return Object.keys(obj).some(key => fieldErrors[`${section}_${key}`]);
}
return false;
});
} else if (fields && typeof fields === 'object') {
return Object.keys(fields).some(key => fieldErrors[`${section}_${key}`]);
const checkForSectionErrors = (section: string, fields: Record<string, unknown>) => {
if (section === 'tool_config') {
return Object.entries(fields as Record<string, Record<string, unknown>>).some(([toolName, toolFields]) =>
Object.keys(toolFields || {}).some(key => fieldErrors[`tool_config_${toolName}_${key}`])
);
}
return false;
return Object.keys(fields).some(key => fieldErrors[`${section}_${key}`]);
};

const expandSectionsWithErrors = () => {
Expand All @@ -64,13 +59,13 @@ export function AgentConfigurationForm({
expandSectionsWithErrors();
}, [fieldErrors, agentConfigSections, openSections, setOpenSections]);

const hasConfigFields = Object.values(agentConfigSections).some(section => {
if (Array.isArray(section)) {
return section.some(obj => obj && typeof obj === 'object' && Object.keys(obj).length > 0);
} else if (section && typeof section === 'object') {
return Object.keys(section).length > 0;
const hasConfigFields = Object.entries(agentConfigSections).some(([section, sectionData]) => {
if (section === 'tool_config') {
return Object.values(sectionData as Record<string, Record<string, unknown>>).some(
toolFields => toolFields && typeof toolFields === 'object' && Object.keys(toolFields).length > 0
);
}
return false;
return sectionData !== null && sectionData !== undefined && typeof sectionData === 'object' && Object.keys(sectionData as object).length > 0;
});

const renderConfigField = (key: string, schema: unknown, configKey: string) => {
Expand All @@ -95,16 +90,25 @@ export function AgentConfigurationForm({
);
};

const renderToolsArrayFields = (section: string, fields: Record<string, unknown>[]) => {
return fields.map((obj, idx) => {
const sObj = obj && typeof obj === 'object' ? obj : {};
if (Object.keys(sObj).length === 0) return null;

const renderRegularFields = (section: string, fields: Record<string, unknown>) => {
return Object.entries(fields).map(([key, schema]) => {
const configKey = `${section}_${key}`;
return typeof key === 'string' ? renderConfigField(key, schema, configKey) : null;
});
};

const renderToolConfigFields = (fields: Record<string, Record<string, unknown>>) => {
return Object.entries(fields).map(([toolName, toolFields]) => {
const sFields = toolFields && typeof toolFields === 'object' ? toolFields : {};
if (Object.keys(sFields).length === 0) return null;
return (
<div key={idx} className="pl-4 border-l-2 border-muted bg-muted/20 rounded-r-md p-3">
<div key={toolName} className="pl-4 border-l-2 border-muted bg-muted/20 rounded-r-md p-3">
<p className="text-xs font-medium text-muted-foreground mb-2 capitalize">
{toolName.replace(/_/g, ' ')}
</p>
<div className="space-y-3">
{Object.entries(sObj).map(([key, schema]) => {
const configKey = `${section}_${key}`;
{Object.entries(sFields).map(([key, schema]) => {
const configKey = `tool_config_${toolName}_${key}`;
return typeof key === 'string' ? renderConfigField(key, schema, configKey) : null;
})}
</div>
Expand All @@ -113,28 +117,23 @@ export function AgentConfigurationForm({
});
};

const renderRegularFields = (section: string, fields: Record<string, unknown>) => {
return Object.entries(fields).map(([key, schema]) => {
const configKey = `${section}_${key}`;
return typeof key === 'string' ? renderConfigField(key, schema, configKey) : null;
});
};

const renderConfigSection = (section: string, fields: Record<string, unknown> | Record<string, unknown>[]) => {
const renderConfigSection = (section: string, fields: Record<string, unknown>) => {
let hasFields = false;
if (Array.isArray(fields)) {
hasFields = fields.some(obj => obj && typeof obj === 'object' && Object.keys(obj).length > 0);
} else if (fields && typeof fields === 'object') {
if (section === 'tool_config') {
hasFields = Object.values(fields as Record<string, Record<string, unknown>>).some(
toolFields => toolFields && typeof toolFields === 'object' && Object.keys(toolFields).length > 0
);
} else {
hasFields = Object.keys(fields).length > 0;
}
if (!hasFields) return null;

const sectionTitle = String(section).replace(/_/g, ' ');

return (
<Collapsible
key={section}
open={openSections[section]}
<Collapsible
key={section}
open={openSections[section]}
onOpenChange={(open) => setOpenSections({ ...openSections, [section]: open })}
>
<CollapsibleTrigger asChild>
Expand All @@ -149,9 +148,9 @@ export function AgentConfigurationForm({
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 mt-2">
<div className="pl-4 space-y-4">
{Array.isArray(fields)
? renderToolsArrayFields(section, fields)
: renderRegularFields(section, fields as Record<string, unknown>)}
{section === 'tool_config'
? renderToolConfigFields(fields as Record<string, Record<string, unknown>>)
: renderRegularFields(section, fields)}
</div>
</CollapsibleContent>
</Collapsible>
Expand Down
75 changes: 37 additions & 38 deletions frontend/src/components/agents-table/hooks/use-agent-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function useAgentForm(
const [description, setDescription] = useState("");
const [type, setType] = useState("");
const [config, setConfig] = useState<Record<string, string | number | boolean>>({});
const [agentConfigSections, setAgentConfigSections] = useState<Record<string, Record<string, unknown> | Record<string, unknown>[]> >({});
const [agentConfigSections, setAgentConfigSections] = useState<Record<string, Record<string, unknown>>>({});
const [openSections, setOpenSections] = useState<Record<string, boolean>>({});
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const [generalError, setGeneralError] = useState<string>("");
Expand Down Expand Up @@ -74,7 +74,7 @@ export function useAgentForm(
};

const extractConfigSectionsFromType = (selectedType: AgentChoice) => {
const configSections: Record<string, Record<string, unknown> | Record<string, unknown>[]> = {};
const configSections: Record<string, Record<string, unknown>> = {};
Object.entries(selectedType).forEach(([section, value]) => {
if (section !== 'key' && section !== 'name' && value && typeof value === 'object') {
configSections[section] = value;
Expand All @@ -94,10 +94,10 @@ export function useAgentForm(
const createDefaultConfigValues = (configSections: Record<string, unknown>) => {
const defaults: Record<string, string | number | boolean> = {};
Object.entries(configSections).forEach(([section, sectionData]) => {
if (Array.isArray(sectionData)) {
sectionData.forEach(obj => {
if (obj && typeof obj === 'object') {
Object.keys(obj).forEach(k => { defaults[`${section}_${k}`] = ""; });
if (section === 'tool_config' && sectionData && typeof sectionData === 'object' && !Array.isArray(sectionData)) {
Object.entries(sectionData as Record<string, Record<string, unknown>>).forEach(([toolName, toolFields]) => {
if (toolFields && typeof toolFields === 'object') {
Object.keys(toolFields).forEach(k => { defaults[`tool_config_${toolName}_${k}`] = ""; });
}
});
} else if (sectionData && typeof sectionData === 'object') {
Expand Down Expand Up @@ -138,31 +138,32 @@ export function useAgentForm(
setConfig(prev => ({ ...prev, [key]: value }));
};

const buildToolConfigDictConfig = (fields: Record<string, Record<string, unknown>>, currentConfig: Record<string, string | number | boolean>) => {
const out: Record<string, Record<string, string | number | boolean>> = {};
Object.entries(fields).forEach(([toolName, toolFields]) => {
out[toolName] = {};
if (toolFields && typeof toolFields === 'object') {
Object.keys(toolFields).forEach(k => {
const v = currentConfig[`tool_config_${toolName}_${k}`];
if (v !== '' && v !== null && v !== undefined) out[toolName][k] = v;
});
}
});
return out;
};

const buildNestedConfigFromFlattened = () => {
const configObj: Record<string, unknown> = {};
Object.entries(agentConfigSections).forEach(([section, fields]) => {
if (Array.isArray(fields)) {
configObj[section] = buildToolsArrayConfig(section, fields);
if (section === 'tool_config') {
configObj[section] = buildToolConfigDictConfig(fields as Record<string, Record<string, unknown>>, config);
} else if (fields && typeof fields === 'object') {
configObj[section] = buildRegularSectionConfig(section, fields);
}
});
return configObj;
};

const buildToolsArrayConfig = (section: string, fields: Record<string, unknown>[]) => {
return fields.map(obj => {
const out: Record<string, string | number | boolean> = {};
if (obj && typeof obj === 'object') {
Object.keys(obj).forEach(k => {
const v = config[`${section}_${k}`];
if (v !== '' && v !== null && v !== undefined) out[k] = v;
});
}
return out;
});
};

const buildRegularSectionConfig = (section: string, fields: Record<string, unknown>) => {
const out: Record<string, string | number | boolean> = {};
Object.keys(fields).forEach(k => {
Expand Down Expand Up @@ -191,20 +192,18 @@ export function useAgentForm(
};

const parseFieldErrors = (errorData: unknown) => {
const newFieldErrors = parseFieldErrorsBase(errorData);

const { config: configErrors, ...topLevelErrors } = (errorData as Record<string, unknown>) ?? {};
const newFieldErrors = parseFieldErrorsBase(topLevelErrors);

// Add agent-specific error parsing for complex sections
if (typeof errorData === 'object' && errorData && 'config' in errorData) {
const configErrors = (errorData as { config: unknown }).config;
if (typeof configErrors === 'object' && configErrors) {
const sectionNames = ['agent_args', 'prompt_input', 'prompt_extension'];

sectionNames.forEach(sectionName => {
parseSingleConfigSection(sectionName, (configErrors as Record<string, unknown>)[sectionName], newFieldErrors);
});

parseToolsArrayErrors((configErrors as Record<string, unknown>).tools, newFieldErrors);
}
if (typeof configErrors === 'object' && configErrors) {
const sectionNames = ['agent_args', 'prompt_input', 'prompt_extension'];

sectionNames.forEach(sectionName => {
parseSingleConfigSection(sectionName, (configErrors as Record<string, unknown>)[sectionName], newFieldErrors);
});

parseToolConfigErrors((configErrors as Record<string, unknown>).tool_config, newFieldErrors);
}

return newFieldErrors;
Expand All @@ -218,12 +217,12 @@ export function useAgentForm(
}
};

const parseToolsArrayErrors = (toolsErrors: unknown, newFieldErrors: Record<string, string>) => {
if (toolsErrors && Array.isArray(toolsErrors)) {
toolsErrors.forEach((toolErrors: unknown) => {
const parseToolConfigErrors = (toolConfigErrors: unknown, newFieldErrors: Record<string, string>) => {
if (toolConfigErrors && typeof toolConfigErrors === 'object' && !Array.isArray(toolConfigErrors)) {
Object.entries(toolConfigErrors as Record<string, unknown>).forEach(([toolName, toolErrors]) => {
if (toolErrors && typeof toolErrors === 'object') {
Object.entries(toolErrors as Record<string, unknown>).forEach(([field, error]) => {
newFieldErrors[`tools_${field}`] = Array.isArray(error) ? String(error[0]) : String(error);
newFieldErrors[`tool_config_${toolName}_${field}`] = Array.isArray(error) ? String(error[0]) : String(error);
});
}
});
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/lib/api/agents.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {BaseApiClient} from "@/lib/api/base.ts";
import {Agent, AgentConfig, AgentDetails} from "@/lib/types.ts";
import {Agent, AgentConfig, AgentDetails, ExtraArgDetail} from "@/lib/types.ts";
import {ApiError} from "@/lib/api-error.ts";

export type AgentChoice = {
key: string;
name: string;
agent_args: Record<string, string>;
prompt_inputs: Record<string, string>;
prompt_input: Record<string, string>;
prompt_extension: Record<string, string>;
tools: Record<string, string>[];
tool_config: Record<string, Record<string, ExtraArgDetail>>;
};

type AvailableAgentsResponse = {
Expand Down
17 changes: 9 additions & 8 deletions frontend/src/lib/form-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

export function flattenConfigForForm(
config: unknown,
config: unknown,
sectionMapping?: Record<string, string>
): Record<string, string | number | boolean> {
const flattenedConfig: Record<string, string | number | boolean> = {};
Expand All @@ -15,17 +15,18 @@ export function flattenConfigForForm(
Object.entries(config as Record<string, unknown>).forEach(([section, sectionData]) => {
const frontendSection = sectionMapping?.[section] || section;

if (Array.isArray(sectionData)) {
sectionData.forEach(obj => {
if (obj && typeof obj === 'object') {
Object.entries(obj).forEach(([key, value]) => {
if (section === 'tool_config' && sectionData && typeof sectionData === 'object' && !Array.isArray(sectionData)) {
// 2-level: {toolName: {fieldName: value}}
Object.entries(sectionData as Record<string, Record<string, unknown>>).forEach(([toolName, toolFields]) => {
if (toolFields && typeof toolFields === 'object') {
Object.entries(toolFields).forEach(([key, value]) => {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
flattenedConfig[`${frontendSection}_${key}`] = value;
flattenedConfig[`${section}_${toolName}_${key}`] = value;
} else if (typeof value === 'object' && value !== null) {
try {
flattenedConfig[`${frontendSection}_${key}`] = JSON.stringify(value);
flattenedConfig[`${section}_${toolName}_${key}`] = JSON.stringify(value);
} catch {
flattenedConfig[`${frontendSection}_${key}`] = '{}';
flattenedConfig[`${section}_${toolName}_${key}`] = '{}';
}
}
});
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export type AgentConfig = {
agent_args?: Record<string, ExtraArgDetail>;
prompt_input?: Record<string, ExtraArgDetail>;
prompt_extension?: Record<string, ExtraArgDetail>;
tools?: Array<Record<string, ExtraArgDetail>>;
tool_config?: Record<string, Record<string, ExtraArgDetail>>;
};

export type ConversationFile = {
Expand Down
11 changes: 8 additions & 3 deletions plugins/enthusiast-common/enthusiast_common/agents/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,17 @@ def _get_system_prompt_variables(self) -> dict:
return {}

def set_runtime_arguments(self, runtime_arguments: Any) -> None:
tools_runtime_arguments = runtime_arguments.pop("tools")
"""Inject stored config values into agent fields and named tool instances."""
tool_config = runtime_arguments.get("tool_config", {})
for key, value in runtime_arguments.items():
if key == "tool_config":
continue
class_field_key = key.upper()
field = getattr(self, class_field_key)
if field is None:
continue
setattr(self, key.upper(), field(**value))
for index, tool_runtime_args in enumerate(tools_runtime_arguments):
self._tools[index].set_runtime_arguments(tool_runtime_args)
for tool in self._tools:
tool_runtime_args = tool_config.get(tool.NAME)
if tool_runtime_args is not None:
tool.set_runtime_arguments(tool_runtime_args)
6 changes: 5 additions & 1 deletion plugins/enthusiast-common/enthusiast_common/agents/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from enum import StrEnum
from typing import TYPE_CHECKING

from ..config import AgentConfig
if TYPE_CHECKING:
from ..config.base import AgentConfig


class ConfigType(StrEnum):
Expand Down
Loading
Loading