Skip to content
Open
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
10 changes: 10 additions & 0 deletions docs/researchers/add-agent-participant.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ to the agent. The prompt context may be useful for e.g. giving different
personalities or instructions to different agents. These settings will apply for
that agent across all experiment stages.

### Generating and Enhancing Personas

To help create diverse and high-fidelity personas, the configuration window includes three AI-assisted tools:

- **🪄 Generate**: Creates a full character sketch (~200–250 words) covering demographics, Big Five personality traits, values, cognitive style, and communication style. Uses a dry, factual, dossier-style tone. If you have already started writing a persona (e.g., "Your name is Joe"), Generate will expand it into a complete sketch while preserving what you wrote. Each generation samples a unique combination of age, education level, pronouns, setting, and verbosity to ensure diversity.
- **✨ Enhance**: Appends 1–2 short, concrete episodic memories or personal experiences to the existing sketch (e.g., a past job conflict or a life event). These "memories" give the agent something specific to draw on during conversation, making it feel more like a real person and less like an LLM. Only available when there is existing text.
- **🔄 Refresh**: Erases the current persona and generates a brand new one from scratch. Disabled when the field is empty.

*Note: You must have a model selected and a valid API key configured to use these features.*

## Supported Stages

- Terms of Service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {customElement, property, state} from 'lit/decorators.js';
import 'emoji-picker-element';

import {core} from '../../core/core';
import {AuthService} from '../../services/auth.service';
import {ExperimentEditor} from '../../services/experiment.editor';
import {ExperimentService} from '../../services/experiment.service';

Expand All @@ -32,6 +33,7 @@ import {styles} from './agent_editor.scss';
export class AgentPersonaEditorComponent extends MobxLitElement {
static override styles: CSSResultGroup = [styles];

private readonly authService = core.getService(AuthService);
private readonly experimentEditor = core.getService(ExperimentEditor);
private readonly experimentService = core.getService(ExperimentService);

Expand Down Expand Up @@ -174,6 +176,7 @@ export class AgentPersonaEditorComponent extends MobxLitElement {
<agent-model-selector
.apiType=${agent.defaultModelSettings.apiType}
.modelName=${agent.defaultModelSettings.modelName}
.experimenterData=${this.authService.experimenterData}
?disabled=${!this.experimentEditor.canEditStages}
@model-settings-change=${handleSettingsChange}
></agent-model-selector>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import '../../pair-components/button';
import '../../pair-components/icon_button';
import '../shared/agent_model_selector';
import '../shared/persona_generation_buttons';

import {generatePersonaContextCallable} from '../../shared/callables';

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

import {MobxLitElement} from '@adobe/lit-mobx';
import {CSSResultGroup, html, nothing} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {Ref, createRef, ref} from 'lit/directives/ref.js';

import {core} from '../../core/core';
import {AnalyticsService, ButtonClick} from '../../services/analytics.service';
import {AuthService} from '../../services/auth.service';
import {ExperimentEditor} from '../../services/experiment.editor';
import {ExperimentManager} from '../../services/experiment.manager';
import {FirebaseService} from '../../services/firebase.service';

import {
AgentModelSettings,
AgentPersonaConfig,
CohortConfig,
ModelResponseStatus,
createAgentModelSettings,
DEFAULT_AGENT_PARTICIPANT_ID,
} from '@deliberation-lab/utils';
Expand All @@ -29,11 +36,14 @@ export class AgentParticipantDialog extends MobxLitElement {
static override styles: CSSResultGroup = [styles];

private readonly analyticsService = core.getService(AnalyticsService);
private readonly authService = core.getService(AuthService);
private readonly experimentManager = core.getService(ExperimentManager);
private readonly experimentEditor = core.getService(ExperimentEditor);
private readonly firebaseService = core.getService(FirebaseService);

@property() isLoading = false;
@property() isSuccess = false;
@property() isQuickAdding = false;

@property() cohort: CohortConfig | undefined = undefined;
@property() agentId = '';
Expand All @@ -42,10 +52,18 @@ export class AgentParticipantDialog extends MobxLitElement {
@property({type: Object}) modelSettings: AgentModelSettings =
createAgentModelSettings();

private readonly textFieldRef: Ref<HTMLElement> = createRef();

private close() {
this.dispatchEvent(new CustomEvent('close'));
}

private resetFields() {
this.agentId = '';
this.promptContext = '';
// Note: modelSettings intentionally preserved so Quick add can reuse it.
}

override render() {
if (!this.cohort) {
return nothing;
Expand All @@ -64,19 +82,14 @@ export class AgentParticipantDialog extends MobxLitElement {
</pr-icon-button>
</div>
<div class="body">
${this.isSuccess ? this.renderSuccess() : this.renderEdit()}
${this.isSuccess ? this.renderSuccess() : this.renderEditContent()}
</div>
${this.isSuccess ? nothing : this.renderFooter()}
</div>
`;
}

private resetFields() {
this.agentId = '';
this.promptContext = '';
this.modelSettings = createAgentModelSettings();
}

private renderEdit() {
private renderEditContent() {
const handleSettingsChange = (e: CustomEvent<AgentModelSettings>) => {
this.modelSettings = e.detail;
};
Expand All @@ -85,10 +98,46 @@ export class AgentParticipantDialog extends MobxLitElement {
<agent-model-selector
.apiType=${this.modelSettings.apiType}
.modelName=${this.modelSettings.modelName}
.experimenterData=${this.authService.experimenterData}
@model-settings-change=${handleSettingsChange}
></agent-model-selector>
${this.renderPromptContext()}
<div class="buttons-wrapper">
`;
}

private renderFooter() {
const handlePersonaTextChange = async (
e: CustomEvent<{text: string; mode: string}>,
) => {
const {text, mode} = e.detail;
if (mode === 'generate' || mode === 'refresh') {
// Generate (merge-expand) and Refresh both replace the full field
this.promptContext = text;
} else {
// Enhance appends episodic memories to existing text
const separator = this.promptContext.trim() ? '\n\n' : '';
this.promptContext = this.promptContext + separator + text;

// Scroll to bottom only for Enhance (text is appended)
await this.updateComplete;
const textField = this.textFieldRef.value;
if (textField) {
const textarea = textField.shadowRoot?.querySelector('textarea');
if (textarea) {
textarea.scrollTop = textarea.scrollHeight;
}
}
}
};

return html`
<div class="footer">
<persona-generation-buttons
.currentText=${this.promptContext}
.modelSettings=${this.modelSettings}
?disabled=${this.isLoading}
@persona-text-change=${handlePersonaTextChange}
></persona-generation-buttons>
<pr-button
?disabled=${!this.modelSettings.modelName}
?loading=${this.isLoading}
Expand Down Expand Up @@ -117,18 +166,63 @@ export class AgentParticipantDialog extends MobxLitElement {
`;
}

private async quickAdd() {
if (!this.cohort || !this.modelSettings.modelName) return;
this.isQuickAdding = true;
try {
const creatorId = this.authService.experimenterData?.email ?? '';
const response = await generatePersonaContextCallable(
this.firebaseService.functions,
{
creatorId,
mode: 'generate',
currentText: '',
apiType: this.modelSettings.apiType,
modelName: this.modelSettings.modelName,
},
);
const text =
(response.status === ModelResponseStatus.OK ||
response.status === ModelResponseStatus.LENGTH_ERROR) &&
response.text
? response.text
: '';
this.experimentEditor.addAgentParticipant();
this.experimentManager.createAgentParticipant(this.cohort.id, {
agentId: DEFAULT_AGENT_PARTICIPANT_ID,
promptContext: text,
modelSettings: this.modelSettings,
});
this.analyticsService.trackButtonClick(ButtonClick.AGENT_PARTICIPANT_ADD);
} finally {
this.isQuickAdding = false;
}
}

private renderSuccess() {
return html`
<div>Agent participant added!</div>
<pr-button
color="secondary"
variant="outlined"
@click=${() => {
this.isSuccess = false;
}}
>
Add another agent
</pr-button>
<div class="button-row">
<pr-button
color="secondary"
variant="outlined"
?disabled=${this.isQuickAdding}
@click=${() => {
this.isSuccess = false;
}}
>
Add another agent
</pr-button>
<pr-button
color="primary"
variant="tonal"
?loading=${this.isQuickAdding}
?disabled=${!this.modelSettings.modelName || this.isQuickAdding}
@click=${this.quickAdd}
>
⚡ Quick add
</pr-button>
</div>
`;
}

Expand All @@ -140,9 +234,18 @@ export class AgentParticipantDialog extends MobxLitElement {

return html`
<md-filled-text-field
${ref(this.textFieldRef)}
?disabled=${this.isLoading}
type="textarea"
label="Prompt context for this specific agent participant (optional)"
rows=${Math.min(
Math.max(
3,
(this.promptContext.match(/\n/g) ?? []).length + 1,
Math.ceil(this.promptContext.length / 72),
),
18,
)}
label="Add an optional persona prompt for this specific agent participant (e.g. You are ...)"
.value=${this.promptContext}
@input=${updatePromptContext}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@
justify-content: end;
}

.buttons-wrapper-spaced {
@include common.flex-row-align-center;
justify-content: space-between;
}

/* Success screen button row in agent-participant-configuration-dialog */
.button-row {
@include common.flex-row-align-center;
gap: common.$spacing-medium;
margin-top: common.$spacing-small;
}

/* Used in agent-participant-configuration-dialog */
.agent-persona-wrapper {
@include common.flex-row;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ h4 {
}

.chip-container {
@include common.flex-column;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: common.$spacing-medium;
padding: common.$spacing-large;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export class Preview extends MobxLitElement {
}

return html`
${this.renderChips()} ${this.renderTable()} ${this.renderStats()}
${this.renderChips()} ${this.renderPersonaPrompt()} ${this.renderTable()}
${this.renderStats()}
<div class="divider"></div>
${this.renderStageDatas()}
`;
Expand Down Expand Up @@ -254,6 +255,28 @@ export class Preview extends MobxLitElement {
`;
}

private renderPersonaPrompt() {
const promptContext = this.profile?.agentConfig?.promptContext;
if (!promptContext) return nothing;

return html`
<div style="padding: 0 16px;">
<details open>
<summary
style="cursor:pointer; color: var(--md-sys-color-primary); font-size: 0.9em; font-weight: 500;"
>
Persona prompt (${promptContext.split(' ').length} words)
</summary>
<div
style="white-space: pre-wrap; margin-top: 8px; font-size: 0.85em; line-height: 1.6; padding: 8px; background: var(--md-sys-color-surface-variant); border-radius: 6px;"
>
${promptContext.trim()}
</div>
</details>
</div>
`;
}

private renderStageDatas() {
if (!this.profile) return;

Expand Down
13 changes: 13 additions & 0 deletions frontend/src/components/gallery/home_gallery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
getQuickstartGroupChatTemplate,
} from '../../shared/templates/quickstart_group_chat';
import {getQuickstartPrivateChatTemplate} from '../../shared/templates/quickstart_private_chat';
import {getAgentParticipantsDemoTemplate} from '../../shared/templates/quickstart_agent_participants_demo';

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

Expand Down Expand Up @@ -248,6 +249,18 @@ export class QuickStartGallery extends MobxLitElement {
<pr-icon icon="3p" color="neutral" size="large"></pr-icon>
<div>Private chat with agent</div>
</div>
<div
class="quick-start-card"
@click=${() => {
this.routerService.navigate(Pages.EXPERIMENT_CREATE);
this.experimentEditor.loadTemplate(
getAgentParticipantsDemoTemplate(),
);
}}
>
<pr-icon icon="diversity_3" color="neutral" size="large"></pr-icon>
<div>Group agent participants demo</div>
</div>
<div
class="quick-start-card"
@click=${() => {
Expand Down
Loading
Loading