Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): support ai network search #9357

Merged
merged 1 commit into from
Jan 9, 2025
Merged
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
1 change: 1 addition & 0 deletions .github/actions/copilot-test/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ runs:
DEV_SERVER_URL: http://localhost:8080
COPILOT_OPENAI_API_KEY: ${{ inputs.openai-key }}
COPILOT_FAL_API_KEY: ${{ inputs.fal-key }}
COPILOT_PERPLEXITY_API_KEY: ${{ inputs.perplexity-key }}

- name: Upload test results
if: ${{ failure() }}
Expand Down
2 changes: 2 additions & 0 deletions .github/actions/deploy/deploy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const {
METRICS_CUSTOMER_IO_TOKEN,
COPILOT_OPENAI_API_KEY,
COPILOT_FAL_API_KEY,
COPILOT_PERPLEXITY_API_KEY,
COPILOT_UNSPLASH_API_KEY,
MAILER_SENDER,
MAILER_USER,
Expand Down Expand Up @@ -147,6 +148,7 @@ const createHelmCommand = ({ isDryRun }) => {
`--set graphql.app.copilot.enabled=true`,
`--set-string graphql.app.copilot.openai.key="${COPILOT_OPENAI_API_KEY}"`,
`--set-string graphql.app.copilot.fal.key="${COPILOT_FAL_API_KEY}"`,
`--set-string graphql.app.copilot.perplexity.key="${COPILOT_PERPLEXITY_API_KEY}"`,
`--set-string graphql.app.copilot.unsplash.key="${COPILOT_UNSPLASH_API_KEY}"`,
`--set-string graphql.app.mailer.sender="${MAILER_SENDER}"`,
`--set-string graphql.app.mailer.user="${MAILER_USER}"`,
Expand Down
5 changes: 5 additions & 0 deletions .github/helm/affine/charts/graphql/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ spec:
secretKeyRef:
name: "{{ .Values.app.copilot.secretName }}"
key: falSecret
- name: COPILOT_PERPLEXITY_API_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.app.copilot.secretName }}"
key: perplexitySecret
- name: COPILOT_UNSPLASH_API_KEY
valueFrom:
secretKeyRef:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ jobs:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}

- name: Upload server test coverage results
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
Expand Down Expand Up @@ -619,6 +620,7 @@ jobs:
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
openai-key: ${{ secrets.COPILOT_OPENAI_API_KEY }}
fal-key: ${{ secrets.COPILOT_FAL_API_KEY }}
perplexity-key: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}

server-e2e-test:
name: ${{ matrix.tests.name }}
Expand Down Expand Up @@ -703,6 +705,7 @@ jobs:
DEV_SERVER_URL: http://localhost:8080
COPILOT_OPENAI_API_KEY: 1
COPILOT_FAL_API_KEY: 1
COPILOT_PERPLEXITY_API_KEY: 1

- name: Upload test results
if: ${{ failure() }}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/copilot-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ jobs:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}

- name: Upload server test coverage results
uses: codecov/codecov-action@v5
Expand Down Expand Up @@ -147,6 +148,7 @@ jobs:
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
openai-key: ${{ secrets.COPILOT_OPENAI_API_KEY }}
fal-key: ${{ secrets.COPILOT_FAL_API_KEY }}
perplexity-key: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}

test-done:
needs:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ jobs:
CAPTCHA_TURNSTILE_SECRET: ${{ secrets.CAPTCHA_TURNSTILE_SECRET }}
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_UNSPLASH_API_KEY: ${{ secrets.COPILOT_UNSPLASH_API_KEY }}
METRICS_CUSTOMER_IO_TOKEN: ${{ secrets.METRICS_CUSTOMER_IO_TOKEN }}
MAILER_SENDER: ${{ secrets.OAUTH_EMAIL_SENDER }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class AffineAIPanelWidget extends WidgetComponent {
box-sizing: border-box;
width: 100%;
height: fit-content;
padding: 8px 0;
padding: 10px 0;
}

.ai-panel-container:not(:has(ai-panel-generating)) {
Expand Down Expand Up @@ -474,6 +474,7 @@ export class AffineAIPanelWidget extends WidgetComponent {
.onBlur=${this.discard}
.onFinish=${this._inputFinish}
.onInput=${this.onInput}
.networkSearchConfig=${config.networkSearchConfig}
></ai-panel-input>`,
],
[
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { AIStarIcon } from '@blocksuite/affine-components/icons';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { stopPropagation } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/utils';
import { SendIcon } from '@blocksuite/icons/lit';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { PublishIcon, SendIcon } from '@blocksuite/icons/lit';
import { css, html, LitElement, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';

export class AIPanelInput extends WithDisposable(LitElement) {
import type { AINetworkSearchConfig } from '../../type';

export class AIPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
static override styles = css`
:host {
width: 100%;
Expand All @@ -20,8 +23,9 @@ export class AIPanelInput extends WithDisposable(LitElement) {
background: var(--affine-background-overlay-panel-color);
}

.icon {
.star {
display: flex;
padding: 2px;
align-items: center;
}

Expand Down Expand Up @@ -66,22 +70,36 @@ export class AIPanelInput extends WithDisposable(LitElement) {
display: flex;
align-items: center;
padding: 2px;
gap: 10px;
gap: 4px;
border-radius: 4px;
background: var(--affine-black-10, rgba(0, 0, 0, 0.1));

background: ${unsafeCSSVarV2('icon/disable')};
svg {
width: 16px;
height: 16px;
color: var(--affine-pure-white, #fff);
width: 20px;
height: 20px;
color: ${unsafeCSSVarV2('button/pureWhiteText')};
}
}
.arrow[data-active] {
background: var(--affine-brand-color, #1e96eb);
background: ${unsafeCSSVarV2('icon/activated')};
}
.arrow[data-active]:hover {
cursor: pointer;
}
.network {
display: flex;
align-items: center;
padding: 2px;
gap: 4px;
cursor: pointer;
svg {
width: 20px;
height: 20px;
color: ${unsafeCSSVarV2('icon/primary')};
}
}
.network[data-active='true'] svg {
color: ${unsafeCSSVarV2('icon/activated')};
}
`;

private readonly _onInput = () => {
Expand All @@ -101,22 +119,32 @@ export class AIPanelInput extends WithDisposable(LitElement) {

private readonly _onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
e.preventDefault();
this._sendToAI();
this._sendToAI(e);
}
};

private readonly _sendToAI = () => {
private readonly _sendToAI = (e: MouseEvent | KeyboardEvent) => {
e.preventDefault();
e.stopPropagation();

const value = this.textarea.value.trim();
if (value.length === 0) return;

this.onFinish?.(value);
this.remove();
};

private readonly _toggleNetworkSearch = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();

const enable = this.networkSearchConfig.enabled.value;
this.networkSearchConfig.setEnabled(!enable);
};

override render() {
return html`<div class="root">
<div class="icon">${AIStarIcon}</div>
<div class="star">${AIStarIcon}</div>
<div class="textarea-container">
<textarea
placeholder="What are your thoughts?"
Expand All @@ -131,6 +159,21 @@ export class AIPanelInput extends WithDisposable(LitElement) {
@paste=${stopPropagation}
@keyup=${stopPropagation}
></textarea>
${this.networkSearchConfig.visible.value
? html`
<div
class="network"
data-active=${!!this.networkSearchConfig.enabled.value}
@click=${this._toggleNetworkSearch}
@pointerdown=${stopPropagation}
>
${PublishIcon()}
<affine-tooltip .offset=${12}
>Toggle Network Search</affine-tooltip
>
</div>
`
: nothing}
<div
class="arrow"
@click=${this._sendToAI}
Expand All @@ -157,6 +200,9 @@ export class AIPanelInput extends WithDisposable(LitElement) {
@state()
private accessor _hasContent = false;

@property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig;

@property({ attribute: false })
accessor onFinish: ((input: string) => void) | undefined = undefined;

Expand Down
9 changes: 8 additions & 1 deletion blocksuite/blocks/src/root-block/widgets/ai-panel/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
AIError,
AIItemGroupConfig,
} from '@blocksuite/affine-components/ai-item';
import type { Signal } from '@preact/signals-core';
import type { nothing, TemplateResult } from 'lit';

export interface CopyConfig {
Expand All @@ -28,6 +29,12 @@ export interface AIPanelGeneratingConfig {
stages?: string[];
}

export interface AINetworkSearchConfig {
visible: Signal<boolean | undefined>;
enabled: Signal<boolean | undefined>;
setEnabled: (state: boolean) => void;
}

export interface AffineAIPanelWidgetConfig {
answerRenderer: (
answer: string,
Expand All @@ -44,10 +51,10 @@ export interface AffineAIPanelWidgetConfig {
finishStateConfig: AIPanelAnswerConfig;
generatingStateConfig: AIPanelGeneratingConfig;
errorStateConfig: AIPanelErrorConfig;
networkSearchConfig: AINetworkSearchConfig;
hideCallback?: () => void;
discardCallback?: () => void;
inputCallback?: (input: string) => void;

copy?: CopyConfig;
}

Expand Down
1 change: 1 addition & 0 deletions packages/backend/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# REDIS_SERVER_HOST=localhost
# COPILOT_FAL_API_KEY=YOUR_KEY
# COPILOT_OPENAI_API_KEY=YOUR_KEY
# COPILOT_PERPLEXITY_API_KEY=YOUR_KEY

# MAILER_HOST=127.0.0.1
# MAILER_PORT=1025
Expand Down
1 change: 1 addition & 0 deletions packages/backend/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@socket.io/redis-adapter": "^8.3.0",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"eventsource-parser": "^3.0.0",
"express": "^4.21.2",
"fast-xml-parser": "^4.5.0",
"get-stream": "^9.0.1",
Expand Down
1 change: 1 addition & 0 deletions packages/backend/server/src/config/affine.env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ AFFiNE.ENV_MAP = {
CAPTCHA_TURNSTILE_SECRET: ['plugins.captcha.turnstile.secret', 'string'],
COPILOT_OPENAI_API_KEY: 'plugins.copilot.openai.apiKey',
COPILOT_FAL_API_KEY: 'plugins.copilot.fal.apiKey',
COPILOT_PERPLEXITY_API_KEY: 'plugins.copilot.perplexity.apiKey',
COPILOT_UNSPLASH_API_KEY: 'plugins.copilot.unsplashKey',
REDIS_SERVER_HOST: 'redis.host',
REDIS_SERVER_PORT: ['redis.port', 'int'],
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/server/src/plugins/copilot/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import type { ClientOptions as OpenAIClientOptions } from 'openai';
import { defineStartupConfig, ModuleConfig } from '../../base/config';
import { StorageConfig } from '../../base/storage/config';
import type { FalConfig } from './providers/fal';
import { PerplexityConfig } from './providers/perplexity';

export interface CopilotStartupConfigurations {
openai?: OpenAIClientOptions;
fal?: FalConfig;
perplexity?: PerplexityConfig;
test?: never;
unsplashKey?: string;
storage: StorageConfig;
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/server/src/plugins/copilot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CopilotProviderService,
FalProvider,
OpenAIProvider,
PerplexityProvider,
registerCopilotProvider,
} from './providers';
import {
Expand All @@ -26,6 +27,7 @@ import { CopilotWorkflowExecutors, CopilotWorkflowService } from './workflow';

registerCopilotProvider(FalProvider);
registerCopilotProvider(OpenAIProvider);
registerCopilotProvider(PerplexityProvider);

@Plugin({
name: 'copilot',
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/server/src/plugins/copilot/prompt/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,11 @@ const chat: Prompt[] = [
},
],
},
{
name: 'Search With AFFiNE AI',
model: 'llama-3.1-sonar-small-128k-online',
messages: [],
},
// use for believer plan
{
name: 'Chat With AFFiNE AI - Believer',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,7 @@ export class CopilotProviderService {
if (!this.cachedProviders.has(provider)) {
this.cachedProviders.set(provider, this.create(provider));
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.cachedProviders.get(provider)!;
return this.cachedProviders.get(provider) as CopilotProvider;
}

async getProviderByCapability<C extends CopilotCapability>(
Expand Down Expand Up @@ -196,3 +194,4 @@ export class CopilotProviderService {

export { FalProvider } from './fal';
export { OpenAIProvider } from './openai';
export { PerplexityProvider } from './perplexity';
Loading
Loading