Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 7 additions & 6 deletions docs/researchers/add-agent-participant.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ same way as a human would.

## Preparing an Experiment for Agent Participants

You can add agent participants to any experiment, as long as you have an
API key configured for your chosen provider (Gemini, Vertex AI, OpenAI,
Claude, or Ollama). No other experiment-level config is necessary. However, we
recommend setting up your experiments with an eye for how agent participants
will see each stage.
You can add agent participants to any experiment, as long as you have an API
key configured for your chosen provider. No other experiment-level config is
necessary. However, we recommend setting up your experiments with an eye for
how agent participants will see each stage.

**Experiment info**: Agent participants will see any text in the experiment info
stage, but they won't see the contents of a linked Youtube video.

**Stage metadata**: This is where agent participants will see what each stage is
about, so consider how clear your stage names and instructions are.
about, so consider how clear your stage names and instructions are. For
details on what each stage displays to agents, see the
[stage display table](../developers/agent-design#stage-display-in-stage-context-prompt-item).

**Progress settings**: Agent participants may move through your experiment
faster than you expect, or get stuck on chat stages where you don't expect.
Expand Down
86 changes: 11 additions & 75 deletions frontend/src/components/experiment_builder/agent_persona_editor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '../../pair-components/button';
import '../../pair-components/icon';
import '../../pair-components/icon_button';
import '../shared/agent_model_selector';

import '@material/web/textfield/filled-text-field.js';
import '@material/web/checkbox/checkbox.js';
Expand All @@ -15,9 +16,9 @@ import {ExperimentEditor} from '../../services/experiment.editor';
import {ExperimentService} from '../../services/experiment.service';

import {
AgentModelSettings,
AgentPersonaConfig,
AgentPersonaType,
ApiKeyType,
StageConfig,
StageKind,
} from '@deliberation-lab/utils';
Expand Down Expand Up @@ -53,8 +54,7 @@ export class AgentPersonaEditorComponent extends MobxLitElement {
${this.renderAgentPrivateName(agentConfig)}
${this.renderAgentPrivateDescription(agentConfig)}
${this.renderAgentName(agentConfig)} ${this.renderAvatars(agentConfig)}
${this.renderAgentApiType(agentConfig)}
${this.renderAgentModel(agentConfig)}
${this.renderModelSelector(agentConfig)}
${this.renderMediatorCohortPreference(agentConfig)}
</div>
<div class="divider main">
Expand Down Expand Up @@ -165,82 +165,18 @@ export class AgentPersonaEditorComponent extends MobxLitElement {
`;
}

private renderAgentApiType(agentConfig: AgentPersonaConfig) {
return html`
<div class="section">
<div class="field-title">LLM API</div>
<div class="action-buttons">
${this.renderApiTypeButton(
agentConfig,
'Gemini',
ApiKeyType.GEMINI_API_KEY,
)}
${this.renderApiTypeButton(
agentConfig,
'Vertex AI',
ApiKeyType.VERTEX_AI_API_KEY,
)}
${this.renderApiTypeButton(
agentConfig,
'OpenAI or compatible API',
ApiKeyType.OPENAI_API_KEY,
)}
${this.renderApiTypeButton(
agentConfig,
'Claude or compatible API',
ApiKeyType.CLAUDE_API_KEY,
)}
${this.renderApiTypeButton(
agentConfig,
'Ollama Server',
ApiKeyType.OLLAMA_CUSTOM_URL,
)}
</div>
</div>
`;
}

private renderApiTypeButton(
agentConfig: AgentPersonaConfig,
apiName: string,
apiType: ApiKeyType,
) {
const updateAgentAPI = () => {
this.updatePersona({
defaultModelSettings: {...agentConfig.defaultModelSettings, apiType},
});
private renderModelSelector(agent: AgentPersonaConfig) {
const handleSettingsChange = (e: CustomEvent<AgentModelSettings>) => {
this.updatePersona({defaultModelSettings: e.detail});
};

const isActive = apiType === agentConfig.defaultModelSettings.apiType;
return html`
<pr-button
color="${isActive ? 'primary' : 'neutral'}"
variant=${isActive ? 'tonal' : 'default'}
@click=${updateAgentAPI}
>
${apiName}
</pr-button>
`;
}

private renderAgentModel(agent: AgentPersonaConfig) {
const updateModel = (e: InputEvent) => {
const modelName = (e.target as HTMLTextAreaElement).value;
this.updatePersona({
defaultModelSettings: {...agent.defaultModelSettings, modelName},
});
};

return html`
<md-filled-text-field
required
label="Model ID"
.error=${!agent.defaultModelSettings.modelName}
.value=${agent.defaultModelSettings.modelName}
<agent-model-selector
.apiType=${agent.defaultModelSettings.apiType}
.modelName=${agent.defaultModelSettings.modelName}
?disabled=${!this.experimentEditor.canEditStages}
@input=${updateModel}
>
</md-filled-text-field>
@model-settings-change=${handleSettingsChange}
></agent-model-selector>
`;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import '../../pair-components/button';
import '../../pair-components/icon_button';
import '../shared/agent_model_selector';

import '@material/web/textfield/filled-text-field.js';

import {MobxLitElement} from '@adobe/lit-mobx';
Expand All @@ -12,10 +14,9 @@ import {ExperimentEditor} from '../../services/experiment.editor';
import {ExperimentManager} from '../../services/experiment.manager';

import {
AgentModelSettings,
AgentPersonaConfig,
CohortConfig,
ApiKeyType,
AgentPersonaType,
createAgentModelSettings,
DEFAULT_AGENT_PARTICIPANT_ID,
} from '@deliberation-lab/utils';
Expand All @@ -38,7 +39,8 @@ export class AgentParticipantDialog extends MobxLitElement {
@property() agentId = '';
@property() promptContext = '';
@property() agent: AgentPersonaConfig | undefined = undefined;
@property() model: string = '';
@property({type: Object}) modelSettings: AgentModelSettings =
createAgentModelSettings();

private close() {
this.dispatchEvent(new CustomEvent('close'));
Expand Down Expand Up @@ -71,32 +73,37 @@ export class AgentParticipantDialog extends MobxLitElement {
private resetFields() {
this.agentId = '';
this.promptContext = '';
this.modelSettings = createAgentModelSettings();
}

private renderEdit() {
const handleSettingsChange = (e: CustomEvent<AgentModelSettings>) => {
this.modelSettings = e.detail;
};

return html`
${this.renderAgentModel()} ${this.renderPromptContext()}
<agent-model-selector
.apiType=${this.modelSettings.apiType}
.modelName=${this.modelSettings.modelName}
@model-settings-change=${handleSettingsChange}
></agent-model-selector>
${this.renderPromptContext()}
<div class="buttons-wrapper">
<pr-button
?disabled=${this.model === ''}
?disabled=${!this.modelSettings.modelName}
?loading=${this.isLoading}
@click=${() => {
this.isLoading = true;
this.analyticsService.trackButtonClick(
ButtonClick.AGENT_PARTICIPANT_ADD,
);
if (this.cohort && this.model) {
if (this.cohort && this.modelSettings.modelName) {
this.experimentEditor.addAgentParticipant();
this.agentId = DEFAULT_AGENT_PARTICIPANT_ID;
const modelSettings = createAgentModelSettings({
apiType: ApiKeyType.GEMINI_API_KEY,
modelName: this.model,
});

this.experimentManager.createAgentParticipant(this.cohort.id, {
agentId: this.agentId,
promptContext: this.promptContext,
modelSettings,
modelSettings: this.modelSettings,
});
}
this.resetFields();
Expand Down Expand Up @@ -125,47 +132,6 @@ export class AgentParticipantDialog extends MobxLitElement {
`;
}

private renderAgentModel() {
return html`
<div class="selections">
<div>Model to use for this specific agent participant:</div>
<div class="model-selector">
${this.renderModelButton(
'gemini-2.5-flash',
'Gemini 2.5 Flash',
ApiKeyType.GEMINI_API_KEY,
)}
${this.renderModelButton(
'gemini-2.5-pro',
'Gemini 2.5 Pro',
ApiKeyType.GEMINI_API_KEY,
)}
</div>
</div>
`;
}

private renderModelButton(
modelId: string,
modelName: string,
apiType: ApiKeyType,
) {
const updateModel = () => {
this.model = modelId;
};

const isActive = modelId == this.model;
return html`
<pr-button
color="${isActive ? 'primary' : 'neutral'}"
variant=${isActive ? 'tonal' : 'default'}
@click=${updateModel}
>
${modelName}
</pr-button>
`;
}

private renderPromptContext() {
const updatePromptContext = (e: InputEvent) => {
const content = (e.target as HTMLTextAreaElement).value;
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/components/shared/agent_model_selector.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@use '../../sass/common';
@use '../../sass/typescale';

:host {
@include common.flex-column;
gap: common.$spacing-large;
}

.field-title {
@include typescale.label-small;
}

.api-type-buttons {
@include common.flex-row;
flex-wrap: wrap;
gap: common.$spacing-medium;
}
94 changes: 94 additions & 0 deletions frontend/src/components/shared/agent_model_selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import '../../pair-components/button';
import '@material/web/textfield/filled-text-field.js';

import {LitElement, CSSResultGroup, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

import {ApiKeyType, getDefaultModelForApiType} from '@deliberation-lab/utils';

import {styles} from './agent_model_selector.scss';

/** Shared component for selecting an LLM API type and model. */
@customElement('agent-model-selector')
export class AgentModelSelector extends LitElement {
static override styles: CSSResultGroup = [styles];

@property() apiType: ApiKeyType = ApiKeyType.GEMINI_API_KEY;
@property() modelName: string = '';
@property({type: Boolean}) disabled = false;

private emitChange(apiType: ApiKeyType, modelName: string) {
this.dispatchEvent(
new CustomEvent('model-settings-change', {
detail: {apiType, modelName},
bubbles: true,
composed: true,
}),
);
}

override render() {
return html` ${this.renderApiType()} ${this.renderModel()} `;
}

private renderApiType() {
return html`
<div>
<div class="field-title">LLM API</div>
<div class="api-type-buttons">
${this.renderApiTypeButton('Gemini', ApiKeyType.GEMINI_API_KEY)}
${this.renderApiTypeButton('Vertex AI', ApiKeyType.VERTEX_AI_API_KEY)}
${this.renderApiTypeButton(
'OpenAI or compatible API',
ApiKeyType.OPENAI_API_KEY,
)}
${this.renderApiTypeButton(
'Claude or compatible API',
ApiKeyType.CLAUDE_API_KEY,
)}
${this.renderApiTypeButton(
'Ollama Server',
ApiKeyType.OLLAMA_CUSTOM_URL,
)}
</div>
</div>
`;
}

private renderApiTypeButton(label: string, apiType: ApiKeyType) {
const isActive = apiType === this.apiType;
return html`
<pr-button
color="${isActive ? 'primary' : 'neutral'}"
variant=${isActive ? 'tonal' : 'default'}
?disabled=${this.disabled}
@click=${() => {
this.emitChange(apiType, getDefaultModelForApiType(apiType));
}}
>
${label}
</pr-button>
`;
}

private renderModel() {
return html`
<md-filled-text-field
label="Model ID"
.value=${this.modelName}
?disabled=${this.disabled}
@input=${(e: InputEvent) => {
const modelName = (e.target as HTMLInputElement).value;
this.emitChange(this.apiType, modelName);
}}
>
</md-filled-text-field>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'agent-model-selector': AgentModelSelector;
}
}