Skip to content

Commit f5c89e3

Browse files
authored
Merge pull request #1033 from rasmi/shared-model-selector
Use shared model selector for agent mediators and participants.
2 parents cfa41ce + 3d20db7 commit f5c89e3

File tree

5 files changed

+148
-134
lines changed

5 files changed

+148
-134
lines changed

docs/researchers/add-agent-participant.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ same way as a human would.
99

1010
## Preparing an Experiment for Agent Participants
1111

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

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

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

2425
**Progress settings**: Agent participants may move through your experiment
2526
faster than you expect, or get stuck on chat stages where you don't expect.

frontend/src/components/experiment_builder/agent_persona_editor.ts

Lines changed: 11 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import '../../pair-components/button';
22
import '../../pair-components/icon';
33
import '../../pair-components/icon_button';
4+
import '../shared/agent_model_selector';
45

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

1718
import {
19+
AgentModelSettings,
1820
AgentPersonaConfig,
1921
AgentPersonaType,
20-
ApiKeyType,
2122
StageConfig,
2223
StageKind,
2324
} from '@deliberation-lab/utils';
@@ -53,8 +54,7 @@ export class AgentPersonaEditorComponent extends MobxLitElement {
5354
${this.renderAgentPrivateName(agentConfig)}
5455
${this.renderAgentPrivateDescription(agentConfig)}
5556
${this.renderAgentName(agentConfig)} ${this.renderAvatars(agentConfig)}
56-
${this.renderAgentApiType(agentConfig)}
57-
${this.renderAgentModel(agentConfig)}
57+
${this.renderModelSelector(agentConfig)}
5858
${this.renderMediatorCohortPreference(agentConfig)}
5959
</div>
6060
<div class="divider main">
@@ -165,82 +165,18 @@ export class AgentPersonaEditorComponent extends MobxLitElement {
165165
`;
166166
}
167167

168-
private renderAgentApiType(agentConfig: AgentPersonaConfig) {
169-
return html`
170-
<div class="section">
171-
<div class="field-title">LLM API</div>
172-
<div class="action-buttons">
173-
${this.renderApiTypeButton(
174-
agentConfig,
175-
'Gemini',
176-
ApiKeyType.GEMINI_API_KEY,
177-
)}
178-
${this.renderApiTypeButton(
179-
agentConfig,
180-
'Vertex AI',
181-
ApiKeyType.VERTEX_AI_API_KEY,
182-
)}
183-
${this.renderApiTypeButton(
184-
agentConfig,
185-
'OpenAI or compatible API',
186-
ApiKeyType.OPENAI_API_KEY,
187-
)}
188-
${this.renderApiTypeButton(
189-
agentConfig,
190-
'Claude or compatible API',
191-
ApiKeyType.CLAUDE_API_KEY,
192-
)}
193-
${this.renderApiTypeButton(
194-
agentConfig,
195-
'Ollama Server',
196-
ApiKeyType.OLLAMA_CUSTOM_URL,
197-
)}
198-
</div>
199-
</div>
200-
`;
201-
}
202-
203-
private renderApiTypeButton(
204-
agentConfig: AgentPersonaConfig,
205-
apiName: string,
206-
apiType: ApiKeyType,
207-
) {
208-
const updateAgentAPI = () => {
209-
this.updatePersona({
210-
defaultModelSettings: {...agentConfig.defaultModelSettings, apiType},
211-
});
168+
private renderModelSelector(agent: AgentPersonaConfig) {
169+
const handleSettingsChange = (e: CustomEvent<AgentModelSettings>) => {
170+
this.updatePersona({defaultModelSettings: e.detail});
212171
};
213172

214-
const isActive = apiType === agentConfig.defaultModelSettings.apiType;
215173
return html`
216-
<pr-button
217-
color="${isActive ? 'primary' : 'neutral'}"
218-
variant=${isActive ? 'tonal' : 'default'}
219-
@click=${updateAgentAPI}
220-
>
221-
${apiName}
222-
</pr-button>
223-
`;
224-
}
225-
226-
private renderAgentModel(agent: AgentPersonaConfig) {
227-
const updateModel = (e: InputEvent) => {
228-
const modelName = (e.target as HTMLTextAreaElement).value;
229-
this.updatePersona({
230-
defaultModelSettings: {...agent.defaultModelSettings, modelName},
231-
});
232-
};
233-
234-
return html`
235-
<md-filled-text-field
236-
required
237-
label="Model ID"
238-
.error=${!agent.defaultModelSettings.modelName}
239-
.value=${agent.defaultModelSettings.modelName}
174+
<agent-model-selector
175+
.apiType=${agent.defaultModelSettings.apiType}
176+
.modelName=${agent.defaultModelSettings.modelName}
240177
?disabled=${!this.experimentEditor.canEditStages}
241-
@input=${updateModel}
242-
>
243-
</md-filled-text-field>
178+
@model-settings-change=${handleSettingsChange}
179+
></agent-model-selector>
244180
`;
245181
}
246182

frontend/src/components/experiment_dashboard/agent_participant_configuration_dialog.ts

Lines changed: 19 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import '../../pair-components/button';
22
import '../../pair-components/icon_button';
3+
import '../shared/agent_model_selector';
4+
35
import '@material/web/textfield/filled-text-field.js';
46

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

1416
import {
17+
AgentModelSettings,
1518
AgentPersonaConfig,
1619
CohortConfig,
17-
ApiKeyType,
18-
AgentPersonaType,
1920
createAgentModelSettings,
2021
DEFAULT_AGENT_PARTICIPANT_ID,
2122
} from '@deliberation-lab/utils';
@@ -38,7 +39,8 @@ export class AgentParticipantDialog extends MobxLitElement {
3839
@property() agentId = '';
3940
@property() promptContext = '';
4041
@property() agent: AgentPersonaConfig | undefined = undefined;
41-
@property() model: string = '';
42+
@property({type: Object}) modelSettings: AgentModelSettings =
43+
createAgentModelSettings();
4244

4345
private close() {
4446
this.dispatchEvent(new CustomEvent('close'));
@@ -71,32 +73,37 @@ export class AgentParticipantDialog extends MobxLitElement {
7173
private resetFields() {
7274
this.agentId = '';
7375
this.promptContext = '';
76+
this.modelSettings = createAgentModelSettings();
7477
}
7578

7679
private renderEdit() {
80+
const handleSettingsChange = (e: CustomEvent<AgentModelSettings>) => {
81+
this.modelSettings = e.detail;
82+
};
83+
7784
return html`
78-
${this.renderAgentModel()} ${this.renderPromptContext()}
85+
<agent-model-selector
86+
.apiType=${this.modelSettings.apiType}
87+
.modelName=${this.modelSettings.modelName}
88+
@model-settings-change=${handleSettingsChange}
89+
></agent-model-selector>
90+
${this.renderPromptContext()}
7991
<div class="buttons-wrapper">
8092
<pr-button
81-
?disabled=${this.model === ''}
93+
?disabled=${!this.modelSettings.modelName}
8294
?loading=${this.isLoading}
8395
@click=${() => {
8496
this.isLoading = true;
8597
this.analyticsService.trackButtonClick(
8698
ButtonClick.AGENT_PARTICIPANT_ADD,
8799
);
88-
if (this.cohort && this.model) {
100+
if (this.cohort && this.modelSettings.modelName) {
89101
this.experimentEditor.addAgentParticipant();
90102
this.agentId = DEFAULT_AGENT_PARTICIPANT_ID;
91-
const modelSettings = createAgentModelSettings({
92-
apiType: ApiKeyType.GEMINI_API_KEY,
93-
modelName: this.model,
94-
});
95-
96103
this.experimentManager.createAgentParticipant(this.cohort.id, {
97104
agentId: this.agentId,
98105
promptContext: this.promptContext,
99-
modelSettings,
106+
modelSettings: this.modelSettings,
100107
});
101108
}
102109
this.resetFields();
@@ -125,47 +132,6 @@ export class AgentParticipantDialog extends MobxLitElement {
125132
`;
126133
}
127134

128-
private renderAgentModel() {
129-
return html`
130-
<div class="selections">
131-
<div>Model to use for this specific agent participant:</div>
132-
<div class="model-selector">
133-
${this.renderModelButton(
134-
'gemini-2.5-flash',
135-
'Gemini 2.5 Flash',
136-
ApiKeyType.GEMINI_API_KEY,
137-
)}
138-
${this.renderModelButton(
139-
'gemini-2.5-pro',
140-
'Gemini 2.5 Pro',
141-
ApiKeyType.GEMINI_API_KEY,
142-
)}
143-
</div>
144-
</div>
145-
`;
146-
}
147-
148-
private renderModelButton(
149-
modelId: string,
150-
modelName: string,
151-
apiType: ApiKeyType,
152-
) {
153-
const updateModel = () => {
154-
this.model = modelId;
155-
};
156-
157-
const isActive = modelId == this.model;
158-
return html`
159-
<pr-button
160-
color="${isActive ? 'primary' : 'neutral'}"
161-
variant=${isActive ? 'tonal' : 'default'}
162-
@click=${updateModel}
163-
>
164-
${modelName}
165-
</pr-button>
166-
`;
167-
}
168-
169135
private renderPromptContext() {
170136
const updatePromptContext = (e: InputEvent) => {
171137
const content = (e.target as HTMLTextAreaElement).value;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@use '../../sass/common';
2+
@use '../../sass/typescale';
3+
4+
:host {
5+
@include common.flex-column;
6+
gap: common.$spacing-large;
7+
}
8+
9+
.field-title {
10+
@include typescale.label-small;
11+
}
12+
13+
.api-type-buttons {
14+
@include common.flex-row;
15+
flex-wrap: wrap;
16+
gap: common.$spacing-medium;
17+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import '../../pair-components/button';
2+
import '@material/web/textfield/filled-text-field.js';
3+
4+
import {LitElement, CSSResultGroup, html} from 'lit';
5+
import {customElement, property} from 'lit/decorators.js';
6+
7+
import {ApiKeyType, getDefaultModelForApiType} from '@deliberation-lab/utils';
8+
9+
import {styles} from './agent_model_selector.scss';
10+
11+
/** Shared component for selecting an LLM API type and model. */
12+
@customElement('agent-model-selector')
13+
export class AgentModelSelector extends LitElement {
14+
static override styles: CSSResultGroup = [styles];
15+
16+
@property() apiType: ApiKeyType = ApiKeyType.GEMINI_API_KEY;
17+
@property() modelName: string = '';
18+
@property({type: Boolean}) disabled = false;
19+
20+
private emitChange(apiType: ApiKeyType, modelName: string) {
21+
this.dispatchEvent(
22+
new CustomEvent('model-settings-change', {
23+
detail: {apiType, modelName},
24+
bubbles: true,
25+
composed: true,
26+
}),
27+
);
28+
}
29+
30+
override render() {
31+
return html` ${this.renderApiType()} ${this.renderModel()} `;
32+
}
33+
34+
private renderApiType() {
35+
return html`
36+
<div>
37+
<div class="field-title">LLM API</div>
38+
<div class="api-type-buttons">
39+
${this.renderApiTypeButton('Gemini', ApiKeyType.GEMINI_API_KEY)}
40+
${this.renderApiTypeButton('Vertex AI', ApiKeyType.VERTEX_AI_API_KEY)}
41+
${this.renderApiTypeButton(
42+
'OpenAI or compatible API',
43+
ApiKeyType.OPENAI_API_KEY,
44+
)}
45+
${this.renderApiTypeButton(
46+
'Claude or compatible API',
47+
ApiKeyType.CLAUDE_API_KEY,
48+
)}
49+
${this.renderApiTypeButton(
50+
'Ollama Server',
51+
ApiKeyType.OLLAMA_CUSTOM_URL,
52+
)}
53+
</div>
54+
</div>
55+
`;
56+
}
57+
58+
private renderApiTypeButton(label: string, apiType: ApiKeyType) {
59+
const isActive = apiType === this.apiType;
60+
return html`
61+
<pr-button
62+
color="${isActive ? 'primary' : 'neutral'}"
63+
variant=${isActive ? 'tonal' : 'default'}
64+
?disabled=${this.disabled}
65+
@click=${() => {
66+
this.emitChange(apiType, getDefaultModelForApiType(apiType));
67+
}}
68+
>
69+
${label}
70+
</pr-button>
71+
`;
72+
}
73+
74+
private renderModel() {
75+
return html`
76+
<md-filled-text-field
77+
label="Model ID"
78+
.value=${this.modelName}
79+
?disabled=${this.disabled}
80+
@input=${(e: InputEvent) => {
81+
const modelName = (e.target as HTMLInputElement).value;
82+
this.emitChange(this.apiType, modelName);
83+
}}
84+
>
85+
</md-filled-text-field>
86+
`;
87+
}
88+
}
89+
90+
declare global {
91+
interface HTMLElementTagNameMap {
92+
'agent-model-selector': AgentModelSelector;
93+
}
94+
}

0 commit comments

Comments
 (0)