From 14000a65c53779869435fd7918714ed690e48af7 Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 25 Mar 2026 23:14:37 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(onboarding):=20implement=20onb?= =?UTF-8?q?oarding=20document=20and=20persona=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a new onboarding document structure that separates agent identity and user persona data. Replace existing `readSoulDocument` and `updateSoulDocument` APIs with `readDocument` and `updateDocument` to handle both SOUL.md and user persona documents. Update related services, client executors, and localization keys to reflect these changes. Ensure document updates are driven by the agent, allowing for incremental updates and improved content management. Signed-off-by: Innei --- ...03-25-onboarding-document-persona-split.md | 66 +++++ ...ave-user-question-simplification-design.md | 253 ++++++++++++++++++ locales/zh-CN/plugin.json | 4 +- .../src/systemRole.ts | 11 +- .../src/toolSystemRole.ts | 9 +- .../src/manifest.ts | 26 +- .../builtin-tool-web-onboarding/src/types.ts | 4 +- src/locales/default/plugin.ts | 4 +- src/server/routers/lambda/user.ts | 56 ++-- src/server/services/onboarding/index.ts | 40 +-- .../serverRuntimes/webOnboarding.ts | 63 +++-- src/services/user/index.ts | 8 +- .../builtin/executors/lobe-web-onboarding.ts | 24 +- 13 files changed, 454 insertions(+), 114 deletions(-) create mode 100644 docs/superpowers/specs/2026-03-25-onboarding-document-persona-split.md create mode 100644 docs/superpowers/specs/2026-03-25-onboarding-save-user-question-simplification-design.md diff --git a/docs/superpowers/specs/2026-03-25-onboarding-document-persona-split.md b/docs/superpowers/specs/2026-03-25-onboarding-document-persona-split.md new file mode 100644 index 0000000000..2997035911 --- /dev/null +++ b/docs/superpowers/specs/2026-03-25-onboarding-document-persona-split.md @@ -0,0 +1,66 @@ +# Onboarding Document & Persona Split + +**Date:** 2026-03-25 +**Status:** Approved + +## Problem + +The onboarding flow currently dumps all collected information into SOUL.md — both agent identity and user profile data. User-related information (work style, interests, pain points) belongs in `user_memory_persona_documents`, not in the agent's soul document. Additionally, document updates happen via server-side auto-sync at commit time rather than being driven by the agent, limiting the agent's ability to compose natural, contextual content. + +## Design + +### Content Responsibility Split + +| Document | Storage | Content | +| ---------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------- | +| **SOUL.md** | `agent_documents` (inbox agent) | Agent identity (name, creature, vibe, emoji) + base template (core truths, boundaries, vibe, continuity) | +| **User Persona** | `user_memory_persona_documents` | User identity, work style, current context, interests, pain points | + +### Tool API + +Two new APIs on `lobe-web-onboarding`, replacing the existing `readSoulDocument` / `updateSoulDocument`: + +| API | Parameters | Behavior | +| ---------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | +| `readDocument` | `{ type: 'soul' \| 'persona' }` | `soul` → read inbox agent's SOUL.md via `AgentDocumentsService`; `persona` → read from `UserPersonaModel` | +| `updateDocument` | `{ type: 'soul' \| 'persona', content: string }` | `soul` → upsert inbox agent's SOUL.md; `persona` → upsert `user_memory_persona_documents` | + +Routing by `type` is handled inside the tool implementation (server runtime + client executor). + +### Agent-Driven Updates (Remove Server-Side Auto-Sync) + +Remove the automatic `upsertInboxDocuments` call from `OnboardingService.commitActiveStep`. Document and persona updates are fully driven by the agent via `readDocument` / `updateDocument` tool calls, guided by the system prompt. + +### System Prompt Constraints + +The onboarding agent's system prompt and tool system prompt are updated with: + +``` +Document management: +- After each profile node commit, call readDocument + updateDocument to persist changes. +- SOUL.md (type: "soul"): only agent identity (name, creature, vibe, emoji) + base template. No user info. +- User Persona (type: "persona"): user identity, work style, current context, interests, pain points. +- Both documents are mutable — read first, merge new info, write full updated content. Do not blindly append. +- Do not put user information into SOUL.md. Do not put agent identity into persona. +``` + +### Incremental Updates + +The agent updates documents after each node commit, not in a batch at the end. Both SOUL.md and user persona are treated as mutable documents — the agent reads the current content, merges new information, and writes the full updated content. No append-only behavior. + +## File Changes + +| File | Action | +| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `packages/builtin-tool-web-onboarding/src/types.ts` | Replace `readSoulDocument`/`updateSoulDocument` with `readDocument`/`updateDocument` | +| `packages/builtin-tool-web-onboarding/src/manifest.ts` | Replace two API manifests, add `type` parameter | +| `src/server/services/toolExecution/serverRuntimes/webOnboarding.ts` | Replace implementation, route by `type` | +| `src/store/tool/slices/builtin/executors/lobe-web-onboarding.ts` | Replace client executor methods | +| `src/services/user/index.ts` | Replace service methods | +| `src/server/routers/lambda/user.ts` | Replace TRPC routes | +| `packages/builtin-agent-onboarding/src/systemRole.ts` | Rewrite document management section | +| `packages/builtin-agent-onboarding/src/toolSystemRole.ts` | Rewrite document management section | +| `src/server/services/onboarding/index.ts` | Remove `upsertInboxDocuments` call from `commitActiveStep` | +| `src/server/services/onboarding/documentHelpers.ts` | Remove `buildSoulDocument` user-info sections (may deprecate file) | +| `src/locales/default/plugin.ts` | Replace i18n keys | +| `locales/zh-CN/plugin.json` | Replace i18n keys | diff --git a/docs/superpowers/specs/2026-03-25-onboarding-save-user-question-simplification-design.md b/docs/superpowers/specs/2026-03-25-onboarding-save-user-question-simplification-design.md new file mode 100644 index 0000000000..53f39bf40f --- /dev/null +++ b/docs/superpowers/specs/2026-03-25-onboarding-save-user-question-simplification-design.md @@ -0,0 +1,253 @@ +# Onboarding `saveUserQuestion` Simplification + +**Date:** 2026-03-25 +**Status:** Approved + +## Problem + +The current onboarding implementation still carries a strong step-machine model: + +- `saveAnswer` is operationally constrained by `activeNode` +- batch updates are nominally supported but practically restricted by node-gating +- AI-facing tool results are delivered primarily as JSON payloads +- document updates and structured onboarding state overlap in responsibility + +This creates unnecessary complexity in prompt design, runtime behavior, and test surface. The intended direction is to weaken the onboarding state model substantially and move long-lived identity/persona content to markdown documents handled explicitly by document tools. + +## Design Goals + +| Goal | Outcome | +| ------------------------------------------------------- | ------------------------------------------------------------------------------ | +| Remove strong node progression semantics | No mandatory `activeNode` gating for `saveUserQuestion` | +| Preserve legacy structured fields still used elsewhere | Keep only `fullName`, `interests`, and `responseLanguage` as structured writes | +| Make markdown documents the canonical long-lived memory | Agent identity and persona content are maintained through document tools only | +| Improve AI ergonomics | Tool `content` should be plain-language message text, not JSON-first | +| Reduce code volume | Delete draft/step-completion logic where it only serves the old state machine | + +## Scope Boundary + +This specification builds on the already approved document split work: + +- `SOUL.md` remains the canonical agent identity document +- `user_persona` remains the canonical user narrative document +- `readDocument` / `updateDocument` already exist and are not redesigned here + +The scope of this document is limited to simplifying onboarding state, tool contracts, and prompt/runtime behavior around `saveUserQuestion`. + +## Recommended Architecture + +### Source of Truth Split + +| Data Category | Canonical Storage | Writer | +| ---------------------- | ------------------------------------- | --------------------------------- | +| Agent identity | `SOUL.md` | `readDocument` / `updateDocument` | +| User persona narrative | `user_persona` document | `readDocument` / `updateDocument` | +| `fullName` | user structured state | `saveUserQuestion` | +| `interests` | user structured state | `saveUserQuestion` | +| `responseLanguage` | user settings | `saveUserQuestion` | +| onboarding completion | `finishedAt` and topic transfer state | `finishOnboarding` | + +### High-Level Flow + +```text +User message + | + v +Agent calls getOnboardingState + | + v +Agent reads SOUL.md / user_persona as needed + | + v +Agent decides what information was learned + | + +-> saveUserQuestion(updates[]) + | -> persist only fullName / interests / responseLanguage + | + +-> updateDocument(...) + -> persist SOUL.md / user_persona changes +``` + +- `saveUserQuestion` no longer owns markdown writeback. +- Document tools become the only writers of onboarding markdown content. +- The onboarding service provides only minimal orchestration and completion state. + +## Tool Surface + +| Tool | Keep | Responsibility | +| --------------------- | ---- | ---------------------------------------------------------------------------------------------- | +| `getOnboardingState` | Yes | Return message-oriented onboarding summary plus minimal machine state | +| `saveUserQuestion` | Yes | Accept a simplified batch payload and persist only `fullName`, `interests`, `responseLanguage` | +| `readDocument` | Yes | Read `SOUL.md` / `user_persona` | +| `updateDocument` | Yes | Update `SOUL.md` / `user_persona` | +| `completeCurrentStep` | No | Remove; no longer needed without strong node progression | +| `returnToOnboarding` | No | Remove or reduce to pure prompt behavior | +| `finishOnboarding` | Yes | Finalize onboarding and transfer topic | + +### API Naming Decision + +| Current Name | New Name | Decision | +| ------------ | ------------------ | -------------------------------------------------------------------------- | +| `saveAnswer` | `saveUserQuestion` | Rename across manifest, types, server runtime, client executor, and router | + +The new name is normative. This specification does not preserve `saveAnswer` as the long-term public tool name. + +## `saveUserQuestion` Semantics + +### Input Model + +`saveUserQuestion` should replace the old node-scoped patch model with one flat payload: + +```ts +interface SaveUserQuestionInput { + fullName?: string; + interests?: string[]; + responseLanguage?: string; +} +``` + +This flat payload is sufficient because only three structured fields remain in scope. “Batch update” now means submitting any combination of these fields in one tool call. + +### Persistence Rules + +| Input Content | Behavior | +| ----------------------------- | ------------------------------------------------------------------------------------------------------- | +| `fullName` | Persist to user profile | +| `interests` | Persist to user profile | +| `responseLanguage` | Persist to user settings | +| unsupported onboarding fields | Do not persist structurally; these belong in markdown documents and should be handled by document tools | + +### No-Op and Partial-Success Rules + +| Case | Result | +| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| At least one supported field is present and valid | `success: true`; persist supported fields | +| Unsupported fields are included alongside supported fields | `success: true`; supported fields are saved and unsupported fields are reported in `ignoredFields` | +| No supported fields are present | `success: false`; return a plain-language message explaining that no structured fields were eligible | +| Supported field value is unchanged | `success: true`; report as unchanged or omit from `savedFields` | + +### Output Model + +Tool `content` must be a natural-language message. Example shape: + +| Field | Purpose | +| --------------------- | ----------------------------------------------------------------------- | +| `content` | Plain-language summary such as “Saved interests and response language.” | +| `state.savedFields` | Minimal machine-readable list of saved structured fields | +| `state.ignoredFields` | Optional list of fields intentionally not persisted structurally | + +JSON should not be the primary AI-visible representation. + +## Onboarding State Simplification + +### Fields to Remove or Weaken + +| Field / Concept | Action | Reason | +| --------------------------------- | ---------------------------- | ----------------------------------------------------------- | +| `completedNodes` | Remove or derive temporarily | Strong step progression is no longer canonical | +| `draft` | Remove | Draft persistence exists only for the old gated flow | +| `activeNode` persistence | Remove | If needed, compute advisory next-question hints dynamically | +| `control.allowedTools` | Remove or drastically reduce | Tool eligibility should not depend on strict step state | +| “complete current step” semantics | Remove | The agent now writes data and documents directly | + +### Fields to Keep + +| Field | Reason | +| --------------- | ---------------------------------------- | +| `activeTopicId` | Maintain onboarding conversation linkage | +| `finishedAt` | Completion gate and re-entry behavior | +| `version` | Migration compatibility | + +The onboarding service may still derive an advisory “next useful question,” but that hint must not restore the old persisted step-machine model. + +## `getOnboardingState` Contract + +`getOnboardingState` should become advisory and message-oriented. + +| Output | Description | +| ------------------------------- | ------------------------------------------------------------------------------- | +| `content` | Plain-language summary of what is already known and what is still useful to ask | +| `state.finished` | Completion flag | +| `state.topicId` | Current onboarding topic | +| `state.missingStructuredFields` | Subset of `fullName`, `interests`, `responseLanguage` still absent | + +It should not expose the old step-machine JSON context as the primary output. + +## Prompt Changes + +### System Prompt + +The onboarding agent prompt should be rewritten to reflect: + +- no single-node gate around `saveUserQuestion` +- batch updates are allowed +- markdown updates must be done explicitly through document tools +- structured persistence is limited to `fullName`, `interests`, and `responseLanguage` +- AI should consume message-oriented tool output rather than JSON dumps + +### Tool System Prompt + +The tool prompt should: + +- remove statements such as “activeNode is the only step you may act on” +- remove step-completion instructions tied to `completeCurrentStep` +- describe `saveUserQuestion` as a thin structured persistence tool +- describe document tools as the only markdown persistence path + +## UI and Store Consumer Migration + +The codebase still contains consumers that derive onboarding progress from `completedNodes` and `activeNode`. These must be updated as part of the same implementation plan. + +| Consumer Area | Required Change | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------- | +| `src/store/user/slices/agentOnboarding/selectors.ts` | Remove `activeNode` derivation from `completedNodes` | +| `src/features/Onboarding/Agent/context.ts` | Stop reconstructing flow state from persisted node progression | +| `src/features/Onboarding/Agent/index.tsx` | Ensure bootstrap logic tolerates minimal onboarding state | +| onboarding-related tests | Replace step-progression assertions with minimal-state and message-oriented assertions | + +## Migration Scope + +| File Area | Change | +| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `packages/types/src/user/agentOnboarding.ts` | Remove strong state-machine structures; keep minimal metadata and updated tool payload types | +| `packages/builtin-tool-web-onboarding/src/manifest.ts` | Simplify APIs, schema descriptions, and tool text | +| `packages/builtin-tool-web-onboarding/src/types.ts` | Update API enum and remove obsolete names | +| `src/server/services/onboarding/index.ts` | Replace node-progression logic with minimal persistence + completion service | +| `src/server/services/toolExecution/serverRuntimes/webOnboarding.ts` | Return message-first tool content | +| `src/services/user/index.ts` | Update client contract | +| `src/store/tool/slices/builtin/executors/lobe-web-onboarding.ts` | Return message-first content; remove JSON-first behavior | +| `src/server/routers/lambda/user.ts` | Simplify mutation/query schema | +| `src/store/user/slices/agentOnboarding/selectors.ts` | Remove selector assumptions tied to `completedNodes` | +| `src/features/Onboarding/Agent/context.ts` | Simplify onboarding bootstrap context | +| `src/features/Onboarding/Agent/index.tsx` | Remove hard dependencies on strong onboarding state | +| `packages/builtin-agent-onboarding/src/systemRole.ts` | Rewrite behavioral instructions | +| `packages/builtin-agent-onboarding/src/toolSystemRole.ts` | Rewrite tool-facing instructions | + +## Verification Strategy + +| Area | Verification | +| ------------------------ | ----------------------------------------------------------------- | +| Batch structured updates | `saveUserQuestion` persists multiple supported fields in one call | +| Field filtering | Unsupported fields are ignored structurally and reported clearly | +| Document responsibility | Only document tools update markdown content | +| AI-visible output | `content` is plain-language text, not JSON dumps | +| Completion | `finishOnboarding` still marks completion and transfers the topic | + +## Risks + +| Risk | Mitigation | +| ------------------------------------- | ------------------------------------------------------------------------------------- | +| Loss of deterministic next-step flow | Accept weaker orchestration and let prompts infer the next useful question | +| UI assumptions about node progress | Audit onboarding UI and convert any hard dependency into derived or optional behavior | +| Hidden dependencies on removed fields | Retain the minimal structured set required by legacy onboarding consumers | + +## Decision Summary + +| Decision | Result | +| ---------------------- | ----------------------------------------------------- | +| State model | Weaken substantially | +| Markdown writes | Document tools only | +| Structured persistence | Keep only `fullName`, `interests`, `responseLanguage` | +| Batch updates | Supported | +| AI-facing output | Message-first, not JSON-first | +| Old step-machine APIs | Remove where no longer necessary | diff --git a/locales/zh-CN/plugin.json b/locales/zh-CN/plugin.json index 956bb7c0cf..ee469e5c6b 100644 --- a/locales/zh-CN/plugin.json +++ b/locales/zh-CN/plugin.json @@ -239,10 +239,10 @@ "builtins.lobe-web-onboarding.apiName.completeCurrentStep": "完成当前步骤", "builtins.lobe-web-onboarding.apiName.finishOnboarding": "完成引导", "builtins.lobe-web-onboarding.apiName.getOnboardingState": "读取引导状态", - "builtins.lobe-web-onboarding.apiName.readSoulDocument": "读取 SOUL.md", + "builtins.lobe-web-onboarding.apiName.readDocument": "读取文档", "builtins.lobe-web-onboarding.apiName.returnToOnboarding": "回到引导流程", "builtins.lobe-web-onboarding.apiName.saveAnswer": "保存用户答案", - "builtins.lobe-web-onboarding.apiName.updateSoulDocument": "更新 SOUL.md", + "builtins.lobe-web-onboarding.apiName.updateDocument": "更新文档", "builtins.lobe-web-onboarding.title": "用户引导", "confirm": "确认", "debug.arguments": "调用参数", diff --git a/packages/builtin-agent-onboarding/src/systemRole.ts b/packages/builtin-agent-onboarding/src/systemRole.ts index 4102bbc1f3..edce249f4e 100644 --- a/packages/builtin-agent-onboarding/src/systemRole.ts +++ b/packages/builtin-agent-onboarding/src/systemRole.ts @@ -33,11 +33,12 @@ Questioning: - Prefer one actionable question over a questionnaire. - Keep visible choices natural and executable. -SOUL.md management: -- After completing each profile node (agentIdentity, userIdentity, workStyle, workContext, painPoints), read the current SOUL.md with readSoulDocument and update it with updateSoulDocument to reflect the newly collected information. -- Build on the existing SOUL.md content. Do not overwrite the base template — append or update the relevant section. -- The SOUL.md sections are: Identity Core, About My Human, How We Work Together, Current Context, Where I Can Help Most. -- Keep SOUL.md content concise and natural — describe the user like a person, not a form. +Document management: +- After each profile node commit, call readDocument + updateDocument to persist changes incrementally. +- SOUL.md (type: "soul"): only agent identity (name, creature, vibe, emoji) + base template. No user information. +- User Persona (type: "persona"): user identity, work style, current context, interests, pain points. No agent identity. +- Both documents are mutable — read first, merge new info, write full updated content. Do not blindly append. +- Do not put user information into SOUL.md. Do not put agent identity into persona. Boundaries: - Do not browse, research, or solve unrelated tasks during onboarding. diff --git a/packages/builtin-agent-onboarding/src/toolSystemRole.ts b/packages/builtin-agent-onboarding/src/toolSystemRole.ts index 2b25b5a4d2..bbd3fbf7f7 100644 --- a/packages/builtin-agent-onboarding/src/toolSystemRole.ts +++ b/packages/builtin-agent-onboarding/src/toolSystemRole.ts @@ -27,10 +27,11 @@ Question surfaces: - Keep choices actionable. - Prefer natural reply options. -SOUL.md management: -- After committing a profile node (agentIdentity, userIdentity, workStyle, workContext, painPoints), call readSoulDocument to fetch the current SOUL.md. -- Merge the new information into the appropriate section and call updateSoulDocument with the full updated content. -- Preserve the base template (core truths, boundaries, vibe) — only append or update the profile sections. +Document management: +- After committing a profile node, call readDocument to fetch the relevant document, then updateDocument with full updated content. +- For agentIdentity: update SOUL.md (type: "soul") with agent name, creature, vibe, emoji. Preserve the base template. +- For userIdentity, workStyle, workContext, painPoints: update User Persona (type: "persona") with the new information. +- Both documents are mutable. Read first, merge, write full content. Do not blindly append. Summary: - Summarize the user like a person. diff --git a/packages/builtin-tool-web-onboarding/src/manifest.ts b/packages/builtin-tool-web-onboarding/src/manifest.ts index 48960c3106..635ba8f717 100644 --- a/packages/builtin-tool-web-onboarding/src/manifest.ts +++ b/packages/builtin-tool-web-onboarding/src/manifest.ts @@ -96,25 +96,37 @@ export const WebOnboardingManifest: BuiltinToolManifest = { }, { description: - 'Read the current SOUL.md document from the inbox agent. Use this to understand the existing soul profile before making updates.', - name: WebOnboardingApiName.readSoulDocument, + 'Read a document by type. Use "soul" to read SOUL.md (agent identity + base template), or "persona" to read the user persona document (user identity, work style, context, pain points).', + name: WebOnboardingApiName.readDocument, parameters: { - properties: {}, + properties: { + type: { + description: 'Document type to read.', + enum: ['soul', 'persona'], + type: 'string', + }, + }, + required: ['type'], type: 'object', }, }, { description: - 'Update the SOUL.md document on the inbox agent with new content. The content should be the full updated SOUL.md markdown.', - name: WebOnboardingApiName.updateSoulDocument, + 'Update a document by type with full content. Use "soul" for SOUL.md (agent identity + base template only, no user info), or "persona" for user persona (user identity, work style, context, pain points only, no agent info).', + name: WebOnboardingApiName.updateDocument, parameters: { properties: { content: { - description: 'The full updated SOUL.md content in markdown format.', + description: 'The full updated document content in markdown format.', + type: 'string', + }, + type: { + description: 'Document type to update.', + enum: ['soul', 'persona'], type: 'string', }, }, - required: ['content'], + required: ['type', 'content'], type: 'object', }, }, diff --git a/packages/builtin-tool-web-onboarding/src/types.ts b/packages/builtin-tool-web-onboarding/src/types.ts index 2e23d1b23c..8dc8c43294 100644 --- a/packages/builtin-tool-web-onboarding/src/types.ts +++ b/packages/builtin-tool-web-onboarding/src/types.ts @@ -4,8 +4,8 @@ export const WebOnboardingApiName = { completeCurrentStep: 'completeCurrentStep', finishOnboarding: 'finishOnboarding', getOnboardingState: 'getOnboardingState', - readSoulDocument: 'readSoulDocument', + readDocument: 'readDocument', returnToOnboarding: 'returnToOnboarding', saveAnswer: 'saveAnswer', - updateSoulDocument: 'updateSoulDocument', + updateDocument: 'updateDocument', } as const; diff --git a/src/locales/default/plugin.ts b/src/locales/default/plugin.ts index dc146fac29..d9a99de8f1 100644 --- a/src/locales/default/plugin.ts +++ b/src/locales/default/plugin.ts @@ -241,10 +241,10 @@ export default { 'builtins.lobe-web-onboarding.apiName.completeCurrentStep': 'Complete current step', 'builtins.lobe-web-onboarding.apiName.finishOnboarding': 'Finish onboarding', 'builtins.lobe-web-onboarding.apiName.getOnboardingState': 'Read onboarding state', - 'builtins.lobe-web-onboarding.apiName.readSoulDocument': 'Read SOUL.md', + 'builtins.lobe-web-onboarding.apiName.readDocument': 'Read document', 'builtins.lobe-web-onboarding.apiName.returnToOnboarding': 'Return to onboarding', 'builtins.lobe-web-onboarding.apiName.saveAnswer': 'Save user answer', - 'builtins.lobe-web-onboarding.apiName.updateSoulDocument': 'Update SOUL.md', + 'builtins.lobe-web-onboarding.apiName.updateDocument': 'Update document', 'builtins.lobe-web-onboarding.title': 'User Onboarding', 'builtins.lobe-topic-reference.apiName.getTopicContext': 'Get Topic Context', 'builtins.lobe-topic-reference.title': 'Topic Reference', diff --git a/src/server/routers/lambda/user.ts b/src/server/routers/lambda/user.ts index 079b985574..8fed43c80d 100644 --- a/src/server/routers/lambda/user.ts +++ b/src/server/routers/lambda/user.ts @@ -250,28 +250,50 @@ export const userRouter = router({ return onboardingService.finishOnboarding(); }), - readSoulDocument: userProcedure.query(async ({ ctx }) => { - const onboardingService = new OnboardingService(ctx.serverDB, ctx.userId); - const docService = new AgentDocumentsService(ctx.serverDB, ctx.userId); - const inboxAgentId = await onboardingService.getInboxAgentId(); - const doc = await docService.getDocumentByFilename(inboxAgentId, 'SOUL.md'); + readOnboardingDocument: userProcedure + .input(z.object({ type: z.enum(['soul', 'persona']) })) + .query(async ({ ctx, input }) => { + if (input.type === 'soul') { + const onboardingService = new OnboardingService(ctx.serverDB, ctx.userId); + const docService = new AgentDocumentsService(ctx.serverDB, ctx.userId); + const inboxAgentId = await onboardingService.getInboxAgentId(); + const doc = await docService.getDocumentByFilename(inboxAgentId, 'SOUL.md'); - return { content: doc?.content || '', id: doc?.id }; - }), + return { content: doc?.content || '', id: doc?.id, type: 'soul' as const }; + } - updateSoulDocument: userProcedure - .input(z.object({ content: z.string() })) + const { UserPersonaModel } = await import('@/database/models/userMemory/persona'); + const personaModel = new UserPersonaModel(ctx.serverDB, ctx.userId); + const persona = await personaModel.getLatestPersonaDocument(); + + return { content: persona?.persona || '', id: persona?.id, type: 'persona' as const }; + }), + + updateOnboardingDocument: userProcedure + .input(z.object({ content: z.string(), type: z.enum(['soul', 'persona']) })) .mutation(async ({ ctx, input }) => { - const onboardingService = new OnboardingService(ctx.serverDB, ctx.userId); - const docService = new AgentDocumentsService(ctx.serverDB, ctx.userId); - const inboxAgentId = await onboardingService.getInboxAgentId(); - const doc = await docService.upsertDocumentByFilename({ - agentId: inboxAgentId, - content: input.content, - filename: 'SOUL.md', + if (input.type === 'soul') { + const onboardingService = new OnboardingService(ctx.serverDB, ctx.userId); + const docService = new AgentDocumentsService(ctx.serverDB, ctx.userId); + const inboxAgentId = await onboardingService.getInboxAgentId(); + const doc = await docService.upsertDocumentByFilename({ + agentId: inboxAgentId, + content: input.content, + filename: 'SOUL.md', + }); + + return { id: doc?.id, type: 'soul' as const }; + } + + const { UserPersonaModel } = await import('@/database/models/userMemory/persona'); + const personaModel = new UserPersonaModel(ctx.serverDB, ctx.userId); + const result = await personaModel.upsertPersona({ + editedBy: 'agent_tool', + persona: input.content, + profile: 'default', }); - return { id: doc?.id }; + return { id: result.document.id, type: 'persona' as const }; }), resetAgentOnboarding: userProcedure.mutation(async ({ ctx }) => { diff --git a/src/server/services/onboarding/index.ts b/src/server/services/onboarding/index.ts index bfbc8fdc26..d3141de205 100644 --- a/src/server/services/onboarding/index.ts +++ b/src/server/services/onboarding/index.ts @@ -24,8 +24,7 @@ import { AgentService } from '@/server/services/agent'; import { AgentDocumentsService } from '@/server/services/agentDocuments'; import { translation } from '@/server/translation'; -import { buildIdentityDocument, buildSoulDocument } from './documentHelpers'; -import { NODE_HANDLERS, PROFILE_DOCUMENT_NODES } from './nodeHandlers'; +import { NODE_HANDLERS } from './nodeHandlers'; import { getNodeDraftState } from './nodeSchema'; type OnboardingPatchInput = Record; @@ -206,35 +205,6 @@ export class OnboardingService { this.inboxDocumentsInitialized = true; }; - private upsertInboxDocuments = async ( - state: UserAgentOnboarding, - writeIdentity: boolean, - ): Promise => { - const inboxAgentId = await this.getInboxAgentId(); - - await this.ensureInboxDocuments(inboxAgentId); - - const upserts = [ - this.agentDocumentsService.upsertDocument({ - agentId: inboxAgentId, - content: buildSoulDocument(state), - filename: 'SOUL.md', - }), - ]; - - if (writeIdentity && state.agentIdentity) { - upserts.push( - this.agentDocumentsService.upsertDocument({ - agentId: inboxAgentId, - content: buildIdentityDocument(state.agentIdentity), - filename: 'IDENTITY.md', - }), - ); - } - - await Promise.all(upserts); - }; - private transferToInbox = async (topicId: string): Promise => { const inboxAgentId = await this.getInboxAgentId(); const topic = await this.topicModel.findById(topicId); @@ -682,14 +652,6 @@ export class OnboardingService { draft: nextDraft, }); - if (PROFILE_DOCUMENT_NODES.has(activeNode)) { - try { - await this.upsertInboxDocuments(state, activeNode === 'agentIdentity'); - } catch (error) { - console.error('[OnboardingService] Failed to upsert inbox documents:', error); - } - } - const nextContext = await this.getState(); return { diff --git a/src/server/services/toolExecution/serverRuntimes/webOnboarding.ts b/src/server/services/toolExecution/serverRuntimes/webOnboarding.ts index aec2f2b496..25afe71a3d 100644 --- a/src/server/services/toolExecution/serverRuntimes/webOnboarding.ts +++ b/src/server/services/toolExecution/serverRuntimes/webOnboarding.ts @@ -3,6 +3,7 @@ import { WebOnboardingManifest, } from '@lobechat/builtin-tool-web-onboarding'; +import { UserPersonaModel } from '@/database/models/userMemory/persona'; import { AgentDocumentsService } from '@/server/services/agentDocuments'; import { OnboardingService } from '@/server/services/onboarding'; import { createWebOnboardingToolResult } from '@/utils/webOnboardingToolResult'; @@ -17,6 +18,7 @@ export const webOnboardingRuntime: ServerRuntimeRegistration = { const service = new OnboardingService(context.serverDB, context.userId); const docService = new AgentDocumentsService(context.serverDB, context.userId); + const personaModel = new UserPersonaModel(context.serverDB, context.userId); const proxy: Record) => Promise> = {}; for (const api of WebOnboardingManifest.api) { @@ -47,35 +49,60 @@ export const webOnboardingRuntime: ServerRuntimeRegistration = { return createWebOnboardingToolResult(result); } - case 'readSoulDocument': { - const inboxAgentId = await service.getInboxAgentId(); - const doc = await docService.getDocumentByFilename(inboxAgentId, 'SOUL.md'); + case 'readDocument': { + const docType = args.type as 'soul' | 'persona'; - if (!doc) { - return { content: 'SOUL.md not found.', success: false }; + if (docType === 'soul') { + const inboxAgentId = await service.getInboxAgentId(); + const doc = await docService.getDocumentByFilename(inboxAgentId, 'SOUL.md'); + + if (!doc) return { content: 'SOUL.md not found.', success: false }; + + return { + content: doc.content || '', + state: { content: doc.content, id: doc.id, type: 'soul' }, + success: true, + }; } + const persona = await personaModel.getLatestPersonaDocument(); + return { - content: doc.content || '', - state: { content: doc.content, filename: 'SOUL.md', id: doc.id }, + content: persona?.persona || '', + state: { content: persona?.persona, id: persona?.id, type: 'persona' }, success: true, }; } - case 'updateSoulDocument': { - const inboxAgentId = await service.getInboxAgentId(); - const doc = await docService.upsertDocumentByFilename({ - agentId: inboxAgentId, - content: args.content as string, - filename: 'SOUL.md', - }); + case 'updateDocument': { + const updateType = args.type as 'soul' | 'persona'; + const content = args.content as string; - if (!doc) { - return { content: 'Failed to update SOUL.md.', success: false }; + if (updateType === 'soul') { + const inboxAgentId = await service.getInboxAgentId(); + const doc = await docService.upsertDocumentByFilename({ + agentId: inboxAgentId, + content, + filename: 'SOUL.md', + }); + + if (!doc) return { content: 'Failed to update SOUL.md.', success: false }; + + return { + content: `Updated SOUL.md (${doc.id}).`, + state: { id: doc.id, type: 'soul' }, + success: true, + }; } + const result = await personaModel.upsertPersona({ + editedBy: 'agent_tool', + persona: content, + profile: 'default', + }); + return { - content: `Updated SOUL.md (${doc.id}).`, - state: { filename: 'SOUL.md', id: doc.id }, + content: `Updated user persona (${result.document.id}).`, + state: { id: result.document.id, type: 'persona' }, success: true, }; } diff --git a/src/services/user/index.ts b/src/services/user/index.ts index 008a0c9ab8..f6a8bc4b8d 100644 --- a/src/services/user/index.ts +++ b/src/services/user/index.ts @@ -62,12 +62,12 @@ export class UserService { return lambdaClient.user.finishOnboarding.mutate({}); }; - readSoulDocument = async () => { - return lambdaClient.user.readSoulDocument.query(); + readOnboardingDocument = async (type: 'soul' | 'persona') => { + return lambdaClient.user.readOnboardingDocument.query({ type }); }; - updateSoulDocument = async (content: string) => { - return lambdaClient.user.updateSoulDocument.mutate({ content }); + updateOnboardingDocument = async (type: 'soul' | 'persona', content: string) => { + return lambdaClient.user.updateOnboardingDocument.mutate({ content, type }); }; makeUserOnboarded = async () => { diff --git a/src/store/tool/slices/builtin/executors/lobe-web-onboarding.ts b/src/store/tool/slices/builtin/executors/lobe-web-onboarding.ts index 90112cc192..7c78e3a866 100644 --- a/src/store/tool/slices/builtin/executors/lobe-web-onboarding.ts +++ b/src/store/tool/slices/builtin/executors/lobe-web-onboarding.ts @@ -73,33 +73,29 @@ class WebOnboardingExecutor extends BaseExecutor { return createWebOnboardingToolResult(result); }; - readSoulDocument = async (): Promise => { - const result = await userService.readSoulDocument(); - - if (!result.content) { - return { content: 'SOUL.md not found.', success: false }; - } + readDocument = async (params: { type: 'soul' | 'persona' }): Promise => { + const result = await userService.readOnboardingDocument(params.type); return { - content: result.content, - state: { content: result.content, filename: 'SOUL.md', id: result.id }, + content: result.content || '', + state: { content: result.content, id: result.id, type: params.type }, success: true, }; }; - updateSoulDocument = async ( - params: { content: string }, + updateDocument = async ( + params: { content: string; type: 'soul' | 'persona' }, _ctx: BuiltinToolContext, ): Promise => { - const result = await userService.updateSoulDocument(params.content); + const result = await userService.updateOnboardingDocument(params.type, params.content); if (!result.id) { - return { content: 'Failed to update SOUL.md.', success: false }; + return { content: `Failed to update ${params.type} document.`, success: false }; } return { - content: `Updated SOUL.md (${result.id}).`, - state: { filename: 'SOUL.md', id: result.id }, + content: `Updated ${params.type} document (${result.id}).`, + state: { id: result.id, type: params.type }, success: true, }; };