Просмотр исходного кода

Enhance editor assistant: add context instructions and improve request body schema

Yuki Takei 9 месяцев назад
Родитель
Сommit
3fcc6a2473

+ 92 - 82
apps/app/src/features/openai/server/routes/edit/index.ts

@@ -43,7 +43,10 @@ const LlmEditorAssistantResponseSchema = z.object({
 type ReqBody = {
   userMessage: string,
   pageBody: string,
+  isPageBodyPartial?: boolean, // Whether the page body is a partial content
+  partialPageBodyStartIndex?: number, // 0-based index for the start of the partial page body
   selectedText?: string,
+  selectedPosition?: number,
   threadId?: string,
 }
 
@@ -70,77 +73,75 @@ const withMarkdownCaution = `# IMPORTANT:
 `;
 
 function instruction(withMarkdown: boolean): string {
-  return `
-  # USER INTENT DETECTION:
-  First, analyze the user's message to determine their intent:
-  - **Consultation Type**: Questions, discussions, explanations, or advice seeking WITHOUT explicit request to edit/modify/generate text
-  - **Edit Type**: Clear requests to edit, modify, fix, generate, create, or write content
-
-  ## EXAMPLES OF USER INTENT:
-  ### Consultation Type Examples:
-  - "What do you think about this code?"
-  - "Please give me advice on this text structure"
-  - "Why is this error occurring?"
-  - "Is there a better approach?"
-  - "Can you explain how this works?"
-  - "What are the pros and cons of this method?"
-  - "How should I organize this document?"
-
-  ### Edit Type Examples:
-  - "Please fix the following"
-  - "Add a function that..."
-  - "Rewrite this section to..."
-  - "Correct the errors in this code"
-  - "Generate a new paragraph about..."
-  - "Modify this to include..."
-  - "Create a template for..."
-
-  # RESPONSE FORMAT:
-  ## For Consultation Type (discussion/advice only):
-  Respond with a JSON object containing ONLY message objects:
-  {
-    "contents": [
-      { "message": "Your thoughtful response to the user's question or consultation.\n\nYou can use multiple paragraphs as needed." }
-    ]
-  }
-
-  ## For Edit Type (explicit editing request):
-  The SEARCH field must contain exact content including whitespace and indentation.
-  The startLine field is REQUIRED and must specify the line number where search begins.
-
-  Respond with a JSON object in the following format:
-  {
-    "contents": [
-      { "message": "Your brief message about the upcoming changes or proposals.\n\n" },
-      {
-        "search": "exact existing content",
-        "replace": "new content",
-        "startLine": 42  // REQUIRED: line number (1-based) where search begins
-      },
-      { "message": "Additional explanation if needed." },
-      {
-        "search": "another exact content",
-        "replace": "replacement content",
-        "startLine": 58  // REQUIRED
-      },
-      ...more items if needed
-      { "message": "Your friendly message explaining what changes were made or suggested." }
-    ]
-  }
-
-  The array should contain:
-  - [At the beginning of the list] A "message" object that has your brief message about the upcoming change or proposal. Be sure that should be written in the present or future tense and add two consecutive line feeds ('\n\n') at the end.
-  - Objects with a "message" key for explanatory text to the user if needed.
-  - Edit objects with "search" (exact existing content), "replace" (new content), and "startLine" (1-based line number, REQUIRED) fields.
-  - [At the end of the list] A "message" object that contains your friendly message explaining that the operation was completed and what changes were made.
-
-  ${withMarkdown ? withMarkdownCaution : ''}
-
-  # Multilingual Support:
-  Always provide messages in the same language as the user's request.`;
+  return `# RESPONSE FORMAT:
+
+## For Consultation Type (discussion/advice only):
+Respond with a JSON object containing ONLY message objects:
+{
+  "contents": [
+    { "message": "Your thoughtful response to the user's question or consultation.\n\nYou can use multiple paragraphs as needed." }
+  ]
+}
+
+## For Edit Type (explicit editing request):
+The SEARCH field must contain exact content including whitespace and indentation.
+The startLine field is REQUIRED and must specify the line number where search begins.
+
+Respond with a JSON object in the following format:
+{
+  "contents": [
+    { "message": "Your brief message about the upcoming changes or proposals.\n\n" },
+    {
+      "search": "exact existing content",
+      "replace": "new content",
+      "startLine": 42  // REQUIRED: line number (1-based) where search begins
+    },
+    { "message": "Additional explanation if needed." },
+    {
+      "search": "another exact content",
+      "replace": "replacement content",
+      "startLine": 58  // REQUIRED
+    },
+    ...more items if needed
+    { "message": "Your friendly message explaining what changes were made or suggested." }
+  ]
+}
+
+The array should contain:
+- [At the beginning of the list] A "message" object that has your brief message about the upcoming change or proposal. Be sure that should be written in the present or future tense and add two consecutive line feeds ('\n\n') at the end.
+- Objects with a "message" key for explanatory text to the user if needed.
+- Edit objects with "search" (exact existing content), "replace" (new content), and "startLine" (1-based line number, REQUIRED) fields.
+- [At the end of the list] A "message" object that contains your friendly message explaining that the operation was completed and what changes were made.
+
+${withMarkdown ? withMarkdownCaution : ''}`;
 }
 /* eslint-disable max-len */
 
+function instructionForContexts(args: Pick<ReqBody, 'pageBody' | 'isPageBodyPartial' | 'partialPageBodyStartIndex' | 'selectedText' | 'selectedPosition'>): string {
+  return `# Contexts:
+## ${args.isPageBodyPartial ? 'pageBodyPartial' : 'pageBody'}:
+
+\`\`\`markdown
+${args.pageBody}
+\`\`\`
+
+${args.isPageBodyPartial && args.partialPageBodyStartIndex != null
+    ? `- **partialPageBodyStartIndex**: ${args.partialPageBodyStartIndex ?? 0}`
+    : ''
+}
+
+${args.selectedText != null
+    ? `## selectedText: \n\n\`\`\`markdown\n${args.selectedText}\n\`\`\``
+    : ''
+}
+
+${args.selectedPosition != null
+    ? `- **selectedPosition**: ${args.selectedPosition}`
+    : ''
+}
+`;
+}
+
 /**
  * Create endpoint handlers for editor assistant
  */
@@ -157,10 +158,18 @@ export const postMessageToEditHandlersFactory: PostMessageHandlersFactory = (cro
     body('pageBody')
       .isString()
       .withMessage('pageBody must be string and not empty'),
+    body('isPageBodyPartial')
+      .optional()
+      .isBoolean()
+      .withMessage('isPageBodyPartial must be boolean'),
     body('selectedText')
       .optional()
       .isString()
       .withMessage('selectedText must be string'),
+    body('selectedPosition')
+      .optional()
+      .isNumeric()
+      .withMessage('selectedPosition must be number'),
     body('threadId').optional().isString().withMessage('threadId must be string'),
   ];
 
@@ -168,7 +177,10 @@ export const postMessageToEditHandlersFactory: PostMessageHandlersFactory = (cro
     accessTokenParser, loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const {
-        userMessage, pageBody, selectedText, threadId,
+        userMessage,
+        pageBody, isPageBodyPartial, partialPageBodyStartIndex,
+        selectedText, selectedPosition,
+        threadId,
       } = req.body;
 
       // Parameter check
@@ -227,22 +239,20 @@ export const postMessageToEditHandlersFactory: PostMessageHandlersFactory = (cro
         // Create stream
         const stream = openaiClient.beta.threads.runs.stream(thread.id, {
           assistant_id: assistant.id,
+          additional_instructions: [
+            instruction(pageBody != null),
+            instructionForContexts({
+              pageBody,
+              isPageBodyPartial,
+              partialPageBodyStartIndex,
+              selectedText,
+              selectedPosition,
+            }),
+          ].join('\n'),
           additional_messages: [
-            {
-              role: 'assistant',
-              content: instruction(pageBody != null),
-            },
             {
               role: 'user',
-              content: `Current markdown content:
-\`\`\`markdown
-${pageBody}
-\`\`\`
-${selectedText != null
-    ? `Current selected text by user:\`\`\`markdown\n${selectedText}\n\`\`\``
-    : ''
-}
-User request: ${userMessage}`,
+              content: `User request: ${userMessage}`,
             },
           ],
           response_format: zodResponseFormat(LlmEditorAssistantResponseSchema, 'editor_assistant_response'),

+ 65 - 0
apps/app/src/features/openai/server/services/assistant/editor-assistant.ts

@@ -6,6 +6,61 @@ import { AssistantType } from './assistant-types';
 import { getOrCreateAssistant } from './create-assistant';
 import { instructionsForFileSearch, instructionsForInjectionCountermeasures } from './instructions/commons';
 
+
+/* eslint-disable max-len */
+const instructionsForUserIntentDetection = `# USER INTENT DETECTION:
+  First, analyze the user's message to determine their intent:
+  - **Consultation Type**: Questions, discussions, explanations, or advice seeking WITHOUT explicit request to edit/modify/generate text
+  - **Edit Type**: Clear requests to edit, modify, fix, generate, create, or write content
+
+  ## EXAMPLES OF USER INTENT:
+  ### Consultation Type Examples:
+  - "What do you think about this code?"
+  - "Please give me advice on this text structure"
+  - "Why is this error occurring?"
+  - "Is there a better approach?"
+  - "Can you explain how this works?"
+  - "What are the pros and cons of this method?"
+  - "How should I organize this document?"
+
+  ### Edit Type Examples:
+  - "Please fix the following"
+  - "Add a function that..."
+  - "Rewrite this section to..."
+  - "Generate a new paragraph about..."
+  - "Modify this to include..."
+  - "Translate this text to English"`;
+/* eslint-enable max-len */
+
+const instructionsForContexts = `## Editing Contexts
+
+The user will provide you with following contexts related to their markdown content.
+
+### Page body
+The main content of the page, which is written in markdown format. The uer is editing currently this content.
+
+- **pageBody**:
+  - The main content of the page, which is written in markdown format.
+
+- **pageBodyPartial**:
+  - A partial content of the page body, which is written in markdown format and around the cursor position.
+  - This is used when the whole page body is too large to process at once.
+
+- **partialPageBodyStartIndex**:
+  - The start index of the partial page body in the whole page body.
+  - This is expected to be used to provide **startLine** exactly.
+
+### Selected text
+
+- **selectedText**:
+  - The text selected by the user in the page body. The user is focusing on this text to edit.
+
+- **selectedPosition**:
+  - The position of the cursor at the selectedText in the whole page body.
+  - This is expected to be used to **selectedText** exactly and provide **startLine** exactly.
+`;
+
+
 let editorAssistant: OpenAI.Beta.Assistant | undefined;
 
 export const getOrCreateEditorAssistant = async(): Promise<OpenAI.Beta.Assistant> => {
@@ -25,6 +80,16 @@ Your task is to help users edit their markdown content based on their requests.
 ${instructionsForInjectionCountermeasures}
 ---
 
+# Multilingual Support:
+Always provide messages in the same language as the user's request.
+---
+
+${instructionsForContexts}
+---
+
+${instructionsForUserIntentDetection}
+---
+
 ${instructionsForFileSearch}
 `,
     /* eslint-enable max-len */