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/support model load balance #503

Merged
merged 11 commits into from
Feb 14, 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
28 changes: 15 additions & 13 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -146,31 +146,33 @@ model ActionResult {

model ActionStep {
/// Primary key
pk BigInt @id @default(autoincrement())
pk BigInt @id @default(autoincrement())
/// Action result id which this step belongs to
resultId String @map("result_id")
resultId String @map("result_id")
/// Action result version
version Int @default(0) @map("version")
version Int @default(0) @map("version")
/// Step order
order Int @default(0) @map("order")
order Int @default(0) @map("order")
/// Step name
name String @map("name")
name String @map("name")
/// Step content
content String @map("content")
content String @map("content")
/// Step reasoning content
reasoningContent String? @map("reasoning_content")
/// Structured data output (JSON)
structuredData String @default("{}") @map("structured_data")
structuredData String @default("{}") @map("structured_data")
/// Action logs
logs String @default("[]") @map("logs")
logs String @default("[]") @map("logs")
/// Action artifacts (JSON array)
artifacts String @default("[]") @map("artifacts")
artifacts String @default("[]") @map("artifacts")
/// Token usage summary (JSON array)
tokenUsage String @default("[]") @map("token_usage")
tokenUsage String @default("[]") @map("token_usage")
/// Create timestamp
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
/// Update timestamp
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
/// Soft delete timestamp
deletedAt DateTime? @map("deleted_at") @db.Timestamptz()
deletedAt DateTime? @map("deleted_at") @db.Timestamptz()

@@index([resultId, version, order])
@@map("action_steps")
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/action/action.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { modelInfoPO2DTO } from '@/misc/misc.dto';

export function actionStepPO2DTO(step: ActionStepModel): ActionStep {
return {
...pick(step, ['name', 'content']),
...pick(step, ['name', 'content', 'reasoningContent']),
logs: JSON.parse(step.logs || '[]'),
artifacts: JSON.parse(step.artifacts || '[]'),
structuredData: JSON.parse(step.structuredData || '{}'),
Expand Down
34 changes: 34 additions & 0 deletions apps/api/src/knowledge/knowledge.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
UseGuards,
ParseIntPipe,
DefaultValuePipe,
UseInterceptors,
UploadedFile,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import {
User,
UpsertResourceRequest,
Expand Down Expand Up @@ -84,6 +87,37 @@ export class KnowledgeController {
return buildSuccessResponse(resourcePO2DTO(resource));
}

@UseGuards(JwtAuthGuard)
@Post('resource/createWithFile')
@UseInterceptors(FileInterceptor('file'))
async createResourceWithFile(
@LoginedUser() user: User,
@UploadedFile() file: Express.Multer.File,
@Body() body: UpsertResourceRequest,
): Promise<UpsertResourceResponse> {
if (!file) {
throw new ParamsError('File is required');
}

// Convert file content to string
const content = file.buffer.toString('utf-8');

// Create resource with file content
const resource = await this.knowledgeService.createResource(
user,
{
...body,
content,
},
{
checkStorageQuota: true,
},
);

await this.knowledgeService.syncStorageUsage(user);
return buildSuccessResponse(resourcePO2DTO(resource));
}

@UseGuards(JwtAuthGuard)
@Post('resource/batchCreate')
async importResource(
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/misc/misc.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class MiscController {
return buildSuccessResponse({ content: result });
}

@UseGuards(JwtAuthGuard)
@Get('static/:objectKey')
@Header('Access-Control-Allow-Origin', '*')
@Header('Cross-Origin-Resource-Policy', 'cross-origin')
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/skill/skill.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Queue } from 'bullmq';
import * as Y from 'yjs';
import { InjectQueue } from '@nestjs/bullmq';
import { AIMessage, HumanMessage } from '@langchain/core/messages';
import { getWholeParsedContent } from '@refly-packages/utils';
import {
Prisma,
SkillTrigger as SkillTriggerModel,
Expand Down Expand Up @@ -632,7 +633,7 @@ export class SkillService {
? steps.map(
(step) =>
new AIMessage({
content: step.content, // TODO: dump artifact content to message
content: getWholeParsedContent(step.reasoningContent, step.content), // // TODO: dump artifact content to message
additional_kwargs: {
skillMeta: result.actionMeta,
structuredData: step.structuredData,
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/utils/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,14 @@ export class ResultAggregator {
version: number;
}): Prisma.ActionStepCreateManyInput[] {
return this.stepNames.map((stepName, order) => {
const { name, content, structuredData, artifacts, usageItems, logs } = this.data[stepName];
const { name, content, structuredData, artifacts, usageItems, logs, reasoningContent } =
this.data[stepName];
const aggregatedUsage = aggregateTokenUsage(usageItems);

return {
name,
content,
reasoningContent,
resultId,
version,
order,
Expand Down
24 changes: 14 additions & 10 deletions apps/extension/src/hooks/use-save-resource.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CreateResourceData, type BaseResponse } from '@refly/openapi-schema';
import { UpsertResourceRequest, type BaseResponse } from '@refly/openapi-schema';
import { getMarkdown, preprocessHtmlContent } from '@refly/utils/html2md';
import { convertHTMLToMarkdown } from '@refly/utils/markdown';
import getClient from '@refly-packages/ai-workspace-common/requests/proxiedRequest';
Expand Down Expand Up @@ -54,19 +54,23 @@ export const useSaveCurrentWeblinkAsResource = () => {
isPublic: false,
readOnly: true,
collabEnabled: false,
content: pageContent || '',
};

const createResourceData: CreateResourceData = {
body: {
title: resource?.title,
resourceType: 'weblink',
data: resource?.data,
content: resource?.content,
},
const textBlob = new Blob([pageContent], { type: 'text/plain' });
const textFile = new File([textBlob], 'content.txt', { type: 'text/plain' });

const createResourceData: UpsertResourceRequest = {
title: resource?.title,
resourceType: 'weblink',
data: resource?.data,
};

const { error } = await getClient().createResource(createResourceData);
const { error } = await getClient().createResourceWithFile({
body: {
...createResourceData,
file: textFile,
},
});
// const resourceId = data?.data?.resourceId;
// const url = `${getClientOrigin(false)}/resource/${resourceId}`;
const url = `${getClientOrigin(false)}`;
Expand Down
11 changes: 8 additions & 3 deletions apps/extension/src/hooks/use-save-selected-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,23 @@ export const useSaveSelectedContent = () => {
const title = metadata?.title || document?.title || 'Untitled';
const url = metadata?.url || document?.location?.href || 'https://www.refly.ai';

// Create a text file from the content
const textBlob = new Blob([content], { type: 'text/plain' });
const textFile = new File([textBlob], 'content.txt', { type: 'text/plain' });
const createResourceData: UpsertResourceRequest = {
resourceType: 'text',
title,
content: content || '',
data: {
url,
title,
},
};

const { error } = await getClient().createResource({
body: createResourceData,
const { error } = await getClient().createResourceWithFile({
body: {
...createResourceData,
file: textFile,
},
});

// const resourceId = data?.data?.resourceId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,17 @@ export const NodeSelector = (props: NodeSelectorProps) => {

const { nodes } = useCanvasData();

const targetNodes = nodes.filter((node) => !['group'].includes(node?.type));
const sortedItems: IContextItem[] = targetNodes.map((node) => ({
const targetNodes = nodes.filter((node) => !['group', 'skill'].includes(node?.type));
const sortedItems: IContextItem[] = [...targetNodes].reverse().map((node) => ({
title: node.data?.title,
entityId: node.data?.entityId,
type: node.type,
metadata: node.data?.metadata,
}));

const processedItems = useMemo(() => {
return (
sortedItems?.filter((item) =>
item?.title?.toLowerCase().includes(searchValue.toLowerCase()),
) ?? []
return sortedItems.filter((item) =>
item?.title?.toLowerCase().includes(searchValue.toLowerCase()),
);
}, [sortedItems, searchValue]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ const ChatInputComponent = forwardRef<HTMLDivElement, ChatInputProps>(
ref={ref}
className={cn(
'w-full h-full flex flex-col flex-grow overflow-y-auto relative',
isDragging && 'ring-2 ring-blue-500 ring-opacity-50',
isDragging && 'ring-2 ring-green-500 ring-opacity-50 rounded-lg',
)}
onPaste={handlePaste}
onDragOver={(e) => {
Expand Down Expand Up @@ -227,8 +227,8 @@ const ChatInputComponent = forwardRef<HTMLDivElement, ChatInputProps>(
}}
>
{isDragging && (
<div className="absolute inset-0 bg-blue-50 bg-opacity-50 flex items-center justify-center pointer-events-none z-10">
<div className="text-blue-500 text-sm">{t('common.dropImageHere')}</div>
<div className="absolute inset-0 bg-green-50/50 flex items-center justify-center pointer-events-none z-10 rounded-lg border-2 border-green-500/30">
<div className="text-green-600 text-sm font-medium">{t('common.dropImageHere')}</div>
</div>
)}
<AutoComplete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,36 +77,32 @@ export const BaseMarkContextSelector = (props: BaseMarkContextSelectorProps) =>

// Memoize the filtered and sorted nodes to prevent unnecessary recalculations
const processedNodes = useMemo(() => {
const sortedItems: IContextItem[] = [
...(selectedItems || []),
...(
targetNodes?.filter(
// First get unselected nodes and reverse them to show most recent first
const unselectedNodes =
targetNodes
?.filter(
(node) => !selectedItems.some((selected) => selected.entityId === node.data?.entityId),
) || []
).map((node) => ({
title:
node?.type === 'memo'
? node.data?.contentPreview
? `${node.data?.title} - ${node.data?.contentPreview?.slice(0, 10)}`
: node.data?.title
: node.data?.title,
entityId: node.data?.entityId,
type: node.type,
metadata: node.data?.metadata,
})),
];

const filteredItems =
sortedItems?.filter((item) =>
item?.title?.toLowerCase().includes(searchValue.toLowerCase()),
) ?? [];

return [
...(selectedItems ?? []),
...(filteredItems?.filter(
(item) => !selectedItems?.some((selected) => selected?.entityId === item?.entityId),
) ?? []),
];
)
.reverse()
.map((node) => ({
title:
node?.type === 'memo'
? node.data?.contentPreview
? `${node.data?.title} - ${node.data?.contentPreview?.slice(0, 10)}`
: node.data?.title
: node.data?.title,
entityId: node.data?.entityId,
type: node.type,
metadata: node.data?.metadata,
})) ?? [];

// Filter based on search value
const filteredUnselectedNodes = unselectedNodes.filter((item) =>
item?.title?.toLowerCase().includes(searchValue.toLowerCase()),
);

// Return selected items first, followed by filtered & reversed unselected nodes
return [...(selectedItems ?? []), ...filteredUnselectedNodes];
}, [targetNodes, searchValue, selectedItems]);

// Memoize the render data transformation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { getArtifactIcon } from '@refly-packages/ai-workspace-common/components/
import { RecommendQuestions } from '@refly-packages/ai-workspace-common/components/canvas/node-preview/skill-response/recommend-questions';
import { useNodeSelection } from '@refly-packages/ai-workspace-common/hooks/canvas/use-node-selection';
import { IContextItem } from '@refly-packages/ai-workspace-common/stores/context-panel';
import { getWholeParsedContent } from '@refly-packages/ai-workspace-common/utils/content-parser';
import { getWholeParsedContent } from '@refly-packages/utils/content-parser';

const parseStructuredData = (structuredData: Record<string, unknown>, field: string) => {
return typeof structuredData[field] === 'string'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { useCanvasContext } from '@refly-packages/ai-workspace-common/context/ca
import { IconRerun } from '@refly-packages/ai-workspace-common/components/common/icon';

import { locateToNodePreviewEmitter } from '@refly-packages/ai-workspace-common/events/locateToNodePreview';
import { getWholeParsedContent } from '@refly-packages/ai-workspace-common/utils/content-parser';
import { getWholeParsedContent } from '@refly-packages/utils/content-parser';
interface SkillResponseNodePreviewProps {
node: CanvasNode<ResponseNodeMeta>;
resultId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@refly-packages/ai-workspace-common/components/canvas/nodes';
import { aggregateTokenUsage } from '@refly-packages/utils/models';
import { useSetNodeDataByEntity } from './use-set-node-data-by-entity';
import { getWholeParsedContent } from '@refly-packages/ai-workspace-common/utils/content-parser';
import { getWholeParsedContent } from '@refly-packages/utils/content-parser';

const generateFullNodeDataUpdates = (
payload: ActionResult,
Expand Down
9 changes: 9 additions & 0 deletions packages/ai-workspace-common/src/queries/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
createLabelInstance,
createPortalSession,
createResource,
createResourceWithFile,
createShare,
createSkillInstance,
createSkillTrigger,
Expand Down Expand Up @@ -371,6 +372,14 @@ export const UseCreateResourceKeyFn = (mutationKey?: Array<unknown>) => [
useCreateResourceKey,
...(mutationKey ?? []),
];
export type CreateResourceWithFileMutationResult = Awaited<
ReturnType<typeof createResourceWithFile>
>;
export const useCreateResourceWithFileKey = 'CreateResourceWithFile';
export const UseCreateResourceWithFileKeyFn = (mutationKey?: Array<unknown>) => [
useCreateResourceWithFileKey,
...(mutationKey ?? []),
];
export type BatchCreateResourceMutationResult = Awaited<ReturnType<typeof batchCreateResource>>;
export const useBatchCreateResourceKey = 'BatchCreateResource';
export const UseBatchCreateResourceKeyFn = (mutationKey?: Array<unknown>) => [
Expand Down
Loading