feat(core): add ai workspace all docs switch (#13345)

Close [AI-397](https://linear.app/affine-design/issue/AI-397)

<img width="272" height="186" alt="截屏2025-07-29 11 54 20"
src="https://github.com/user-attachments/assets/e171fb57-66cf-4244-894d-c27b18cbe83a"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced an AI tools configuration service, allowing users to
customize AI tool usage (e.g., workspace search, reading docs) in chat
and AI features.
* Added a toggle in chat preferences for enabling or disabling
workspace-wide document search.
* AI chat components now respect user-configured tool settings across
chat, retry, and playground scenarios.

* **Improvements**
* Enhanced chat and AI interfaces to propagate and honor user tool
configuration throughout the frontend and backend.
* Made draft and tool configuration services optional and safely handled
their absence in chat components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wu Yue
2025-07-30 10:10:39 +08:00
committed by GitHub
parent 091bac1047
commit f7a094053e
23 changed files with 321 additions and 61 deletions
@@ -56,7 +56,7 @@ import { StreamObjectParser } from './providers/utils';
import { ChatSession, ChatSessionService } from './session';
import { CopilotStorage } from './storage';
import { ChatMessage, ChatQuerySchema } from './types';
import { getSignal } from './utils';
import { getSignal, getTools } from './utils';
import { CopilotWorkflowService, GraphExecutorState } from './workflow';
export interface ChatEvent {
@@ -244,7 +244,8 @@ export class CopilotController implements BeforeApplicationShutdown {
info.finalMessage = finalMessage.filter(m => m.role !== 'system');
metrics.ai.counter('chat_calls').add(1, { model });
const { reasoning, webSearch } = ChatQuerySchema.parse(query);
const { reasoning, webSearch, toolsConfig } =
ChatQuerySchema.parse(query);
const content = await provider.text({ modelId: model }, finalMessage, {
...session.config.promptConfig,
signal: getSignal(req).signal,
@@ -253,6 +254,7 @@ export class CopilotController implements BeforeApplicationShutdown {
workspace: session.config.workspaceId,
reasoning,
webSearch,
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
});
session.push({
@@ -306,7 +308,8 @@ export class CopilotController implements BeforeApplicationShutdown {
}
});
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
const { messageId, reasoning, webSearch, toolsConfig } =
ChatQuerySchema.parse(query);
const source$ = from(
provider.streamText({ modelId: model }, finalMessage, {
@@ -317,6 +320,7 @@ export class CopilotController implements BeforeApplicationShutdown {
workspace: session.config.workspaceId,
reasoning,
webSearch,
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
})
).pipe(
connect(shared$ =>
@@ -398,7 +402,8 @@ export class CopilotController implements BeforeApplicationShutdown {
}
});
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
const { messageId, reasoning, webSearch, toolsConfig } =
ChatQuerySchema.parse(query);
const source$ = from(
provider.streamObject({ modelId: model }, finalMessage, {
@@ -409,6 +414,7 @@ export class CopilotController implements BeforeApplicationShutdown {
workspace: session.config.workspaceId,
reasoning,
webSearch,
tools: getTools(session.config.promptConfig?.tools, toolsConfig),
})
).pipe(
connect(shared$ =>
@@ -57,28 +57,28 @@ export const VertexSchema: JSONSchema = {
// ========== prompt ==========
export const PromptToolsSchema = z
.enum([
'codeArtifact',
'conversationSummary',
// work with morph
'docEdit',
// work with indexer
'docRead',
'docKeywordSearch',
// work with embeddings
'docSemanticSearch',
// work with exa/model internal tools
'webSearch',
// artifact tools
'docCompose',
// section editing
'sectionEdit',
])
.array();
export const PromptConfigStrictSchema = z.object({
tools: z
.enum([
'codeArtifact',
'conversationSummary',
// work with morph
'docEdit',
// work with indexer
'docRead',
'docKeywordSearch',
// work with embeddings
'docSemanticSearch',
// work with exa/model internal tools
'webSearch',
// artifact tools
'docCompose',
// section editing
'sectionEdit',
])
.array()
.nullable()
.optional(),
tools: PromptToolsSchema.nullable().optional(),
// params requirements
requireContent: z.boolean().nullable().optional(),
requireAttachment: z.boolean().nullable().optional(),
@@ -107,6 +107,8 @@ export const PromptConfigSchema =
export type PromptConfig = z.infer<typeof PromptConfigSchema>;
export type PromptTools = z.infer<typeof PromptToolsSchema>;
// ========== message ==========
export const EmbeddingMessage = z.array(z.string().trim().min(1)).min(1);
@@ -16,6 +16,23 @@ const zMaybeString = z.preprocess(val => {
return s === '' || s == null ? undefined : s;
}, z.string().min(1).optional());
const ToolsConfigSchema = z.preprocess(
val => {
// if val is a string, try to parse it as JSON
if (typeof val === 'string') {
try {
return JSON.parse(val);
} catch {
return {};
}
}
return val || {};
},
z.record(z.enum(['searchWorkspace', 'readingDocs']), z.boolean()).default({})
);
export type ToolsConfig = z.infer<typeof ToolsConfigSchema>;
export const ChatQuerySchema = z
.object({
messageId: zMaybeString,
@@ -23,15 +40,25 @@ export const ChatQuerySchema = z
retry: zBool,
reasoning: zBool,
webSearch: zBool,
toolsConfig: ToolsConfigSchema,
})
.catchall(z.string())
.transform(
({ messageId, modelId, retry, reasoning, webSearch, ...params }) => ({
({
messageId,
modelId,
retry,
reasoning,
webSearch,
toolsConfig,
...params
}) => ({
messageId,
modelId,
retry,
reasoning,
webSearch,
toolsConfig,
params,
})
);
@@ -3,7 +3,8 @@ import { Readable } from 'node:stream';
import type { Request } from 'express';
import { readBufferWithLimit } from '../../base';
import { MAX_EMBEDDABLE_SIZE } from './types';
import { PromptTools } from './providers';
import { MAX_EMBEDDABLE_SIZE, ToolsConfig } from './types';
export function readStream(
readable: Readable,
@@ -49,3 +50,33 @@ export function getSignal(req: Request): SignalReturnType {
onConnectionClosed: cb => (callback = cb),
};
}
export function getTools(
tools?: PromptTools | null,
toolsConfig?: ToolsConfig
) {
if (!tools || !toolsConfig) {
return tools;
}
let result: PromptTools = tools;
(Object.keys(toolsConfig) as Array<keyof ToolsConfig>).forEach(key => {
const value = toolsConfig[key];
switch (key) {
case 'searchWorkspace':
if (value === false) {
result = result.filter(tool => {
return tool !== 'docKeywordSearch' && tool !== 'docSemanticSearch';
});
}
break;
case 'readingDocs':
if (value === false) {
result = result.filter(tool => {
return tool !== 'docRead';
});
}
break;
}
});
return result;
}
@@ -1,3 +1,4 @@
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
import type {
AddContextFileInput,
ContextMatchedDocChunk,
@@ -142,6 +143,7 @@ declare global {
webSearch?: boolean;
reasoning?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig | undefined;
contexts?: {
docs: AIDocContextOption[];
files: AIFileContextOption[];
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
@@ -105,6 +106,9 @@ export class AIChatPanelTitle extends SignalWatcher(
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor session!: CopilotChatHistoryFragment | null | undefined;
@@ -142,6 +146,7 @@ export class AIChatPanelTitle extends SignalWatcher(
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.aiToolsConfigService=${this.aiToolsConfigService}
></playground-content>
`;
@@ -1,4 +1,7 @@
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
@@ -119,6 +122,9 @@ export class ChatPanel extends SignalWatcher(
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor session: CopilotChatHistoryFragment | null | undefined;
@@ -387,6 +393,7 @@ export class ChatPanel extends SignalWatcher(
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.session=${this.session}
.status=${this.status}
.embeddingProgress=${this.embeddingProgress}
@@ -413,6 +420,7 @@ export class ChatPanel extends SignalWatcher(
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiDraftService=${this.aiDraftService}
.aiToolsConfigService=${this.aiToolsConfigService}
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
.onContextChange=${this.onContextChange}
.width=${this.sidebarWidth}
@@ -1,6 +1,9 @@
import './ai-chat-composer-tip';
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type {
ContextEmbedStatus,
@@ -118,7 +121,10 @@ export class AIChatComposer extends SignalWatcher(
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor chips: ChatChip[] = [];
@@ -166,6 +172,7 @@ export class AIChatComposer extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
.aiDraftService=${this.aiDraftService}
.aiToolsConfigService=${this.aiToolsConfigService}
.portalContainer=${this.portalContainer}
.onChatSuccess=${this.onChatSuccess}
.trackOptions=${this.trackOptions}
@@ -1,4 +1,7 @@
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { AIDraftState } from '@affine/core/modules/ai-button/services/ai-draft';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
@@ -153,7 +156,10 @@ export class AIChatContent extends SignalWatcher(
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor onEmbeddingProgressChange:
@@ -273,6 +279,9 @@ export class AIChatContent extends SignalWatcher(
};
private readonly updateDraft = async (context: Partial<ChatContextValue>) => {
if (!this.aiDraftService) {
return;
}
const draft: Partial<AIDraftState> = pick(context, [
'quote',
'images',
@@ -344,15 +353,17 @@ export class AIChatContent extends SignalWatcher(
this.initChatContent().catch(console.error);
this.aiDraftService
.getDraft()
.then(draft => {
this.chatContextValue = {
...this.chatContextValue,
...draft,
};
})
.catch(console.error);
if (this.aiDraftService) {
this.aiDraftService
.getDraft()
.then(draft => {
this.chatContextValue = {
...this.chatContextValue,
...draft,
};
})
.catch(console.error);
}
this._disposables.add(
AIProvider.slots.actions.subscribe(({ event }) => {
@@ -405,6 +416,7 @@ export class AIChatContent extends SignalWatcher(
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.width=${this.width}
@@ -434,6 +446,7 @@ export class AIChatContent extends SignalWatcher(
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.notificationService=${this.notificationService}
.aiDraftService=${this.aiDraftService}
.aiToolsConfigService=${this.aiToolsConfigService}
.trackOptions=${{
where: 'chat-panel',
control: 'chat-send',
@@ -1,4 +1,7 @@
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
@@ -353,7 +356,10 @@ export class AIChatInput extends SignalWatcher(
accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor isRootSession: boolean = true;
@@ -406,13 +412,15 @@ export class AIChatInput extends SignalWatcher(
protected override firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.aiDraftService
.getDraft()
.then(draft => {
this.textarea.value = draft.input;
this.isInputEmpty = !this.textarea.value.trim();
})
.catch(console.error);
if (this.aiDraftService) {
this.aiDraftService
.getDraft()
.then(draft => {
this.textarea.value = draft.input;
this.isInputEmpty = !this.textarea.value.trim();
})
.catch(console.error);
}
}
protected override render() {
@@ -493,6 +501,7 @@ export class AIChatInput extends SignalWatcher(
.networkSearchVisible=${!!this.networkSearchConfig.visible.value}
.isNetworkActive=${this._isNetworkActive}
.onNetworkActiveChange=${this._toggleNetworkSearch}
.toolsConfigService=${this.aiToolsConfigService}
></chat-input-preference>
${status === 'transmitting' || status === 'loading'
? html`<button
@@ -536,9 +545,11 @@ export class AIChatInput extends SignalWatcher(
textarea.style.overflowY = 'scroll';
}
await this.aiDraftService.setDraft({
input: value,
});
if (this.aiDraftService) {
await this.aiDraftService.setDraft({
input: value,
});
}
};
private readonly _handleKeyDown = async (evt: KeyboardEvent) => {
@@ -593,9 +604,11 @@ export class AIChatInput extends SignalWatcher(
this.isInputEmpty = true;
this.textarea.style.height = 'unset';
await this.aiDraftService.setDraft({
input: '',
});
if (this.aiDraftService) {
await this.aiDraftService.setDraft({
input: '',
});
}
await this.send(value);
};
@@ -647,6 +660,7 @@ export class AIChatInput extends SignalWatcher(
control: this.trackOptions?.control,
webSearch: this._isNetworkActive,
reasoning: this._isReasoningActive,
toolsConfig: this.aiToolsConfigService.config.value,
modelId: this.modelId,
});
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import {
menu,
@@ -7,6 +8,7 @@ import {
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import {
ArrowDownSmallIcon,
CloudWorkspaceIcon,
ThinkingIcon,
WebIcon,
} from '@blocksuite/icons/lit';
@@ -81,6 +83,9 @@ export class ChatInputPreference extends SignalWatcher(
| undefined;
// --------- search props end ---------
@property({ attribute: false })
accessor toolsConfigService!: AIToolsConfigService;
// private readonly _onModelChange = (modelId: string) => {
// this.onModelChange?.(modelId);
// };
@@ -126,6 +131,19 @@ export class ChatInputPreference extends SignalWatcher(
onChange: (value: boolean) => this.onNetworkActiveChange?.(value),
class: { 'preference-action': true },
testId: 'chat-network-search',
}),
menu.toggleSwitch({
name: 'Workspace All Docs',
prefix: CloudWorkspaceIcon(),
on:
!!this.toolsConfigService.config.value.searchWorkspace &&
!!this.toolsConfigService.config.value.readingDocs,
onChange: (value: boolean) =>
this.toolsConfigService.setConfig({
searchWorkspace: value,
readingDocs: value,
}),
class: { 'preference-action': true },
})
);
}
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit';
@@ -206,6 +207,9 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor docDisplayService!: DocDisplayConfig;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
@@ -467,6 +471,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
isRootSession: true,
reasoning: this._isReasoningActive,
webSearch: this._isNetworkActive,
toolsConfig: this.aiToolsConfigService.config.value,
});
for await (const text of stream) {
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
@@ -173,6 +174,9 @@ export class PlaygroundChat extends SignalWatcher(
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor addChat!: () => Promise<void>;
@@ -338,6 +342,7 @@ export class PlaygroundChat extends SignalWatcher(
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.messages=${this.messages}
@@ -357,6 +362,7 @@ export class PlaygroundChat extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
></ai-chat-composer>
</div>`;
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
@@ -92,6 +93,9 @@ export class PlaygroundContent extends SignalWatcher(
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor sessions: CopilotChatHistoryFragment[] = [];
@@ -347,6 +351,7 @@ export class PlaygroundContent extends SignalWatcher(
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.addChat=${this.addChat}
></playground-chat>
</div>
@@ -1,3 +1,7 @@
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type {
@@ -393,6 +397,7 @@ export class AIChatBlockPeekView extends LitElement {
control: 'chat-send',
reasoning: this._isReasoningActive,
webSearch: this._isNetworkActive,
toolsConfig: this.aiToolsConfigService.config.value,
});
for await (const text of stream) {
@@ -608,6 +613,7 @@ export class AIChatBlockPeekView extends LitElement {
.searchMenuConfig=${this.searchMenuConfig}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.notificationService=${notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.onChatSuccess=${this._onChatSuccess}
.trackOptions=${{
where: 'ai-chat-block',
@@ -646,6 +652,12 @@ export class AIChatBlockPeekView extends LitElement {
@property({ attribute: false })
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor _historyMessages: ChatMessage[] = [];
@@ -682,7 +694,9 @@ export const AIChatBlockPeekViewTemplate = (
networkSearchConfig: AINetworkSearchConfig,
reasoningConfig: AIReasoningConfig,
affineFeatureFlagService: FeatureFlagService,
affineWorkspaceDialogService: WorkspaceDialogService
affineWorkspaceDialogService: WorkspaceDialogService,
aiDraftService: AIDraftService,
aiToolsConfigService: AIToolsConfigService
) => {
return html`<ai-chat-block-peek-view
.blockModel=${blockModel}
@@ -693,5 +707,7 @@ export const AIChatBlockPeekViewTemplate = (
.reasoningConfig=${reasoningConfig}
.affineFeatureFlagService=${affineFeatureFlagService}
.affineWorkspaceDialogService=${affineWorkspaceDialogService}
.aiDraftService=${aiDraftService}
.aiToolsConfigService=${aiToolsConfigService}
></ai-chat-block-peek-view>`;
};
@@ -1,4 +1,5 @@
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
import type { UserFriendlyError } from '@affine/error';
import {
addContextCategoryMutation,
@@ -415,6 +416,7 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
signal,
}: {
sessionId: string;
@@ -422,6 +424,7 @@ export class CopilotClient {
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
signal?: AbortSignal;
}) {
let url = `/api/copilot/chat/${sessionId}`;
@@ -430,6 +433,7 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
});
if (queryString) {
url += `?${queryString}`;
@@ -446,12 +450,14 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
}: {
sessionId: string;
messageId?: string;
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
},
endpoint = Endpoint.Stream
) {
@@ -461,6 +467,7 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
});
if (queryString) {
url += `?${queryString}`;
@@ -486,7 +493,9 @@ export class CopilotClient {
return this.eventSource(url);
}
paramsToQueryString(params: Record<string, string | boolean | undefined>) {
paramsToQueryString(
params: Record<string, string | boolean | undefined | Record<string, any>>
) {
const queryString = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (typeof value === 'boolean') {
@@ -495,6 +504,8 @@ export class CopilotClient {
}
} else if (typeof value === 'string') {
queryString.append(key, value);
} else if (typeof value === 'object' && value !== null) {
queryString.append(key, JSON.stringify(value));
}
});
return queryString.toString();
@@ -1,3 +1,4 @@
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
import { partition } from 'lodash-es';
import { AIProvider } from './ai-provider';
@@ -22,6 +23,7 @@ export type TextToTextOptions = {
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
};
export type ToImageOptions = TextToTextOptions & {
@@ -119,6 +121,7 @@ export function textToText({
reasoning,
webSearch,
modelId,
toolsConfig,
}: TextToTextOptions) {
let messageId: string | undefined;
@@ -141,6 +144,7 @@ export function textToText({
reasoning,
webSearch,
modelId,
toolsConfig,
},
endpoint
);
@@ -11,7 +11,10 @@ import { getViewManager } from '@affine/core/blocksuite/manager/view';
import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service';
import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
import { useAISpecs } from '@affine/core/components/hooks/affine/use-ai-specs';
import { AIDraftService } from '@affine/core/modules/ai-button';
import {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import {
EventSourceService,
FetchService,
@@ -223,6 +226,7 @@ export const Component = () => {
confirmModal.openConfirmModal
);
content.aiDraftService = framework.get(AIDraftService);
content.aiToolsConfigService = framework.get(AIToolsConfigService);
content.createSession = createSession;
content.onOpenDoc = onOpenDoc;
@@ -4,7 +4,10 @@ import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-
import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service';
import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
import { useAISpecs } from '@affine/core/components/hooks/affine/use-ai-specs';
import { AIDraftService } from '@affine/core/modules/ai-button';
import {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { AppThemeService } from '@affine/core/modules/theme';
@@ -97,6 +100,8 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
confirmModal.openConfirmModal
);
chatPanelRef.current.aiDraftService = framework.get(AIDraftService);
chatPanelRef.current.aiToolsConfigService =
framework.get(AIToolsConfigService);
containerRef.current?.append(chatPanelRef.current);
} else {
@@ -1,6 +1,10 @@
export { AIButtonProvider } from './provider/ai-button';
export { AIButtonService } from './services/ai-button';
export { AIDraftService } from './services/ai-draft';
export {
type AIToolsConfig,
AIToolsConfigService,
} from './services/tools-config';
import type { Framework } from '@toeverything/infra';
@@ -13,6 +17,7 @@ import { AIDraftService } from './services/ai-draft';
import { AINetworkSearchService } from './services/network-search';
import { AIPlaygroundService } from './services/playground';
import { AIReasoningService } from './services/reasoning';
import { AIToolsConfigService } from './services/tools-config';
export const configureAIButtonModule = (framework: Framework) => {
framework.service(AIButtonService, container => {
@@ -40,3 +45,7 @@ export function configureAIDraftModule(framework: Framework) {
.scope(WorkspaceScope)
.service(AIDraftService, [GlobalStateService, CacheStorage]);
}
export function configureAIToolsConfigModule(framework: Framework) {
framework.service(AIToolsConfigService, [GlobalStateService]);
}
@@ -0,0 +1,50 @@
import {
createSignalFromObservable,
type Signal,
} from '@blocksuite/affine/shared/utils';
import { LiveData, Service } from '@toeverything/infra';
import { map } from 'rxjs';
import type { GlobalStateService } from '../../storage';
const AI_TOOLS_CONFIG_KEY = 'AIToolsConfig';
export interface AIToolsConfig {
searchWorkspace?: boolean;
readingDocs?: boolean;
}
export class AIToolsConfigService extends Service {
constructor(private readonly globalStateService: GlobalStateService) {
super();
const { signal, cleanup: enabledCleanup } =
createSignalFromObservable<AIToolsConfig>(this.config$, {
searchWorkspace: true,
readingDocs: true,
});
this.config = signal;
this.disposables.push(enabledCleanup);
}
config: Signal<AIToolsConfig>;
private readonly config$ = LiveData.from(
this.globalStateService.globalState.watch<AIToolsConfig>(
AI_TOOLS_CONFIG_KEY
),
undefined
).pipe(
map(config => ({
searchWorkspace: config?.searchWorkspace ?? true,
readingDocs: config?.readingDocs ?? true,
}))
);
setConfig = (data: Partial<AIToolsConfig>) => {
this.globalStateService.globalState.set(AI_TOOLS_CONFIG_KEY, {
...this.config.value,
...data,
});
};
}
@@ -7,6 +7,7 @@ import {
configureAINetworkSearchModule,
configureAIPlaygroundModule,
configureAIReasoningModule,
configureAIToolsConfigModule,
} from './ai-button';
import { configureAppSidebarModule } from './app-sidebar';
import { configAtMenuConfigModule } from './at-menu-config';
@@ -112,6 +113,7 @@ export function configureCommonModules(framework: Framework) {
configureAIPlaygroundModule(framework);
configureAIButtonModule(framework);
configureAIDraftModule(framework);
configureAIToolsConfigModule(framework);
configureTemplateDocModule(framework);
configureBlobManagementModule(framework);
configureMediaModule(framework);
@@ -2,6 +2,10 @@ import { toReactNode } from '@affine/component';
import { AIChatBlockPeekViewTemplate } from '@affine/core/blocksuite/ai';
import type { AIChatBlockModel } from '@affine/core/blocksuite/ai/blocks/ai-chat-block/model/ai-chat-model';
import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
import {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { EditorHost } from '@blocksuite/affine/std';
@@ -27,6 +31,8 @@ export const AIChatBlockPeekView = ({
const framework = useFramework();
const affineFeatureFlagService = framework.get(FeatureFlagService);
const affineWorkspaceDialogService = framework.get(WorkspaceDialogService);
const aiDraftService = framework.get(AIDraftService);
const aiToolsConfigService = framework.get(AIToolsConfigService);
return useMemo(() => {
const template = AIChatBlockPeekViewTemplate(
@@ -37,7 +43,9 @@ export const AIChatBlockPeekView = ({
networkSearchConfig,
reasoningConfig,
affineFeatureFlagService,
affineWorkspaceDialogService
affineWorkspaceDialogService,
aiDraftService,
aiToolsConfigService
);
return toReactNode(template);
}, [
@@ -49,5 +57,7 @@ export const AIChatBlockPeekView = ({
reasoningConfig,
affineFeatureFlagService,
affineWorkspaceDialogService,
aiDraftService,
aiToolsConfigService,
]);
};