;
+ parseAndValidate(raw: any): CONFIG;
+ getProviderConfigFields: () => UIConfigField[];
+ getProviderMetadata: () => ProviderMetadata;
+};
+
+export const createProviderInstance = >(
+ Provider: P,
+ id: string,
+ name: string,
+ rawConfig: unknown,
+): InstanceType
=> {
+ const cfg = Provider.parseAndValidate(rawConfig);
+ return new Provider(id, name, cfg) as InstanceType
;
+};
+
+export default BaseModelProvider;
From 4bcbdad6cb3346faabc1ac3b1e0439ac0f00d10d Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Tue, 18 Nov 2025 14:39:04 +0530
Subject: [PATCH 013/199] feat(providers): implement custom classes
---
src/lib/models/providers/baseProvider.ts | 45 -----
src/lib/models/providers/index.ts | 18 +-
.../providers/{ollama.ts => ollama/index.ts} | 24 +--
.../providers/ollama/ollamaEmbedding.ts | 39 +++++
src/lib/models/providers/ollama/ollamaLLM.ts | 149 ++++++++++++++++
.../providers/{openai.ts => openai/index.ts} | 26 ++-
.../providers/openai/openaiEmbedding.ts | 41 +++++
src/lib/models/providers/openai/openaiLLM.ts | 163 ++++++++++++++++++
8 files changed, 417 insertions(+), 88 deletions(-)
delete mode 100644 src/lib/models/providers/baseProvider.ts
rename src/lib/models/providers/{ollama.ts => ollama/index.ts} (83%)
create mode 100644 src/lib/models/providers/ollama/ollamaEmbedding.ts
create mode 100644 src/lib/models/providers/ollama/ollamaLLM.ts
rename src/lib/models/providers/{openai.ts => openai/index.ts} (87%)
create mode 100644 src/lib/models/providers/openai/openaiEmbedding.ts
create mode 100644 src/lib/models/providers/openai/openaiLLM.ts
diff --git a/src/lib/models/providers/baseProvider.ts b/src/lib/models/providers/baseProvider.ts
deleted file mode 100644
index 980a2b2..0000000
--- a/src/lib/models/providers/baseProvider.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Embeddings } from '@langchain/core/embeddings';
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Model, ModelList, ProviderMetadata } from '../types';
-import { UIConfigField } from '@/lib/config/types';
-
-abstract class BaseModelProvider {
- constructor(
- protected id: string,
- protected name: string,
- protected config: CONFIG,
- ) {}
- abstract getDefaultModels(): Promise;
- abstract getModelList(): Promise;
- abstract loadChatModel(modelName: string): Promise;
- abstract loadEmbeddingModel(modelName: string): Promise;
- static getProviderConfigFields(): UIConfigField[] {
- throw new Error('Method not implemented.');
- }
- static getProviderMetadata(): ProviderMetadata {
- throw new Error('Method not Implemented.');
- }
- static parseAndValidate(raw: any): any {
- /* Static methods can't access class type parameters */
- throw new Error('Method not Implemented.');
- }
-}
-
-export type ProviderConstructor = {
- new (id: string, name: string, config: CONFIG): BaseModelProvider;
- parseAndValidate(raw: any): CONFIG;
- getProviderConfigFields: () => UIConfigField[];
- getProviderMetadata: () => ProviderMetadata;
-};
-
-export const createProviderInstance = >(
- Provider: P,
- id: string,
- name: string,
- rawConfig: unknown,
-): InstanceType
=> {
- const cfg = Provider.parseAndValidate(rawConfig);
- return new Provider(id, name, cfg) as InstanceType
;
-};
-
-export default BaseModelProvider;
diff --git a/src/lib/models/providers/index.ts b/src/lib/models/providers/index.ts
index addca61..6e508e1 100644
--- a/src/lib/models/providers/index.ts
+++ b/src/lib/models/providers/index.ts
@@ -1,27 +1,11 @@
import { ModelProviderUISection } from '@/lib/config/types';
-import { ProviderConstructor } from './baseProvider';
+import { ProviderConstructor } from '../base/provider';
import OpenAIProvider from './openai';
import OllamaProvider from './ollama';
-import TransformersProvider from './transformers';
-import AnthropicProvider from './anthropic';
-import GeminiProvider from './gemini';
-import GroqProvider from './groq';
-import DeepSeekProvider from './deepseek';
-import LMStudioProvider from './lmstudio';
-import LemonadeProvider from './lemonade';
-import AimlProvider from '@/lib/models/providers/aiml';
export const providers: Record> = {
openai: OpenAIProvider,
ollama: OllamaProvider,
- transformers: TransformersProvider,
- anthropic: AnthropicProvider,
- gemini: GeminiProvider,
- groq: GroqProvider,
- deepseek: DeepSeekProvider,
- aiml: AimlProvider,
- lmstudio: LMStudioProvider,
- lemonade: LemonadeProvider,
};
export const getModelProvidersUIConfigSection =
diff --git a/src/lib/models/providers/ollama.ts b/src/lib/models/providers/ollama/index.ts
similarity index 83%
rename from src/lib/models/providers/ollama.ts
rename to src/lib/models/providers/ollama/index.ts
index 9ae5899..762c2bf 100644
--- a/src/lib/models/providers/ollama.ts
+++ b/src/lib/models/providers/ollama/index.ts
@@ -1,10 +1,11 @@
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Model, ModelList, ProviderMetadata } from '../types';
-import BaseModelProvider from './baseProvider';
-import { ChatOllama, OllamaEmbeddings } from '@langchain/ollama';
-import { Embeddings } from '@langchain/core/embeddings';
import { UIConfigField } from '@/lib/config/types';
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
+import BaseModelProvider from '../../base/provider';
+import { Model, ModelList, ProviderMetadata } from '../../types';
+import BaseLLM from '../../base/llm';
+import BaseEmbedding from '../../base/embedding';
+import OllamaLLM from './ollamaLLM';
+import OllamaEmbedding from './ollamaEmbedding';
interface OllamaConfig {
baseURL: string;
@@ -76,7 +77,7 @@ class OllamaProvider extends BaseModelProvider {
};
}
- async loadChatModel(key: string): Promise {
+ async loadChatModel(key: string): Promise> {
const modelList = await this.getModelList();
const exists = modelList.chat.find((m) => m.key === key);
@@ -87,14 +88,13 @@ class OllamaProvider extends BaseModelProvider {
);
}
- return new ChatOllama({
- temperature: 0.7,
+ return new OllamaLLM({
+ baseURL: this.config.baseURL,
model: key,
- baseUrl: this.config.baseURL,
});
}
- async loadEmbeddingModel(key: string): Promise {
+ async loadEmbeddingModel(key: string): Promise> {
const modelList = await this.getModelList();
const exists = modelList.embedding.find((m) => m.key === key);
@@ -104,9 +104,9 @@ class OllamaProvider extends BaseModelProvider {
);
}
- return new OllamaEmbeddings({
+ return new OllamaEmbedding({
model: key,
- baseUrl: this.config.baseURL,
+ baseURL: this.config.baseURL,
});
}
diff --git a/src/lib/models/providers/ollama/ollamaEmbedding.ts b/src/lib/models/providers/ollama/ollamaEmbedding.ts
new file mode 100644
index 0000000..0fd306a
--- /dev/null
+++ b/src/lib/models/providers/ollama/ollamaEmbedding.ts
@@ -0,0 +1,39 @@
+import { Ollama } from 'ollama';
+import BaseEmbedding from '../../base/embedding';
+
+type OllamaConfig = {
+ model: string;
+ baseURL?: string;
+};
+
+class OllamaEmbedding extends BaseEmbedding {
+ ollamaClient: Ollama;
+
+ constructor(protected config: OllamaConfig) {
+ super(config);
+
+ this.ollamaClient = new Ollama({
+ host: this.config.baseURL || 'http://localhost:11434',
+ });
+ }
+
+ async embedText(texts: string[]): Promise {
+ const response = await this.ollamaClient.embed({
+ input: texts,
+ model: this.config.model,
+ });
+
+ return response.embeddings;
+ }
+
+ async embedChunks(chunks: Chunk[]): Promise {
+ const response = await this.ollamaClient.embed({
+ input: chunks.map((c) => c.content),
+ model: this.config.model,
+ });
+
+ return response.embeddings;
+ }
+}
+
+export default OllamaEmbedding;
diff --git a/src/lib/models/providers/ollama/ollamaLLM.ts b/src/lib/models/providers/ollama/ollamaLLM.ts
new file mode 100644
index 0000000..fd12b77
--- /dev/null
+++ b/src/lib/models/providers/ollama/ollamaLLM.ts
@@ -0,0 +1,149 @@
+import z from 'zod';
+import BaseLLM from '../../base/llm';
+import {
+ GenerateObjectInput,
+ GenerateOptions,
+ GenerateTextInput,
+ GenerateTextOutput,
+ StreamTextOutput,
+} from '../../types';
+import { Ollama } from 'ollama';
+import { parse } from 'partial-json';
+
+type OllamaConfig = {
+ baseURL: string;
+ model: string;
+ options?: GenerateOptions;
+};
+
+class OllamaLLM extends BaseLLM {
+ ollamaClient: Ollama;
+
+ constructor(protected config: OllamaConfig) {
+ super(config);
+
+ this.ollamaClient = new Ollama({
+ host: this.config.baseURL || 'http://localhost:11434',
+ });
+ }
+
+ withOptions(options: GenerateOptions) {
+ this.config.options = {
+ ...this.config.options,
+ ...options,
+ };
+ return this;
+ }
+
+ async generateText(input: GenerateTextInput): Promise {
+ this.withOptions(input.options || {});
+
+ const res = await this.ollamaClient.chat({
+ model: this.config.model,
+ messages: input.messages,
+ options: {
+ top_p: this.config.options?.topP,
+ temperature: this.config.options?.temperature,
+ num_predict: this.config.options?.maxTokens,
+ frequency_penalty: this.config.options?.frequencyPenalty,
+ presence_penalty: this.config.options?.presencePenalty,
+ stop: this.config.options?.stopSequences,
+ },
+ });
+
+ return {
+ content: res.message.content,
+ additionalInfo: {
+ reasoning: res.message.thinking,
+ },
+ };
+ }
+
+ async *streamText(
+ input: GenerateTextInput,
+ ): AsyncGenerator {
+ this.withOptions(input.options || {});
+
+ const stream = await this.ollamaClient.chat({
+ model: this.config.model,
+ messages: input.messages,
+ stream: true,
+ options: {
+ top_p: this.config.options?.topP,
+ temperature: this.config.options?.temperature,
+ num_predict: this.config.options?.maxTokens,
+ frequency_penalty: this.config.options?.frequencyPenalty,
+ presence_penalty: this.config.options?.presencePenalty,
+ stop: this.config.options?.stopSequences,
+ },
+ });
+
+ for await (const chunk of stream) {
+ yield {
+ contentChunk: chunk.message.content,
+ done: chunk.done,
+ additionalInfo: {
+ reasoning: chunk.message.thinking,
+ },
+ };
+ }
+ }
+
+ async generateObject(input: GenerateObjectInput): Promise {
+ this.withOptions(input.options || {});
+
+ const response = await this.ollamaClient.chat({
+ model: this.config.model,
+ messages: input.messages,
+ format: z.toJSONSchema(input.schema),
+ options: {
+ top_p: this.config.options?.topP,
+ temperature: this.config.options?.temperature,
+ num_predict: this.config.options?.maxTokens,
+ frequency_penalty: this.config.options?.frequencyPenalty,
+ presence_penalty: this.config.options?.presencePenalty,
+ stop: this.config.options?.stopSequences,
+ },
+ });
+
+ try {
+ return input.schema.parse(JSON.parse(response.message.content)) as T;
+ } catch (err) {
+ throw new Error(`Error parsing response from Ollama: ${err}`);
+ }
+ }
+
+ async *streamObject(input: GenerateObjectInput): AsyncGenerator {
+ let recievedObj: string = '';
+
+ this.withOptions(input.options || {});
+
+ const stream = await this.ollamaClient.chat({
+ model: this.config.model,
+ messages: input.messages,
+ format: z.toJSONSchema(input.schema),
+ stream: true,
+ options: {
+ top_p: this.config.options?.topP,
+ temperature: this.config.options?.temperature,
+ num_predict: this.config.options?.maxTokens,
+ frequency_penalty: this.config.options?.frequencyPenalty,
+ presence_penalty: this.config.options?.presencePenalty,
+ stop: this.config.options?.stopSequences,
+ },
+ });
+
+ for await (const chunk of stream) {
+ recievedObj += chunk.message.content;
+
+ try {
+ yield parse(recievedObj) as T;
+ } catch (err) {
+ console.log('Error parsing partial object from Ollama:', err);
+ yield {} as T;
+ }
+ }
+ }
+}
+
+export default OllamaLLM;
diff --git a/src/lib/models/providers/openai.ts b/src/lib/models/providers/openai/index.ts
similarity index 87%
rename from src/lib/models/providers/openai.ts
rename to src/lib/models/providers/openai/index.ts
index 6055b34..8b5eacb 100644
--- a/src/lib/models/providers/openai.ts
+++ b/src/lib/models/providers/openai/index.ts
@@ -1,10 +1,13 @@
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Model, ModelList, ProviderMetadata } from '../types';
-import BaseModelProvider from './baseProvider';
-import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
import { Embeddings } from '@langchain/core/embeddings';
import { UIConfigField } from '@/lib/config/types';
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
+import { Model, ModelList, ProviderMetadata } from '../../types';
+import OpenAIEmbedding from './openaiEmbedding';
+import BaseEmbedding from '../../base/embedding';
+import BaseModelProvider from '../../base/provider';
+import BaseLLM from '../../base/llm';
+import OpenAILLM from './openaiLLM';
interface OpenAIConfig {
apiKey: string;
@@ -145,7 +148,7 @@ class OpenAIProvider extends BaseModelProvider {
};
}
- async loadChatModel(key: string): Promise {
+ async loadChatModel(key: string): Promise> {
const modelList = await this.getModelList();
const exists = modelList.chat.find((m) => m.key === key);
@@ -156,17 +159,14 @@ class OpenAIProvider extends BaseModelProvider {
);
}
- return new ChatOpenAI({
+ return new OpenAILLM({
apiKey: this.config.apiKey,
- temperature: 0.7,
model: key,
- configuration: {
- baseURL: this.config.baseURL,
- },
+ baseURL: this.config.baseURL,
});
}
- async loadEmbeddingModel(key: string): Promise {
+ async loadEmbeddingModel(key: string): Promise> {
const modelList = await this.getModelList();
const exists = modelList.embedding.find((m) => m.key === key);
@@ -176,12 +176,10 @@ class OpenAIProvider extends BaseModelProvider {
);
}
- return new OpenAIEmbeddings({
+ return new OpenAIEmbedding({
apiKey: this.config.apiKey,
model: key,
- configuration: {
- baseURL: this.config.baseURL,
- },
+ baseURL: this.config.baseURL,
});
}
diff --git a/src/lib/models/providers/openai/openaiEmbedding.ts b/src/lib/models/providers/openai/openaiEmbedding.ts
new file mode 100644
index 0000000..ea15680
--- /dev/null
+++ b/src/lib/models/providers/openai/openaiEmbedding.ts
@@ -0,0 +1,41 @@
+import OpenAI from 'openai';
+import BaseEmbedding from '../../base/embedding';
+
+type OpenAIConfig = {
+ apiKey: string;
+ model: string;
+ baseURL?: string;
+};
+
+class OpenAIEmbedding extends BaseEmbedding {
+ openAIClient: OpenAI;
+
+ constructor(protected config: OpenAIConfig) {
+ super(config);
+
+ this.openAIClient = new OpenAI({
+ apiKey: config.apiKey,
+ baseURL: config.baseURL,
+ });
+ }
+
+ async embedText(texts: string[]): Promise {
+ const response = await this.openAIClient.embeddings.create({
+ model: this.config.model,
+ input: texts,
+ });
+
+ return response.data.map((embedding) => embedding.embedding);
+ }
+
+ async embedChunks(chunks: Chunk[]): Promise {
+ const response = await this.openAIClient.embeddings.create({
+ model: this.config.model,
+ input: chunks.map((c) => c.content),
+ });
+
+ return response.data.map((embedding) => embedding.embedding);
+ }
+}
+
+export default OpenAIEmbedding;
diff --git a/src/lib/models/providers/openai/openaiLLM.ts b/src/lib/models/providers/openai/openaiLLM.ts
new file mode 100644
index 0000000..95594e6
--- /dev/null
+++ b/src/lib/models/providers/openai/openaiLLM.ts
@@ -0,0 +1,163 @@
+import OpenAI from 'openai';
+import BaseLLM from '../../base/llm';
+import { zodTextFormat, zodResponseFormat } from 'openai/helpers/zod';
+import {
+ GenerateObjectInput,
+ GenerateOptions,
+ GenerateTextInput,
+ GenerateTextOutput,
+ StreamTextOutput,
+} from '../../types';
+import { parse } from 'partial-json';
+
+type OpenAIConfig = {
+ apiKey: string;
+ model: string;
+ baseURL?: string;
+ options?: GenerateOptions;
+};
+
+class OpenAILLM extends BaseLLM {
+ openAIClient: OpenAI;
+
+ constructor(protected config: OpenAIConfig) {
+ super(config);
+
+ this.openAIClient = new OpenAI({
+ apiKey: this.config.apiKey,
+ baseURL: this.config.baseURL || 'https://api.openai.com/v1',
+ });
+ }
+
+ withOptions(options: GenerateOptions) {
+ this.config.options = {
+ ...this.config.options,
+ ...options,
+ };
+
+ return this;
+ }
+
+ async generateText(input: GenerateTextInput): Promise {
+ this.withOptions(input.options || {});
+
+ const response = await this.openAIClient.chat.completions.create({
+ model: this.config.model,
+ messages: input.messages,
+ temperature: this.config.options?.temperature || 1.0,
+ top_p: this.config.options?.topP,
+ max_completion_tokens: this.config.options?.maxTokens,
+ stop: this.config.options?.stopSequences,
+ frequency_penalty: this.config.options?.frequencyPenalty,
+ presence_penalty: this.config.options?.presencePenalty,
+ });
+
+ if (response.choices && response.choices.length > 0) {
+ return {
+ content: response.choices[0].message.content!,
+ additionalInfo: {
+ finishReason: response.choices[0].finish_reason,
+ },
+ };
+ }
+
+ throw new Error('No response from OpenAI');
+ }
+
+ async *streamText(
+ input: GenerateTextInput,
+ ): AsyncGenerator {
+ this.withOptions(input.options || {});
+
+ const stream = await this.openAIClient.chat.completions.create({
+ model: this.config.model,
+ messages: input.messages,
+ temperature: this.config.options?.temperature || 1.0,
+ top_p: this.config.options?.topP,
+ max_completion_tokens: this.config.options?.maxTokens,
+ stop: this.config.options?.stopSequences,
+ frequency_penalty: this.config.options?.frequencyPenalty,
+ presence_penalty: this.config.options?.presencePenalty,
+ stream: true,
+ });
+
+ for await (const chunk of stream) {
+ if (chunk.choices && chunk.choices.length > 0) {
+ yield {
+ contentChunk: chunk.choices[0].delta.content || '',
+ done: chunk.choices[0].finish_reason !== null,
+ additionalInfo: {
+ finishReason: chunk.choices[0].finish_reason,
+ },
+ };
+ }
+ }
+ }
+
+ async generateObject(input: GenerateObjectInput): Promise {
+ this.withOptions(input.options || {});
+
+ const response = await this.openAIClient.chat.completions.parse({
+ messages: input.messages,
+ model: this.config.model,
+ temperature: this.config.options?.temperature || 1.0,
+ top_p: this.config.options?.topP,
+ max_completion_tokens: this.config.options?.maxTokens,
+ stop: this.config.options?.stopSequences,
+ frequency_penalty: this.config.options?.frequencyPenalty,
+ presence_penalty: this.config.options?.presencePenalty,
+ response_format: zodResponseFormat(input.schema, 'object'),
+ });
+
+ if (response.choices && response.choices.length > 0) {
+ try {
+ return input.schema.parse(response.choices[0].message.parsed) as T;
+ } catch (err) {
+ throw new Error(`Error parsing response from OpenAI: ${err}`);
+ }
+ }
+
+ throw new Error('No response from OpenAI');
+ }
+
+ async *streamObject(input: GenerateObjectInput): AsyncGenerator {
+ let recievedObj: string = '';
+
+ this.withOptions(input.options || {});
+
+ const stream = this.openAIClient.responses.stream({
+ model: this.config.model,
+ input: input.messages,
+ temperature: this.config.options?.temperature || 1.0,
+ top_p: this.config.options?.topP,
+ max_completion_tokens: this.config.options?.maxTokens,
+ stop: this.config.options?.stopSequences,
+ frequency_penalty: this.config.options?.frequencyPenalty,
+ presence_penalty: this.config.options?.presencePenalty,
+ text: {
+ format: zodTextFormat(input.schema, 'object'),
+ },
+ });
+
+ for await (const chunk of stream) {
+ if (chunk.type === 'response.output_text.delta' && chunk.delta) {
+ recievedObj += chunk.delta;
+
+ try {
+ yield parse(recievedObj) as T;
+ } catch (err) {
+ console.log('Error parsing partial object from OpenAI:', err);
+ yield {} as T;
+ }
+ } else if (chunk.type === 'response.output_text.done' && chunk.text) {
+ try {
+ yield parse(chunk.text) as T;
+ } catch (err) {
+ throw new Error(`Error parsing response from OpenAI: ${err}`);
+ }
+ }
+ }
+ }
+}
+
+export default OpenAILLM;
From f44ad973aa563d8ba13956cff16ae239129f4480 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Tue, 18 Nov 2025 14:39:43 +0530
Subject: [PATCH 014/199] feat(types): add llm types
---
src/lib/models/types.ts | 51 +++++++++++++++++++++++++++++++++++++++++
src/lib/types.ts | 12 +++++-----
2 files changed, 57 insertions(+), 6 deletions(-)
diff --git a/src/lib/models/types.ts b/src/lib/models/types.ts
index fdd5df2..ce77d3b 100644
--- a/src/lib/models/types.ts
+++ b/src/lib/models/types.ts
@@ -1,3 +1,5 @@
+import z from 'zod';
+
type Model = {
name: string;
key: string;
@@ -25,10 +27,59 @@ type ModelWithProvider = {
providerId: string;
};
+type GenerateOptions = {
+ temperature?: number;
+ maxTokens?: number;
+ topP?: number;
+ stopSequences?: string[];
+ frequencyPenalty?: number;
+ presencePenalty?: number;
+};
+
+type GenerateTextInput = {
+ messages: Message[];
+ options?: GenerateOptions;
+};
+
+type GenerateTextOutput = {
+ content: string;
+ additionalInfo?: Record;
+};
+
+type StreamTextOutput = {
+ contentChunk: string;
+ additionalInfo?: Record;
+ done?: boolean;
+};
+
+type GenerateObjectInput = {
+ schema: z.ZodTypeAny;
+ messages: Message[];
+ options?: GenerateOptions;
+};
+
+type GenerateObjectOutput = {
+ object: T;
+ additionalInfo?: Record;
+};
+
+type StreamObjectOutput = {
+ objectChunk: Partial;
+ additionalInfo?: Record;
+ done?: boolean;
+};
+
export type {
Model,
ModelList,
ProviderMetadata,
MinimalProvider,
ModelWithProvider,
+ GenerateOptions,
+ GenerateTextInput,
+ GenerateTextOutput,
+ StreamTextOutput,
+ GenerateObjectInput,
+ GenerateObjectOutput,
+ StreamObjectOutput,
};
diff --git a/src/lib/types.ts b/src/lib/types.ts
index f7a9ac9..3dcb0d6 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -1,9 +1,9 @@
type Message = {
- role: 'user' | 'assistant' | 'system';
- content: string;
-}
+ role: 'user' | 'assistant' | 'system';
+ content: string;
+};
type Chunk = {
- content: string;
- metadata: Record;
-}
\ No newline at end of file
+ content: string;
+ metadata: Record;
+};
From c3830795cb800329a898f201e67cdc5e2c9b9487 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Thu, 20 Nov 2025 14:51:17 +0530
Subject: [PATCH 015/199] feat(app): add new session manager
---
src/lib/session.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++
src/lib/types.ts | 6 ++++++
2 files changed, 51 insertions(+)
create mode 100644 src/lib/session.ts
diff --git a/src/lib/session.ts b/src/lib/session.ts
new file mode 100644
index 0000000..6c5e18e
--- /dev/null
+++ b/src/lib/session.ts
@@ -0,0 +1,45 @@
+import { EventEmitter } from 'stream';
+/* todo implement history saving and better artifact typing and handling */
+class SessionManager {
+ private static sessions = new Map();
+ readonly id: string;
+ private artifacts = new Map();
+ private emitter = new EventEmitter();
+
+ constructor() {
+ this.id = crypto.randomUUID();
+ }
+
+ static getSession(id: string): SessionManager | undefined {
+ return this.sessions.get(id);
+ }
+
+ static getAllSessions(): SessionManager[] {
+ return Array.from(this.sessions.values());
+ }
+
+ emit(event: string, data: any) {
+ this.emitter.emit(event, data);
+ }
+
+ emitArtifact(artifact: Artifact) {
+ this.artifacts.set(artifact.id, artifact);
+ this.emitter.emit('addArtifact', artifact);
+ }
+
+ appendToArtifact(artifactId: string, data: any) {
+ const artifact = this.artifacts.get(artifactId);
+ if (artifact) {
+ if (typeof artifact.data === 'string') {
+ artifact.data += data;
+ } else if (Array.isArray(artifact.data)) {
+ artifact.data.push(data);
+ } else if (typeof artifact.data === 'object') {
+ Object.assign(artifact.data, data);
+ }
+ this.emitter.emit('updateArtifact', artifact);
+ }
+ }
+}
+
+export default SessionManager;
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 3dcb0d6..a96c7ca 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -7,3 +7,9 @@ type Chunk = {
content: string;
metadata: Record;
};
+
+type Artifact = {
+ id: string;
+ type: string;
+ data: any;
+};
From 3cc8882b28ef1430b59b7383e4cc7ffe862d39a7 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Thu, 20 Nov 2025 14:51:49 +0530
Subject: [PATCH 016/199] feat(prompts): add classifier prompt
---
src/lib/prompts/search/classifier.ts | 176 +++++++++++++++++++++++++++
1 file changed, 176 insertions(+)
create mode 100644 src/lib/prompts/search/classifier.ts
diff --git a/src/lib/prompts/search/classifier.ts b/src/lib/prompts/search/classifier.ts
new file mode 100644
index 0000000..40e3203
--- /dev/null
+++ b/src/lib/prompts/search/classifier.ts
@@ -0,0 +1,176 @@
+export const getClassifierPrompt = (input: {
+ intentDesc: string;
+ widgetDesc: string;
+}) => {
+ return `
+
+You are an expert query classifier for an intelligent search agent. Your task is to analyze user queries and determine the optimal way to answer them—selecting the right intent(s) and widgets.
+
+
+
+Given a conversation history and follow-up question, you must:
+1. Determine if search should be skipped (skipSearch: boolean)
+2. Generate a standalone, self-contained version of the question (standaloneFollowUp: string)
+3. Identify the intent(s) that describe how to fulfill the query (intent: array)
+4. Select appropriate widgets (widgets: array)
+
+
+
+**THE MOST IMPORTANT RULE**: skipSearch should be TRUE only in TWO cases:
+1. Widget-only queries (weather, stocks, calculator)
+2. Greetings or simple writing tasks (NOT questions)
+
+**DEFAULT TO skipSearch: false** for everything else, including:
+- Any question ("what is", "how does", "explain", "tell me about")
+- Any request for information or facts
+- Anything you're unsure about
+
+Ask yourself: "Is the user ASKING about something or requesting INFORMATION?"
+- YES → skipSearch: false (use web_search)
+- NO (just greeting or simple writing) → skipSearch: true
+
+
+
+Follow this decision tree IN ORDER:
+
+1. **Widget-Only Queries** → skipSearch: TRUE, intent: ['widget_response']
+ - Weather queries: "weather in NYC", "temperature in Paris", "is it raining in Seattle"
+ - Stock queries: "AAPL stock price", "how is Tesla doing", "MSFT stock"
+ - Calculator queries: "what is 25% of 80", "calculate 15*23", "sqrt(144)"
+ - These are COMPLETE answers—no search needed
+
+2. **Writing/Greeting Tasks** → skipSearch: TRUE, intent: ['writing_task']
+ - ONLY for greetings and simple writing:
+ - Greetings: "hello", "hi", "how are you", "thanks", "goodbye"
+ - Simple writing needing NO facts: "write a thank you email", "draft a birthday message", "compose a poem"
+ - NEVER for: questions, "what is X", "how does X work", explanations, definitions, facts, code help
+ - If user is ASKING about something (not requesting writing), use web_search
+
+3. **Image Display Queries** → skipSearch: FALSE, intent: ['image_preview']
+ - "Show me images of cats"
+ - "Pictures of the Eiffel Tower"
+ - "Visual examples of modern architecture"
+ - Requests for images to visualize something
+
+4. **Widget + Additional Info** → skipSearch: FALSE, intent: ['web_search', 'widget_response']
+ - "weather in NYC and best things to do there"
+ - "AAPL stock and recent Apple news"
+ - "calculate my mortgage and explain how interest works"
+
+5. **Pure Search Queries** → skipSearch: FALSE
+ - Default to web_search for general questions
+ - Use discussions_search when user explicitly mentions Reddit, forums, opinions, experiences
+ - Use academic_search when user explicitly mentions research, papers, studies, scientific
+ - Can combine multiple search intents when appropriate
+
+6. **Fallback when web_search unavailable** → skipSearch: TRUE, intent: ['writing_task'] or []
+ - If no search intents are available and no widgets apply
+ - Set skipSearch to true and use writing_task or empty intent
+
+
+
+Example 1: Widget-only query
+Query: "What is the weather in New York?"
+Reasoning: User wants current weather → weather widget provides this completely
+Output: skipSearch: true, intent: ['widget_response'], widgets: [weather widget for New York]
+
+Example 2: Widget-only query
+Query: "AAPL stock price"
+Reasoning: User wants stock price → stock_ticker widget provides this completely
+Output: skipSearch: true, intent: ['widget_response'], widgets: [stock_ticker for AAPL]
+
+Example 3: Widget + search query
+Query: "What's the weather in NYC and what are some good outdoor activities?"
+Reasoning: Weather widget handles weather, but outdoor activities need web search
+Output: skipSearch: false, intent: ['web_search', 'widget_response'], widgets: [weather widget for NYC]
+
+Example 4: Pure search query
+Query: "What are the latest developments in AI?"
+Reasoning: No widget applies, needs current web information
+Output: skipSearch: false, intent: ['web_search'], widgets: []
+
+Example 5: Writing task (greeting/simple writing only)
+Query: "Write me a thank you email for a job interview"
+Reasoning: Simple writing task needing no external facts → writing_task
+Output: skipSearch: true, intent: ['writing_task'], widgets: []
+
+Example 5b: Question about something - ALWAYS needs search
+Query: "What is Kimi K2?"
+Reasoning: User is ASKING about something → needs web search for accurate info
+Output: skipSearch: false, intent: ['web_search'], widgets: []
+
+Example 5c: Another question - needs search
+Query: "Explain how photosynthesis works"
+Reasoning: User is ASKING for explanation → needs web search
+Output: skipSearch: false, intent: ['web_search'], widgets: []
+
+Example 6: Image display
+Query: "Show me images of cats"
+Reasoning: User wants to see images → requires image search
+Output: skipSearch: false, intent: ['image_preview'], widgets: []
+
+Example 7: Multiple search sources
+Query: "What does the research say about meditation benefits?"
+Reasoning: Benefits from both academic papers and web articles
+Output: skipSearch: false, intent: ['academic_search', 'web_search'], widgets: []
+
+Example 8: Discussions search
+Query: "What do people on Reddit think about the new iPhone?"
+Reasoning: User explicitly wants forum/community opinions → discussions_search
+Output: skipSearch: false, intent: ['discussions_search'], widgets: []
+
+Example 9: Academic search only
+Query: "Find scientific papers on climate change effects"
+Reasoning: User explicitly wants academic/research papers
+Output: skipSearch: false, intent: ['academic_search'], widgets: []
+
+
+
+Transform the follow-up into a self-contained question:
+- Include ALL necessary context from chat history
+- Replace pronouns (it, they, this, that) with specific nouns
+- Replace references ("the previous one", "what you mentioned") with actual content
+- Preserve the original complexity—don't over-elaborate simple questions
+- The question should be answerable without seeing the conversation
+
+
+
+Available intents:
+${input.intentDesc}
+
+Rules:
+- Include at least one intent when applicable
+- For questions/information requests:
+ - Default to web_search unless user explicitly requests another source
+ - Use discussions_search when user mentions: Reddit, forums, opinions, experiences, "what do people think"
+ - Use academic_search when user mentions: research, papers, studies, scientific, scholarly
+ - Can combine intents (e.g., ['academic_search', 'web_search'])
+- If web_search is NOT in available intents and query needs search:
+ - Check if discussions_search or academic_search applies
+ - If no search intent available and no widgets: use writing_task or empty array []
+- private_search: ONLY when user provides specific URLs/documents
+- widget_response: when widgets fully answer the query
+- writing_task: ONLY for greetings and simple writing (never for questions)
+
+
+
+Available widgets:
+${input.widgetDesc}
+
+Rules:
+- Include ALL applicable widgets regardless of skipSearch value
+- Each widget type can only be included once
+- Widgets provide structured, real-time data that enhances any response
+
+
+
+Your classification must be precise and consistent:
+{
+ "skipSearch": ,
+ "standaloneFollowUp": "",
+ "intent": [],
+ "widgets": []
+}
+
+ `;
+};
From afc68ca91f619d3e445d86cd58f55bf54efce171 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Thu, 20 Nov 2025 14:52:24 +0530
Subject: [PATCH 017/199] feat(ollamaLLM): disable thinking in obj mode
---
src/lib/models/providers/ollama/ollamaLLM.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/lib/models/providers/ollama/ollamaLLM.ts b/src/lib/models/providers/ollama/ollamaLLM.ts
index fd12b77..0e64d7b 100644
--- a/src/lib/models/providers/ollama/ollamaLLM.ts
+++ b/src/lib/models/providers/ollama/ollamaLLM.ts
@@ -96,9 +96,10 @@ class OllamaLLM extends BaseLLM {
model: this.config.model,
messages: input.messages,
format: z.toJSONSchema(input.schema),
+ think: false,
options: {
top_p: this.config.options?.topP,
- temperature: this.config.options?.temperature,
+ temperature: 0,
num_predict: this.config.options?.maxTokens,
frequency_penalty: this.config.options?.frequencyPenalty,
presence_penalty: this.config.options?.presencePenalty,
@@ -123,6 +124,7 @@ class OllamaLLM extends BaseLLM {
messages: input.messages,
format: z.toJSONSchema(input.schema),
stream: true,
+ think: false,
options: {
top_p: this.config.options?.topP,
temperature: this.config.options?.temperature,
From cbcb03c7acafd2d05cd2f1873396d03dc6db1915 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Thu, 20 Nov 2025 14:52:41 +0530
Subject: [PATCH 018/199] feat(llm): update return type to partial
---
src/lib/models/base/llm.ts | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/src/lib/models/base/llm.ts b/src/lib/models/base/llm.ts
index 5d6f52d..e701fa9 100644
--- a/src/lib/models/base/llm.ts
+++ b/src/lib/models/base/llm.ts
@@ -1,10 +1,8 @@
import {
GenerateObjectInput,
- GenerateObjectOutput,
GenerateOptions,
GenerateTextInput,
GenerateTextOutput,
- StreamObjectOutput,
StreamTextOutput,
} from '../types';
@@ -15,12 +13,10 @@ abstract class BaseLLM {
abstract streamText(
input: GenerateTextInput,
): AsyncGenerator;
- abstract generateObject(
- input: GenerateObjectInput,
- ): Promise>;
+ abstract generateObject(input: GenerateObjectInput): Promise;
abstract streamObject(
input: GenerateObjectInput,
- ): AsyncGenerator>;
+ ): AsyncGenerator>;
}
export default BaseLLM;
From 8b515201f341a3aba53f2f3bcbc7bc3a58b14303 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Thu, 20 Nov 2025 14:53:03 +0530
Subject: [PATCH 019/199] feat(app): add search types
---
src/lib/agents/search/types.ts | 64 ++++++++++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
create mode 100644 src/lib/agents/search/types.ts
diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts
new file mode 100644
index 0000000..c65d940
--- /dev/null
+++ b/src/lib/agents/search/types.ts
@@ -0,0 +1,64 @@
+import { EventEmitter } from 'stream';
+import z from 'zod';
+import BaseLLM from '../../models/base/llm';
+import BaseEmbedding from '@/lib/models/base/embedding';
+
+export type SearchSources = 'web' | 'discussions' | 'academic';
+
+export type SearchAgentConfig = {
+ sources: SearchSources[];
+ llm: BaseLLM;
+ embedding: BaseEmbedding;
+};
+
+export type SearchAgentInput = {
+ chatHistory: Message[];
+ followUp: string;
+ config: SearchAgentConfig;
+};
+
+export interface Intent {
+ name: string;
+ description: string;
+ requiresSearch: boolean;
+ enabled: (config: { sources: SearchSources[] }) => boolean;
+}
+
+export type Widget = z.ZodObject> = {
+ name: string;
+ description: string;
+ schema: TSchema;
+ execute: (
+ params: z.infer,
+ additionalConfig: AdditionalConfig,
+ ) => Promise;
+};
+
+export type WidgetConfig = {
+ type: string;
+ params: Record;
+};
+
+export type WidgetOutput = {
+ type: string;
+ data: any;
+};
+
+export type ClassifierInput = {
+ llm: BaseLLM;
+ enabledSources: SearchSources[];
+ query: string;
+ chatHistory: Message[];
+};
+
+export type ClassifierOutput = {
+ skipSearch: boolean;
+ intents: string[];
+ widgets: WidgetConfig[];
+};
+
+export type AdditionalConfig = {
+ llm: BaseLLM;
+ embedding: BaseLLM;
+ emitter: EventEmitter;
+};
From 036b44611f054d20240a3ca5198b2afc12f710fa Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Thu, 20 Nov 2025 14:55:24 +0530
Subject: [PATCH 020/199] feat(search): add classifier
---
src/lib/agents/search/classifier/index.ts | 72 +++++++++++++++++++
.../classifier/intents/academicSearch.ts | 11 +++
.../classifier/intents/discussionSearch.ts | 11 +++
.../agents/search/classifier/intents/index.ts | 14 ++++
.../search/classifier/intents/registry.ts | 29 ++++++++
.../search/classifier/intents/webSearch.ts | 11 +++
.../classifier/intents/widgetResponse.ts | 11 +++
.../search/classifier/intents/writingTask.ts | 11 +++
src/lib/agents/search/types.ts | 1 +
9 files changed, 171 insertions(+)
create mode 100644 src/lib/agents/search/classifier/index.ts
create mode 100644 src/lib/agents/search/classifier/intents/academicSearch.ts
create mode 100644 src/lib/agents/search/classifier/intents/discussionSearch.ts
create mode 100644 src/lib/agents/search/classifier/intents/index.ts
create mode 100644 src/lib/agents/search/classifier/intents/registry.ts
create mode 100644 src/lib/agents/search/classifier/intents/webSearch.ts
create mode 100644 src/lib/agents/search/classifier/intents/widgetResponse.ts
create mode 100644 src/lib/agents/search/classifier/intents/writingTask.ts
diff --git a/src/lib/agents/search/classifier/index.ts b/src/lib/agents/search/classifier/index.ts
new file mode 100644
index 0000000..9b2bf6b
--- /dev/null
+++ b/src/lib/agents/search/classifier/index.ts
@@ -0,0 +1,72 @@
+import z from 'zod';
+import { ClassifierInput, ClassifierOutput } from '../types';
+import { WidgetRegistry } from '../widgets';
+import { IntentRegistry } from './intents';
+import { getClassifierPrompt } from '@/lib/prompts/search/classifier';
+import formatChatHistoryAsString from '@/lib/utils/formatHistory';
+
+class Classifier {
+ async classify(input: ClassifierInput): Promise {
+ const availableIntents = IntentRegistry.getAvailableIntents({
+ sources: input.enabledSources,
+ });
+ const availableWidgets = WidgetRegistry.getAll();
+
+ const classificationSchema = z.object({
+ skipSearch: z
+ .boolean()
+ .describe(
+ 'Set to true to SKIP search. Skip ONLY when: (1) widgets alone fully answer the query (e.g., weather, stocks, calculator), (2) simple greetings or writing tasks (NOT questions). Set to false for ANY question or information request.',
+ ),
+ standaloneFollowUp: z
+ .string()
+ .describe(
+ 'A self-contained, context-independent reformulation of the user\'s question. Must include all necessary context from chat history, replace pronouns with specific nouns, and be clear enough to answer without seeing the conversation. Keep the same complexity as the original question.',
+ ),
+ intents: z
+ .array(z.enum(availableIntents.map((i) => i.name)))
+ .describe(
+ 'The intent(s) that best describe how to fulfill the user\'s query. Can include multiple intents (e.g., [\'web_search\', \'widget_response\'] for \'weather in NYC and recent news\'). Always include at least one intent when applicable.',
+ ),
+ widgets: z
+ .array(z.union(availableWidgets.map((w) => w.schema)))
+ .describe(
+ 'Widgets that can display structured data to answer (fully or partially) the query. Include all applicable widgets regardless of skipSearch value.',
+ ),
+ });
+
+ const classifierPrompt = getClassifierPrompt({
+ intentDesc: IntentRegistry.getDescriptions({
+ sources: input.enabledSources,
+ }),
+ widgetDesc: WidgetRegistry.getDescriptions(),
+ });
+
+ const res = await input.llm.generateObject<
+ z.infer
+ >({
+ messages: [
+ {
+ role: 'system',
+ content: classifierPrompt,
+ },
+ {
+ role: 'user',
+ content: `${formatChatHistoryAsString(input.chatHistory)} \n\n${input.query} `,
+ },
+ ],
+ schema: classificationSchema,
+ });
+
+ res.widgets = res.widgets.map((widgetConfig) => {
+ return {
+ type: widgetConfig.type,
+ params: widgetConfig,
+ };
+ });
+
+ return res as ClassifierOutput;
+ }
+}
+
+export default Classifier;
diff --git a/src/lib/agents/search/classifier/intents/academicSearch.ts b/src/lib/agents/search/classifier/intents/academicSearch.ts
new file mode 100644
index 0000000..b6da377
--- /dev/null
+++ b/src/lib/agents/search/classifier/intents/academicSearch.ts
@@ -0,0 +1,11 @@
+import { Intent } from '../../types';
+
+const academicSearchIntent: Intent = {
+ name: 'academic_search',
+ description:
+ 'Use this intent to find scholarly articles, research papers, and academic resources when the user is seeking credible and authoritative information on a specific topic.',
+ requiresSearch: true,
+ enabled: (config) => config.sources.includes('academic'),
+};
+
+export default academicSearchIntent;
diff --git a/src/lib/agents/search/classifier/intents/discussionSearch.ts b/src/lib/agents/search/classifier/intents/discussionSearch.ts
new file mode 100644
index 0000000..76b3b01
--- /dev/null
+++ b/src/lib/agents/search/classifier/intents/discussionSearch.ts
@@ -0,0 +1,11 @@
+import { Intent } from '../../types';
+
+const discussionSearchIntent: Intent = {
+ name: 'discussion_search',
+ description:
+ 'Use this intent to search through discussion forums, community boards, or social media platforms when the user is looking for opinions, experiences, or community-driven information on a specific topic.',
+ requiresSearch: true,
+ enabled: (config) => config.sources.includes('discussions'),
+};
+
+export default discussionSearchIntent;
diff --git a/src/lib/agents/search/classifier/intents/index.ts b/src/lib/agents/search/classifier/intents/index.ts
new file mode 100644
index 0000000..feefd2d
--- /dev/null
+++ b/src/lib/agents/search/classifier/intents/index.ts
@@ -0,0 +1,14 @@
+import academicSearchIntent from './academicSearch';
+import discussionSearchIntent from './discussionSearch';
+import IntentRegistry from './registry';
+import webSearchIntent from './webSearch';
+import widgetResponseIntent from './widgetResponse';
+import writingTaskIntent from './writingTask';
+
+IntentRegistry.register(webSearchIntent);
+IntentRegistry.register(academicSearchIntent);
+IntentRegistry.register(discussionSearchIntent);
+IntentRegistry.register(widgetResponseIntent);
+IntentRegistry.register(writingTaskIntent);
+
+export { IntentRegistry };
diff --git a/src/lib/agents/search/classifier/intents/registry.ts b/src/lib/agents/search/classifier/intents/registry.ts
new file mode 100644
index 0000000..bc3464b
--- /dev/null
+++ b/src/lib/agents/search/classifier/intents/registry.ts
@@ -0,0 +1,29 @@
+import { Intent, SearchAgentConfig, SearchSources } from '../../types';
+
+class IntentRegistry {
+ private static intents = new Map();
+
+ static register(intent: Intent) {
+ this.intents.set(intent.name, intent);
+ }
+
+ static get(name: string): Intent | undefined {
+ return this.intents.get(name);
+ }
+
+ static getAvailableIntents(config: { sources: SearchSources[] }): Intent[] {
+ return Array.from(
+ this.intents.values().filter((intent) => intent.enabled(config)),
+ );
+ }
+
+ static getDescriptions(config: { sources: SearchSources[] }): string {
+ const availableintnets = this.getAvailableIntents(config);
+
+ return availableintnets
+ .map((intent) => `${intent.name}: ${intent.description}`)
+ .join('\n\n');
+ }
+}
+
+export default IntentRegistry;
diff --git a/src/lib/agents/search/classifier/intents/webSearch.ts b/src/lib/agents/search/classifier/intents/webSearch.ts
new file mode 100644
index 0000000..9fccd2f
--- /dev/null
+++ b/src/lib/agents/search/classifier/intents/webSearch.ts
@@ -0,0 +1,11 @@
+import { Intent } from '../../types';
+
+const webSearchIntent: Intent = {
+ name: 'web_search',
+ description:
+ 'Use this intent to find current information from the web when the user is asking a question or needs up-to-date information that cannot be provided by widgets or other intents.',
+ requiresSearch: true,
+ enabled: (config) => config.sources.includes('web'),
+};
+
+export default webSearchIntent;
diff --git a/src/lib/agents/search/classifier/intents/widgetResponse.ts b/src/lib/agents/search/classifier/intents/widgetResponse.ts
new file mode 100644
index 0000000..0cfd58d
--- /dev/null
+++ b/src/lib/agents/search/classifier/intents/widgetResponse.ts
@@ -0,0 +1,11 @@
+import { Intent } from '../../types';
+
+const widgetResponseIntent: Intent = {
+ name: 'widget_response',
+ description:
+ 'Use this intent to respond to user queries using available widgets when the required information can be obtained from them.',
+ requiresSearch: false,
+ enabled: (config) => true,
+};
+
+export default widgetResponseIntent;
diff --git a/src/lib/agents/search/classifier/intents/writingTask.ts b/src/lib/agents/search/classifier/intents/writingTask.ts
new file mode 100644
index 0000000..95b5af6
--- /dev/null
+++ b/src/lib/agents/search/classifier/intents/writingTask.ts
@@ -0,0 +1,11 @@
+import { Intent } from '../../types';
+
+const writingTaskIntent: Intent = {
+ name: 'writing_task',
+ description:
+ 'Use this intent to assist users with writing tasks such as drafting emails, creating documents, or generating content based on their instructions or greetings.',
+ requiresSearch: false,
+ enabled: (config) => true,
+};
+
+export default writingTaskIntent;
diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts
index c65d940..72d7ecf 100644
--- a/src/lib/agents/search/types.ts
+++ b/src/lib/agents/search/types.ts
@@ -53,6 +53,7 @@ export type ClassifierInput = {
export type ClassifierOutput = {
skipSearch: boolean;
+ standaloneFollowUp: string;
intents: string[];
widgets: WidgetConfig[];
};
From 1614cfa5e54395610831c6e5f5df9ed9d3489838 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Thu, 20 Nov 2025 14:55:50 +0530
Subject: [PATCH 021/199] feat(app): add widgets
---
src/lib/agents/search/widgets/index.ts | 6 +
src/lib/agents/search/widgets/registry.ts | 65 +++++++++
.../agents/search/widgets/weatherWidget.ts | 123 ++++++++++++++++++
3 files changed, 194 insertions(+)
create mode 100644 src/lib/agents/search/widgets/index.ts
create mode 100644 src/lib/agents/search/widgets/registry.ts
create mode 100644 src/lib/agents/search/widgets/weatherWidget.ts
diff --git a/src/lib/agents/search/widgets/index.ts b/src/lib/agents/search/widgets/index.ts
new file mode 100644
index 0000000..7ddc597
--- /dev/null
+++ b/src/lib/agents/search/widgets/index.ts
@@ -0,0 +1,6 @@
+import WidgetRegistry from './registry';
+import weatherWidget from './weatherWidget';
+
+WidgetRegistry.register(weatherWidget);
+
+export { WidgetRegistry };
diff --git a/src/lib/agents/search/widgets/registry.ts b/src/lib/agents/search/widgets/registry.ts
new file mode 100644
index 0000000..d8ceaba
--- /dev/null
+++ b/src/lib/agents/search/widgets/registry.ts
@@ -0,0 +1,65 @@
+import {
+ AdditionalConfig,
+ SearchAgentConfig,
+ Widget,
+ WidgetConfig,
+ WidgetOutput,
+} from '../types';
+
+class WidgetRegistry {
+ private static widgets = new Map();
+
+ static register(widget: Widget) {
+ this.widgets.set(widget.name, widget);
+ }
+
+ static get(name: string): Widget | undefined {
+ return this.widgets.get(name);
+ }
+
+ static getAll(): Widget[] {
+ return Array.from(this.widgets.values());
+ }
+
+ static getDescriptions(): string {
+ return Array.from(this.widgets.values())
+ .map((widget) => `${widget.name}: ${widget.description}`)
+ .join('\n\n');
+ }
+
+ static async execute(
+ name: string,
+ params: any,
+ config: AdditionalConfig,
+ ): Promise {
+ const widget = this.get(name);
+
+ if (!widget) {
+ throw new Error(`Widget with name ${name} not found`);
+ }
+
+ return widget.execute(params, config);
+ }
+
+ static async executeAll(
+ widgets: WidgetConfig[],
+ additionalConfig: AdditionalConfig,
+ ): Promise {
+ const results: WidgetOutput[] = [];
+
+ await Promise.all(
+ widgets.map(async (widgetConfig) => {
+ const output = await this.execute(
+ widgetConfig.type,
+ widgetConfig.params,
+ additionalConfig,
+ );
+ results.push(output);
+ }),
+ );
+
+ return results;
+ }
+}
+
+export default WidgetRegistry;
diff --git a/src/lib/agents/search/widgets/weatherWidget.ts b/src/lib/agents/search/widgets/weatherWidget.ts
new file mode 100644
index 0000000..b9d048c
--- /dev/null
+++ b/src/lib/agents/search/widgets/weatherWidget.ts
@@ -0,0 +1,123 @@
+import z from 'zod';
+import { Widget } from '../types';
+
+const WeatherWidgetSchema = z.object({
+ type: z.literal('weather'),
+ location: z
+ .string()
+ .describe(
+ 'Human-readable location name (e.g., "New York, NY, USA", "London, UK"). Use this OR lat/lon coordinates, never both. Leave empty string if providing coordinates.',
+ ),
+ lat: z
+ .number()
+ .describe(
+ 'Latitude coordinate in decimal degrees (e.g., 40.7128). Only use when location name is empty.',
+ ),
+ lon: z
+ .number()
+ .describe(
+ 'Longitude coordinate in decimal degrees (e.g., -74.0060). Only use when location name is empty.',
+ ),
+});
+
+const weatherWidget = {
+ name: 'weather',
+ description:
+ 'Provides current weather information for a specified location. It can return details such as temperature, humidity, wind speed, and weather conditions. It needs either a location name or latitude/longitude coordinates to function.',
+ schema: WeatherWidgetSchema,
+ execute: async (params, _) => {
+ if (
+ params.location === '' &&
+ (params.lat === undefined || params.lon === undefined)
+ ) {
+ throw new Error(
+ 'Either location name or both latitude and longitude must be provided.',
+ );
+ }
+
+ if (params.location !== '') {
+ const openStreetMapUrl = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(params.location)}&format=json&limit=1`;
+
+ const locationRes = await fetch(openStreetMapUrl, {
+ headers: {
+ 'User-Agent': 'Perplexica',
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const data = await locationRes.json();
+
+ const location = data[0];
+
+ if (!location) {
+ throw new Error(
+ `Could not find coordinates for location: ${params.location}`,
+ );
+ }
+
+ const weatherRes = await fetch(
+ `https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lon}¤t_weather=true`,
+ {
+ headers: {
+ 'User-Agent': 'Perplexica',
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ const weatherData = await weatherRes.json();
+
+ /* this is like a very simple implementation just to see the bacckend works, when we're working on the frontend, we'll return more data i guess? */
+ return {
+ type: 'weather',
+ data: {
+ location: params.location,
+ latitude: location.lat,
+ longitude: location.lon,
+ weather: weatherData.current_weather,
+ },
+ };
+ } else if (params.lat !== undefined && params.lon !== undefined) {
+ const [weatherRes, locationRes] = await Promise.all([
+ fetch(
+ `https://api.open-meteo.com/v1/forecast?latitude=${params.lat}&longitude=${params.lon}¤t_weather=true`,
+ {
+ headers: {
+ 'User-Agent': 'Perplexica',
+ 'Content-Type': 'application/json',
+ },
+ },
+ ),
+ fetch(
+ `https://nominatim.openstreetmap.org/reverse?lat=${params.lat}&lon=${params.lon}&format=json`,
+ {
+ headers: {
+ 'User-Agent': 'Perplexica',
+ 'Content-Type': 'application/json',
+ },
+ },
+ ),
+ ]);
+
+ const weatherData = await weatherRes.json();
+ const locationData = await locationRes.json();
+
+ return {
+ type: 'weather',
+ data: {
+ location: locationData.display_name,
+ latitude: params.lat,
+ longitude: params.lon,
+ weather: weatherData.current_weather,
+ },
+ };
+ }
+
+ return {
+ type: 'weather',
+ data: null,
+ };
+ },
+} satisfies Widget;
+
+export default weatherWidget;
From 9b85c63a80e5ee3e5673c0776e44de6c5fb95da8 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Fri, 21 Nov 2025 23:49:14 +0530
Subject: [PATCH 022/199] feat(db): migrate schema
---
src/lib/db/schema.ts | 26 +++++++++++---------------
1 file changed, 11 insertions(+), 15 deletions(-)
diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts
index b6924ac..71d441f 100644
--- a/src/lib/db/schema.ts
+++ b/src/lib/db/schema.ts
@@ -1,26 +1,22 @@
import { sql } from 'drizzle-orm';
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';
-import { Document } from '@langchain/core/documents';
export const messages = sqliteTable('messages', {
id: integer('id').primaryKey(),
- role: text('type', { enum: ['assistant', 'user', 'source'] }).notNull(),
- chatId: text('chatId').notNull(),
- createdAt: text('createdAt')
- .notNull()
- .default(sql`CURRENT_TIMESTAMP`),
messageId: text('messageId').notNull(),
-
- content: text('content'),
-
- sources: text('sources', {
- mode: 'json',
- })
- .$type()
+ chatId: text('chatId').notNull(),
+ backendId: text('backendId').notNull(),
+ query: text('query').notNull(),
+ createdAt: text('createdAt').notNull(),
+ responseBlocks: text('responseBlocks', { mode: 'json' })
+ .$type()
.default(sql`'[]'`),
+ status: text({ enum: ['answering', 'completed', 'error'] }).default(
+ 'answering',
+ ),
});
-interface File {
+interface DBFile {
name: string;
fileId: string;
}
@@ -31,6 +27,6 @@ export const chats = sqliteTable('chats', {
createdAt: text('createdAt').notNull(),
focusMode: text('focusMode').notNull(),
files: text('files', { mode: 'json' })
- .$type()
+ .$type()
.default(sql`'[]'`),
});
From a494d4c3296958c7c2b01aa54da4053e8ac42838 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Fri, 21 Nov 2025 23:49:27 +0530
Subject: [PATCH 023/199] feat(app): fix migration errors
---
src/lib/db/migrate.ts | 25 ++++++++++++++++++-------
1 file changed, 18 insertions(+), 7 deletions(-)
diff --git a/src/lib/db/migrate.ts b/src/lib/db/migrate.ts
index e4c6987..5e2c374 100644
--- a/src/lib/db/migrate.ts
+++ b/src/lib/db/migrate.ts
@@ -18,12 +18,18 @@ db.exec(`
`);
function sanitizeSql(content: string) {
- return content
- .split(/\r?\n/)
- .filter(
- (l) => !l.trim().startsWith('-->') && !l.includes('statement-breakpoint'),
+ const statements = content
+ .split(/--> statement-breakpoint/g)
+ .map((stmt) =>
+ stmt
+ .split(/\r?\n/)
+ .filter((l) => !l.trim().startsWith('-->'))
+ .join('\n')
+ .trim(),
)
- .join('\n');
+ .filter((stmt) => stmt.length > 0);
+
+ return statements;
}
fs.readdirSync(migrationsFolder)
@@ -32,7 +38,7 @@ fs.readdirSync(migrationsFolder)
.forEach((file) => {
const filePath = path.join(migrationsFolder, file);
let content = fs.readFileSync(filePath, 'utf-8');
- content = sanitizeSql(content);
+ const statements = sanitizeSql(content);
const migrationName = file.split('_')[0] || file;
@@ -108,7 +114,12 @@ fs.readdirSync(migrationsFolder)
db.exec('DROP TABLE messages;');
db.exec('ALTER TABLE messages_with_sources RENAME TO messages;');
} else {
- db.exec(content);
+ // Execute each statement separately
+ statements.forEach((stmt) => {
+ if (stmt.trim()) {
+ db.exec(stmt);
+ }
+ });
}
db.prepare('INSERT OR IGNORE INTO ran_migrations (name) VALUES (?)').run(
From 2568088341e168fe30cf675e0d8f7f157880456b Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Fri, 21 Nov 2025 23:49:52 +0530
Subject: [PATCH 024/199] feat(db): add new migration files
---
drizzle/0002_daffy_wrecker.sql | 15 ++++
drizzle/meta/0002_snapshot.json | 132 ++++++++++++++++++++++++++++++++
drizzle/meta/_journal.json | 7 ++
3 files changed, 154 insertions(+)
create mode 100644 drizzle/0002_daffy_wrecker.sql
create mode 100644 drizzle/meta/0002_snapshot.json
diff --git a/drizzle/0002_daffy_wrecker.sql b/drizzle/0002_daffy_wrecker.sql
new file mode 100644
index 0000000..78b6685
--- /dev/null
+++ b/drizzle/0002_daffy_wrecker.sql
@@ -0,0 +1,15 @@
+PRAGMA foreign_keys=OFF;--> statement-breakpoint
+CREATE TABLE `__new_messages` (
+ `id` integer PRIMARY KEY NOT NULL,
+ `messageId` text NOT NULL,
+ `chatId` text NOT NULL,
+ `backendId` text NOT NULL,
+ `query` text NOT NULL,
+ `createdAt` text NOT NULL,
+ `responseBlocks` text DEFAULT '[]',
+ `status` text DEFAULT 'answering'
+);
+--> statement-breakpoint
+DROP TABLE `messages`;--> statement-breakpoint
+ALTER TABLE `__new_messages` RENAME TO `messages`;--> statement-breakpoint
+PRAGMA foreign_keys=ON;
\ No newline at end of file
diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json
new file mode 100644
index 0000000..2890f66
--- /dev/null
+++ b/drizzle/meta/0002_snapshot.json
@@ -0,0 +1,132 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "1c5eb804-d6b4-48ec-9a8f-75fb729c8e52",
+ "prevId": "6dedf55f-0e44-478f-82cf-14a21ac686f8",
+ "tables": {
+ "chats": {
+ "name": "chats",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "focusMode": {
+ "name": "focusMode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "files": {
+ "name": "files",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "messages": {
+ "name": "messages",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "messageId": {
+ "name": "messageId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "chatId": {
+ "name": "chatId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "backendId": {
+ "name": "backendId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "query": {
+ "name": "query",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "responseBlocks": {
+ "name": "responseBlocks",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'answering'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index cf1610b..c271ddc 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -15,6 +15,13 @@
"when": 1758863991284,
"tag": "0001_wise_rockslide",
"breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "6",
+ "when": 1763732708332,
+ "tag": "0002_daffy_wrecker",
+ "breakpoints": true
}
]
}
From 70bcd8c6f1535a2b43ca4351f228be0815800ce8 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Fri, 21 Nov 2025 23:51:09 +0530
Subject: [PATCH 025/199] feat(types): add artifact to block, add more blocks
---
src/lib/types.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 60 insertions(+), 4 deletions(-)
diff --git a/src/lib/types.ts b/src/lib/types.ts
index a96c7ca..824aea0 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -1,4 +1,4 @@
-type Message = {
+type ChatTurnMessage = {
role: 'user' | 'assistant' | 'system';
content: string;
};
@@ -8,8 +8,64 @@ type Chunk = {
metadata: Record;
};
-type Artifact = {
+type TextBlock = {
id: string;
- type: string;
- data: any;
+ type: 'text';
+ data: string;
};
+
+type SourceBlock = {
+ id: string;
+ type: 'source';
+ data: Chunk[];
+};
+
+type SuggestionBlock = {
+ id: string;
+ type: 'suggestion';
+ data: string[];
+};
+
+type WidgetBlock = {
+ id: string;
+ type: 'widget';
+ data: {
+ widgetType: string;
+ params: Record;
+ };
+};
+
+type ReasoningResearchBlock = {
+ id: string;
+ reasoning: string;
+};
+
+type SearchingResearchBlock = {
+ id: string;
+ searching: string[];
+};
+
+type ReadingResearchBlock = {
+ id: string;
+ reading: Chunk[];
+};
+
+type ResearchBlockSubStep =
+ | ReasoningResearchBlock
+ | SearchingResearchBlock
+ | ReadingResearchBlock;
+
+type ResearchBlock = {
+ id: string;
+ type: 'research';
+ data: {
+ subSteps: ResearchBlockSubStep[];
+ };
+};
+
+type Block =
+ | TextBlock
+ | SourceBlock
+ | SuggestionBlock
+ | WidgetBlock
+ | ResearchBlock;
From f7a43b3cb95dfb5ad49ec6c1d34f25638438037e Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Fri, 21 Nov 2025 23:52:55 +0530
Subject: [PATCH 026/199] feat(session): use blocks, use rfc6902 for stream
with patching
---
src/lib/session.ts | 72 ++++++++++++++++++++++++++++++++++------------
1 file changed, 54 insertions(+), 18 deletions(-)
diff --git a/src/lib/session.ts b/src/lib/session.ts
index 6c5e18e..834cbfd 100644
--- a/src/lib/session.ts
+++ b/src/lib/session.ts
@@ -1,13 +1,20 @@
import { EventEmitter } from 'stream';
-/* todo implement history saving and better artifact typing and handling */
+import { applyPatch } from 'rfc6902';
+
class SessionManager {
private static sessions = new Map();
readonly id: string;
- private artifacts = new Map();
+ private blocks = new Map();
+ private events: { event: string; data: any }[] = [];
private emitter = new EventEmitter();
+ private TTL_MS = 30 * 60 * 1000;
- constructor() {
- this.id = crypto.randomUUID();
+ constructor(id?: string) {
+ this.id = id ?? crypto.randomUUID();
+
+ setTimeout(() => {
+ SessionManager.sessions.delete(this.id);
+ }, this.TTL_MS);
}
static getSession(id: string): SessionManager | undefined {
@@ -18,26 +25,55 @@ class SessionManager {
return Array.from(this.sessions.values());
}
+ static createSession(): SessionManager {
+ const session = new SessionManager();
+ this.sessions.set(session.id, session);
+ return session;
+ }
+
+ removeAllListeners() {
+ this.emitter.removeAllListeners();
+ }
+
emit(event: string, data: any) {
this.emitter.emit(event, data);
+ this.events.push({ event, data });
}
- emitArtifact(artifact: Artifact) {
- this.artifacts.set(artifact.id, artifact);
- this.emitter.emit('addArtifact', artifact);
+ emitBlock(block: Block) {
+ this.blocks.set(block.id, block);
+ this.emit('data', {
+ type: 'block',
+ block: block,
+ });
}
- appendToArtifact(artifactId: string, data: any) {
- const artifact = this.artifacts.get(artifactId);
- if (artifact) {
- if (typeof artifact.data === 'string') {
- artifact.data += data;
- } else if (Array.isArray(artifact.data)) {
- artifact.data.push(data);
- } else if (typeof artifact.data === 'object') {
- Object.assign(artifact.data, data);
- }
- this.emitter.emit('updateArtifact', artifact);
+ getBlock(blockId: string): Block | undefined {
+ return this.blocks.get(blockId);
+ }
+
+ updateBlock(blockId: string, patch: any[]) {
+ const block = this.blocks.get(blockId);
+
+ if (block) {
+ applyPatch(block, patch);
+ this.blocks.set(blockId, block);
+ this.emit('data', {
+ type: 'updateBlock',
+ blockId: blockId,
+ patch: patch,
+ });
+ }
+ }
+
+ addListener(event: string, listener: (data: any) => void) {
+ this.emitter.addListener(event, listener);
+ }
+
+ replay() {
+ for (const { event, data } of this.events) {
+ /* Using emitter directly to avoid infinite loop */
+ this.emitter.emit(event, data);
}
}
}
From 4016b21bdfaa37165609098ad133617178c427c6 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Fri, 21 Nov 2025 23:54:16 +0530
Subject: [PATCH 027/199] Update formatHistory.ts
---
src/lib/utils/formatHistory.ts | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/lib/utils/formatHistory.ts b/src/lib/utils/formatHistory.ts
index 733ffc0..8256ffa 100644
--- a/src/lib/utils/formatHistory.ts
+++ b/src/lib/utils/formatHistory.ts
@@ -1,10 +1,8 @@
-import { BaseMessage, isAIMessage } from '@langchain/core/messages';
-
-const formatChatHistoryAsString = (history: BaseMessage[]) => {
+const formatChatHistoryAsString = (history: Message[]) => {
return history
.map(
(message) =>
- `${isAIMessage(message) ? 'AI' : 'User'}: ${message.content}`,
+ `${message.role === 'assistant' ? 'AI' : 'User'}: ${message.content}`,
)
.join('\n');
};
From 0df0114e76ab0b637026c8bdf6e0f0a9eb2853f1 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Fri, 21 Nov 2025 23:54:30 +0530
Subject: [PATCH 028/199] feat(prompts): add researcher prompt
---
src/lib/prompts/search/researcher.ts | 241 +++++++++++++++++++++++++++
1 file changed, 241 insertions(+)
create mode 100644 src/lib/prompts/search/researcher.ts
diff --git a/src/lib/prompts/search/researcher.ts b/src/lib/prompts/search/researcher.ts
new file mode 100644
index 0000000..2c58b9b
--- /dev/null
+++ b/src/lib/prompts/search/researcher.ts
@@ -0,0 +1,241 @@
+export const getResearcherPrompt = (
+ actionDesc: string,
+ mode: 'fast' | 'balanced' | 'deep_research',
+) => {
+ const today = new Date().toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+
+ return `
+You are an action orchestrator. Your job is to fulfill user requests by selecting and executing appropriate actions - whether that's searching for information, creating calendar events, sending emails, or any other available action.
+
+Today's date: ${today}
+
+You are operating in "${mode}" mode. ${
+ mode === 'fast'
+ ? 'Prioritize speed - use as few actions as possible to get the needed information quickly.'
+ : mode === 'balanced'
+ ? 'Balance speed and depth - use a moderate number of actions to get good information efficiently. Never stop at the first action unless there is no action available or the query is simple.'
+ : 'Conduct deep research - use multiple actions to gather comprehensive information, even if it takes longer.'
+ }
+
+
+${actionDesc}
+
+
+
+
+NEVER ASSUME - your knowledge may be outdated. When a user asks about something you're not certain about, go find out. Don't assume it exists or doesn't exist - just look it up directly.
+
+
+
+
+
+Think like a human would. Your reasoning should be natural and show:
+- What the user is asking for
+- What you need to find out or do
+- Your plan to accomplish it
+
+Keep it to 2-3 natural sentences.
+
+
+
+
+
+## Example 1: Unknown Subject
+
+User: "What is Kimi K2?"
+
+Good reasoning:
+"I'm not sure what Kimi K2 is - could be an AI model, a product, or something else. Let me look it up to find out what it actually is and get the relevant details."
+
+Actions: web_search ["Kimi K2", "Kimi K2 AI"]
+
+## Example 2: Subject You're Uncertain About
+
+User: "What are the features of GPT-5.1?"
+
+Good reasoning:
+"I don't have current information on GPT-5.1 - my knowledge might be outdated. Let me look up GPT-5.1 to see what's available and what features it has."
+
+Actions: web_search ["GPT-5.1", "GPT-5.1 features", "GPT-5.1 release"]
+
+Bad reasoning (wastes time on verification):
+"GPT-5.1 might not exist based on my knowledge. I need to verify if it exists first before looking for features."
+
+## Example 3: After Actions Return Results
+
+User: "What are the features of GPT-5.1?"
+[Previous actions returned information about GPT-5.1]
+
+Good reasoning:
+"Got the information I needed about GPT-5.1. The results cover its features and capabilities - I can now provide a complete answer."
+
+Action: done
+
+## Example 4: Ambiguous Query
+
+User: "Tell me about Mercury"
+
+Good reasoning:
+"Mercury could refer to several things - the planet, the element, or something else. I'll look up both main interpretations to give a useful answer."
+
+Actions: web_search ["Mercury planet facts", "Mercury element"]
+
+## Example 5: Current Events
+
+User: "What's happening with AI regulation?"
+
+Good reasoning:
+"I need current news on AI regulation developments. Let me find the latest updates on this topic."
+
+Actions: web_search ["AI regulation news 2024", "AI regulation bill latest"]
+
+## Example 6: Technical Query
+
+User: "How do I set up authentication in Next.js 14?"
+
+Good reasoning:
+"This is a technical implementation question. I'll find the current best practices and documentation for Next.js 14 authentication."
+
+Actions: web_search ["Next.js 14 authentication guide", "NextAuth.js App Router"]
+
+## Example 7: Comparison Query
+
+User: "Prisma vs Drizzle - which should I use?"
+
+Good reasoning:
+"Need to find factual comparisons between these ORMs - performance, features, trade-offs. Let me gather objective information."
+
+Actions: web_search ["Prisma vs Drizzle comparison 2024", "Drizzle ORM performance"]
+
+## Example 8: Fact-Check
+
+User: "Is it true you only use 10% of your brain?"
+
+Good reasoning:
+"This is a common claim that needs scientific verification. Let me find what the actual research says about this."
+
+Actions: web_search ["10 percent brain myth science", "brain usage neuroscience"]
+
+## Example 9: Recent Product
+
+User: "What are the specs of MacBook Pro M4?"
+
+Good reasoning:
+"I need current information on the MacBook Pro M4. Let me look up the latest specs and details."
+
+Actions: web_search ["MacBook Pro M4 specs", "MacBook Pro M4 specifications Apple"]
+
+## Example 10: Multi-Part Query
+
+User: "Population of Tokyo vs New York?"
+
+Good reasoning:
+"Need current population stats for both cities. I'll look up the comparison data."
+
+Actions: web_search ["Tokyo population 2024", "Tokyo vs New York population"]
+
+## Example 11: Calendar Task
+
+User: "Add a meeting with John tomorrow at 3pm"
+
+Good reasoning:
+"This is a calendar task. I have all the details - meeting with John, tomorrow, 3pm. I'll create the event."
+
+Action: create_calendar_event with the provided details
+
+## Example 12: Email Task
+
+User: "Send an email to sarah@company.com about the project update"
+
+Good reasoning:
+"Need to send an email. I have the recipient but need to compose appropriate content about the project update."
+
+Action: send_email to sarah@company.com with project update content
+
+## Example 13: Multi-Step Task
+
+User: "What's the weather in Tokyo and add a reminder to pack an umbrella if it's rainy"
+
+Good reasoning:
+"Two things here - first I need to check Tokyo's weather, then based on that I might need to create a reminder. Let me start with the weather lookup."
+
+Actions: web_search ["Tokyo weather today forecast"]
+
+## Example 14: Research Then Act
+
+User: "Find the best Italian restaurant near me and make a reservation for 7pm"
+
+Good reasoning:
+"I need to first find top Italian restaurants in the area, then make a reservation. Let me start by finding the options."
+
+Actions: web_search ["best Italian restaurant near me", "top rated Italian restaurants"]
+
+
+
+
+
+## For Information Queries:
+- Just look it up - don't overthink whether something exists
+- Use 1-3 targeted queries
+- Done when you have useful information to answer with
+
+## For Task Execution:
+- Calendar, email, reminders: execute directly with the provided details
+- If details are missing, note what you need
+
+## For Multi-Step Requests:
+- Break it down logically
+- Complete one part before moving to the next
+- Some tasks require information before you can act
+
+## When to Select "done":
+- You have the information needed to answer
+- You've completed the requested task
+- Further actions would be redundant
+
+
+
+
+
+**General subjects:**
+- ["subject name", "subject name + context"]
+
+**Current events:**
+- Include year: "topic 2024", "topic latest news"
+
+**Technical topics:**
+- Include versions: "framework v14 guide"
+- Add context: "documentation", "tutorial", "how to"
+
+**Comparisons:**
+- "X vs Y comparison", "X vs Y benchmarks"
+
+**Keep it simple:**
+- 1-3 actions per iteration
+- Don't over-complicate queries
+
+
+
+
+
+1. **Over-assuming**: Don't assume things exist or don't exist - just look them up
+
+2. **Verification obsession**: Don't waste actions "verifying existence" - just search for the thing directly
+
+3. **Endless loops**: If 2-3 actions don't find something, it probably doesn't exist - report that and move on
+
+4. **Ignoring task context**: If user wants a calendar event, don't just search - create the event
+
+5. **Overthinking**: Keep reasoning simple and action-focused
+
+
+
+
+Reasoning should be 2-3 natural sentences showing your thought process and plan. Then select and configure the appropriate action(s).
+
+`;
+};
From 08feb18197f68c87fba1d8c952676d7eb90d2f73 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Fri, 21 Nov 2025 23:57:29 +0530
Subject: [PATCH 029/199] feat(search-agent): add researcher, research actions
---
.../agents/search/researcher/actions/done.ts | 19 +++
.../agents/search/researcher/actions/index.ts | 8 ++
.../search/researcher/actions/registry.ts | 73 +++++++++++
.../search/researcher/actions/webSearch.ts | 54 +++++++++
src/lib/agents/search/researcher/index.ts | 113 ++++++++++++++++++
src/lib/agents/search/types.ts | 51 +++++++-
6 files changed, 313 insertions(+), 5 deletions(-)
create mode 100644 src/lib/agents/search/researcher/actions/done.ts
create mode 100644 src/lib/agents/search/researcher/actions/index.ts
create mode 100644 src/lib/agents/search/researcher/actions/registry.ts
create mode 100644 src/lib/agents/search/researcher/actions/webSearch.ts
create mode 100644 src/lib/agents/search/researcher/index.ts
diff --git a/src/lib/agents/search/researcher/actions/done.ts b/src/lib/agents/search/researcher/actions/done.ts
new file mode 100644
index 0000000..9ce93ae
--- /dev/null
+++ b/src/lib/agents/search/researcher/actions/done.ts
@@ -0,0 +1,19 @@
+import z from 'zod';
+import { ResearchAction } from '../../types';
+
+const doneAction: ResearchAction = {
+ name: 'done',
+ description:
+ "Indicates that the research process is complete and no further actions are needed. Use this action when you have gathered sufficient information to answer the user's query.",
+ enabled: (_) => true,
+ schema: z.object({
+ type: z.literal('done'),
+ }),
+ execute: async (params, additionalConfig) => {
+ return {
+ type: 'done',
+ };
+ },
+};
+
+export default doneAction;
diff --git a/src/lib/agents/search/researcher/actions/index.ts b/src/lib/agents/search/researcher/actions/index.ts
new file mode 100644
index 0000000..4814b1e
--- /dev/null
+++ b/src/lib/agents/search/researcher/actions/index.ts
@@ -0,0 +1,8 @@
+import doneAction from './done';
+import ActionRegistry from './registry';
+import webSearchAction from './webSearch';
+
+ActionRegistry.register(webSearchAction);
+ActionRegistry.register(doneAction);
+
+export { ActionRegistry };
diff --git a/src/lib/agents/search/researcher/actions/registry.ts b/src/lib/agents/search/researcher/actions/registry.ts
new file mode 100644
index 0000000..4172e8b
--- /dev/null
+++ b/src/lib/agents/search/researcher/actions/registry.ts
@@ -0,0 +1,73 @@
+import {
+ ActionConfig,
+ ActionOutput,
+ AdditionalConfig,
+ ClassifierOutput,
+ ResearchAction,
+} from '../../types';
+
+class ActionRegistry {
+ private static actions: Map = new Map();
+
+ static register(action: ResearchAction) {
+ this.actions.set(action.name, action);
+ }
+
+ static get(name: string): ResearchAction | undefined {
+ return this.actions.get(name);
+ }
+
+ static getAvailableActions(config: {
+ classification: ClassifierOutput;
+ }): ResearchAction[] {
+ return Array.from(
+ this.actions.values().filter((action) => action.enabled(config)),
+ );
+ }
+
+ static getAvailableActionsDescriptions(config: {
+ classification: ClassifierOutput;
+ }): string {
+ const availableActions = this.getAvailableActions(config);
+
+ return availableActions
+ .map((action) => `------------\n##${action.name}\n${action.description}`)
+ .join('\n\n');
+ }
+
+ static async execute(
+ name: string,
+ params: any,
+ additionalConfig: AdditionalConfig,
+ ) {
+ const action = this.actions.get(name);
+
+ if (!action) {
+ throw new Error(`Action with name ${name} not found`);
+ }
+
+ return action.execute(params, additionalConfig);
+ }
+
+ static async executeAll(
+ actions: ActionConfig[],
+ additionalConfig: AdditionalConfig,
+ ): Promise {
+ const results: ActionOutput[] = [];
+
+ await Promise.all(
+ actions.map(async (actionConfig) => {
+ const output = await this.execute(
+ actionConfig.type,
+ actionConfig.params,
+ additionalConfig,
+ );
+ results.push(output);
+ }),
+ );
+
+ return results;
+ }
+}
+
+export default ActionRegistry;
diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts
new file mode 100644
index 0000000..943afff
--- /dev/null
+++ b/src/lib/agents/search/researcher/actions/webSearch.ts
@@ -0,0 +1,54 @@
+import z from 'zod';
+import { ResearchAction } from '../../types';
+import { searchSearxng } from '@/lib/searxng';
+
+const actionSchema = z.object({
+ type: z.literal('web_search'),
+ queries: z
+ .array(z.string())
+ .describe('An array of search queries to perform web searches for.'),
+});
+
+const actionDescription = `
+You have to use this action aggressively to find relevant information from the web to answer user queries. You can combine this action with other actions to gather comprehensive data. Always ensure that you provide accurate and up-to-date information by leveraging web search results.
+When this action is present, you must use it to obtain current information from the web.
+
+### How to use:
+1. For fast search mode, you can use this action once. Make sure to cover all aspects of the user's query in that single search.
+2. If you're on quality mode, you'll get to use this action up to two times. Use the first search to gather general information, and the second search to fill in any gaps or get more specific details based on the initial findings.
+3. If you're set on Deep research mode, then you will get to use this action multiple times to gather more information. Use your judgment to decide when additional searches are necessary to provide a thorough and accurate response.
+
+Input: An array of search queries. Make sure the queries are relevant to the user's request and cover different aspects if necessary. You can include a maximum of 3 queries. Make sure the queries are SEO friendly and not sentences rather keywords which can be used to search a search engine like Google, Bing, etc.
+`;
+
+const webSearchAction: ResearchAction = {
+ name: 'web_search',
+ description: actionDescription,
+ schema: actionSchema,
+ enabled: (config) => config.classification.intents.includes('web_search'),
+ execute: async (input, _) => {
+ let results: Chunk[] = [];
+
+ const search = async (q: string) => {
+ const res = await searchSearxng(q);
+ res.results.forEach((r) => {
+ results.push({
+ content: r.content || r.title,
+ metadata: {
+ title: r.title,
+ url: r.url,
+ },
+ });
+ });
+ };
+
+ await Promise.all(input.queries.map(search));
+
+ return {
+ type: 'search_results',
+ results,
+ };
+ },
+};
+
+export default webSearchAction;
diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts
new file mode 100644
index 0000000..300de72
--- /dev/null
+++ b/src/lib/agents/search/researcher/index.ts
@@ -0,0 +1,113 @@
+import z from 'zod';
+import {
+ ActionConfig,
+ ActionOutput,
+ ResearcherInput,
+ ResearcherOutput,
+} from '../types';
+import { ActionRegistry } from './actions';
+import { getResearcherPrompt } from '@/lib/prompts/search/researcher';
+import SessionManager from '@/lib/session';
+
+class Researcher {
+ async research(
+ session: SessionManager,
+ input: ResearcherInput,
+ ): Promise {
+ let findings: string = '';
+ let actionOutput: ActionOutput[] = [];
+ let maxIteration =
+ input.config.mode === 'fast'
+ ? 1
+ : input.config.mode === 'balanced'
+ ? 3
+ : 25;
+
+ const availableActions = ActionRegistry.getAvailableActions({
+ classification: input.classification,
+ });
+
+ const schema = z.object({
+ reasoning: z
+ .string()
+ .describe('The reasoning behind choosing the next action.'),
+ action: z
+ .union(availableActions.map((a) => a.schema))
+ .describe('The action to be performed next.'),
+ });
+
+ const availableActionsDescription =
+ ActionRegistry.getAvailableActionsDescriptions({
+ classification: input.classification,
+ });
+
+ for (let i = 0; i < maxIteration; i++) {
+ const researcherPrompt = getResearcherPrompt(availableActionsDescription);
+
+ const res = await input.config.llm.generateObject>(
+ {
+ messages: [
+ {
+ role: 'system',
+ content: researcherPrompt,
+ },
+ {
+ role: 'user',
+ content: `
+
+ ${input.classification.standaloneFollowUp}
+
+
+
+ ${findings}
+
+ `,
+ },
+ ],
+ schema,
+ },
+ );
+
+
+ if (res.action.type === 'done') {
+ console.log('Research complete - "done" action selected');
+ break;
+ }
+
+ const actionConfig: ActionConfig = {
+ type: res.action.type as string,
+ params: res.action,
+ };
+
+ findings += 'Reasoning: ' + res.reasoning + '\n';
+ findings += `Executing Action: ${actionConfig.type} with params ${JSON.stringify(actionConfig.params)}\n`;
+
+ const actionResult = await ActionRegistry.execute(
+ actionConfig.type,
+ actionConfig.params,
+ {
+ llm: input.config.llm,
+ embedding: input.config.embedding,
+ session: session,
+ },
+ );
+
+ actionOutput.push(actionResult);
+
+ if (actionResult.type === 'search_results') {
+ findings += actionResult.results
+ .map(
+ (r) =>
+ `Title: ${r.metadata.title}\nURL: ${r.metadata.url}\nContent: ${r.content}\n`,
+ )
+ .join('\n');
+ }
+ }
+
+ return {
+ findings: actionOutput,
+ };
+ }
+}
+
+export default Researcher;
diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts
index 72d7ecf..fc0735d 100644
--- a/src/lib/agents/search/types.ts
+++ b/src/lib/agents/search/types.ts
@@ -1,7 +1,7 @@
-import { EventEmitter } from 'stream';
import z from 'zod';
import BaseLLM from '../../models/base/llm';
import BaseEmbedding from '@/lib/models/base/embedding';
+import SessionManager from '@/lib/session';
export type SearchSources = 'web' | 'discussions' | 'academic';
@@ -9,10 +9,11 @@ export type SearchAgentConfig = {
sources: SearchSources[];
llm: BaseLLM;
embedding: BaseEmbedding;
+ mode: 'fast' | 'balanced' | 'deep_research';
};
export type SearchAgentInput = {
- chatHistory: Message[];
+ chatHistory: ChatTurnMessage[];
followUp: string;
config: SearchAgentConfig;
};
@@ -48,7 +49,7 @@ export type ClassifierInput = {
llm: BaseLLM;
enabledSources: SearchSources[];
query: string;
- chatHistory: Message[];
+ chatHistory: ChatTurnMessage[];
};
export type ClassifierOutput = {
@@ -60,6 +61,46 @@ export type ClassifierOutput = {
export type AdditionalConfig = {
llm: BaseLLM;
- embedding: BaseLLM;
- emitter: EventEmitter;
+ embedding: BaseEmbedding;
+ session: SessionManager;
+};
+
+export type ResearcherInput = {
+ chatHistory: ChatTurnMessage[];
+ followUp: string;
+ classification: ClassifierOutput;
+ config: SearchAgentConfig;
+};
+
+export type ResearcherOutput = {
+ findings: ActionOutput[];
+};
+
+export type SearchActionOutput = {
+ type: 'search_results';
+ results: Chunk[];
+};
+
+export type DoneActionOutput = {
+ type: 'done';
+};
+
+export type ActionOutput = SearchActionOutput | DoneActionOutput;
+
+export interface ResearchAction<
+ TSchema extends z.ZodObject = z.ZodObject,
+> {
+ name: string;
+ description: string;
+ schema: z.ZodObject;
+ enabled: (config: { classification: ClassifierOutput }) => boolean;
+ execute: (
+ params: z.infer,
+ additionalConfig: AdditionalConfig,
+ ) => Promise;
+}
+
+export type ActionConfig = {
+ type: string;
+ params: Record;
};
From c4acc83fd596caf04f11b3d3b20f03bfc444de18 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Fri, 21 Nov 2025 23:57:50 +0530
Subject: [PATCH 030/199] feat(agents): add search agent
---
src/lib/agents/search/index.ts | 48 ++++++++++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
create mode 100644 src/lib/agents/search/index.ts
diff --git a/src/lib/agents/search/index.ts b/src/lib/agents/search/index.ts
new file mode 100644
index 0000000..141ea7f
--- /dev/null
+++ b/src/lib/agents/search/index.ts
@@ -0,0 +1,48 @@
+import { ResearcherOutput, SearchAgentInput } from './types';
+import SessionManager from '@/lib/session';
+import Classifier from './classifier';
+import { WidgetRegistry } from './widgets';
+import Researcher from './researcher';
+
+class SearchAgent {
+ async searchAsync(session: SessionManager, input: SearchAgentInput) {
+ const classifier = new Classifier();
+
+ const classification = await classifier.classify({
+ chatHistory: input.chatHistory,
+ enabledSources: input.config.sources,
+ query: input.followUp,
+ llm: input.config.llm,
+ });
+
+ session.emit('data', {
+ type: 'classification',
+ classification: classification,
+ });
+
+ const widgetPromise = WidgetRegistry.executeAll(classification.widgets, {
+ llm: input.config.llm,
+ embedding: input.config.embedding,
+ session: session,
+ });
+
+ let searchPromise: Promise | null = null;
+
+ if (!classification.skipSearch) {
+ const researcher = new Researcher();
+ searchPromise = researcher.research(session, {
+ chatHistory: input.chatHistory,
+ followUp: input.followUp,
+ classification: classification,
+ config: input.config,
+ });
+ }
+
+ const [widgetOutputs, searchResults] = await Promise.all([
+ widgetPromise,
+ searchPromise,
+ ]);
+ }
+}
+
+export default SearchAgent;
From 55cf88822d0c0184a46ce587d9a2f1c7659ba43f Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Fri, 21 Nov 2025 23:58:04 +0530
Subject: [PATCH 031/199] feat(package): add modules
---
package.json | 4 ++++
yarn.lock | 17 +++++++++++++++++
2 files changed, 21 insertions(+)
diff --git a/package.json b/package.json
index b51dc6f..5752b9f 100644
--- a/package.json
+++ b/package.json
@@ -40,11 +40,15 @@
"markdown-to-jsx": "^7.7.2",
"next": "^15.2.2",
"next-themes": "^0.3.0",
+ "ollama": "^0.6.3",
+ "openai": "^6.9.0",
+ "partial-json": "^0.1.7",
"pdf-parse": "^1.1.1",
"react": "^18",
"react-dom": "^18",
"react-text-to-speech": "^0.14.5",
"react-textarea-autosize": "^8.5.3",
+ "rfc6902": "^5.1.2",
"sonner": "^1.4.41",
"tailwind-merge": "^2.2.2",
"winston": "^3.17.0",
diff --git a/yarn.lock b/yarn.lock
index 8844d50..d202f75 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3992,6 +3992,13 @@ ollama@^0.5.12:
dependencies:
whatwg-fetch "^3.6.20"
+ollama@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/ollama/-/ollama-0.6.3.tgz#b188573dd0ccb3b4759c1f8fa85067cb17f6673c"
+ integrity sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==
+ dependencies:
+ whatwg-fetch "^3.6.20"
+
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -4126,6 +4133,11 @@ parseley@^0.12.0:
leac "^0.6.0"
peberminta "^0.9.0"
+partial-json@^0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/partial-json/-/partial-json-0.1.7.tgz#b735a89edb3e25f231a3c4caeaae71dc9f578605"
+ integrity sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==
+
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -4518,6 +4530,11 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+rfc6902@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/rfc6902/-/rfc6902-5.1.2.tgz#774262ba7b032ab9abf9eb8e0312927e8f425062"
+ integrity sha512-zxcb+PWlE8PwX0tiKE6zP97THQ8/lHmeiwucRrJ3YFupWEmp25RmFSlB1dNTqjkovwqG4iq+u1gzJMBS3um8mA==
+
rgbcolor@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
From 9ac2da36079fe9147658ee8252a165a0149e35d0 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sat, 22 Nov 2025 22:22:34 +0530
Subject: [PATCH 032/199] feat(app): remove old search agent
---
src/lib/prompts/webSearch.ts | 137 --------
src/lib/prompts/writingAssistant.ts | 17 -
src/lib/search/index.ts | 59 ----
src/lib/search/metaSearchAgent.ts | 514 ----------------------------
4 files changed, 727 deletions(-)
delete mode 100644 src/lib/prompts/webSearch.ts
delete mode 100644 src/lib/prompts/writingAssistant.ts
delete mode 100644 src/lib/search/index.ts
delete mode 100644 src/lib/search/metaSearchAgent.ts
diff --git a/src/lib/prompts/webSearch.ts b/src/lib/prompts/webSearch.ts
deleted file mode 100644
index b99b542..0000000
--- a/src/lib/prompts/webSearch.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { BaseMessageLike } from '@langchain/core/messages';
-
-export const webSearchRetrieverPrompt = `
-You are an AI question rephraser. You will be given a conversation and a follow-up question, you will have to rephrase the follow up question so it is a standalone question and can be used by another LLM to search the web for information to answer it.
-If it is a simple writing task or a greeting (unless the greeting contains a question after it) like Hi, Hello, How are you, etc. than a question then you need to return \`not_needed\` as the response (This is because the LLM won't need to search the web for finding information on this topic).
-If the user asks some question from some URL or wants you to summarize a PDF or a webpage (via URL) you need to return the links inside the \`links\` XML block and the question inside the \`question\` XML block. If the user wants to you to summarize the webpage or the PDF you need to return \`summarize\` inside the \`question\` XML block in place of a question and the link to summarize in the \`links\` XML block.
-You must always return the rephrased question inside the \`question\` XML block, if there are no links in the follow-up question then don't insert a \`links\` XML block in your response.
-
-**Note**: All user messages are individual entities and should be treated as such do not mix conversations.
-`;
-
-export const webSearchRetrieverFewShots: BaseMessageLike[] = [
- [
- 'user',
- `
-
-
-What is the capital of France
- `,
- ],
- [
- 'assistant',
- `
-Capital of france
- `,
- ],
- [
- 'user',
- `
-
-
-Hi, how are you?
- `,
- ],
- [
- 'assistant',
- `
-not_needed
- `,
- ],
- [
- 'user',
- `
-
-
-What is Docker?
- `,
- ],
- [
- 'assistant',
- `
-What is Docker
- `,
- ],
- [
- 'user',
- `
-
-
-Can you tell me what is X from https://example.com
- `,
- ],
- [
- 'assistant',
- `
-What is X?
-
-
-https://example.com
- `,
- ],
- [
- 'user',
- `
-
-
-Summarize the content from https://example.com
- `,
- ],
- [
- 'assistant',
- `
-summarize
-
-
-https://example.com
- `,
- ],
-];
-
-export const webSearchResponsePrompt = `
- You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
-
- Your task is to provide answers that are:
- - **Informative and relevant**: Thoroughly address the user's query using the given context.
- - **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
- - **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
- - **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
- - **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
-
- ### Formatting Instructions
- - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate.
- - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience.
- - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
- - **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience.
- - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
- - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
-
- ### Citation Requirements
- - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
- - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
- - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
- - Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
- - Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
- - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
-
- ### Special Instructions
- - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
- - If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- - If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
-
- ### User instructions
- These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
- {systemInstructions}
-
- ### Example Output
- - Begin with a brief introduction summarizing the event or query topic.
- - Follow with detailed sections under clear headings, covering all aspects of the query if possible.
- - Provide explanations or historical context as needed to enhance understanding.
- - End with a conclusion or overall perspective if relevant.
-
-
- {context}
-
-
- Current date & time in ISO format (UTC timezone) is: {date}.
-`;
diff --git a/src/lib/prompts/writingAssistant.ts b/src/lib/prompts/writingAssistant.ts
deleted file mode 100644
index 565827a..0000000
--- a/src/lib/prompts/writingAssistant.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export const writingAssistantPrompt = `
-You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Writing Assistant', this means you will be helping the user write a response to a given query.
-Since you are a writing assistant, you would not perform web searches. If you think you lack information to answer the query, you can ask the user for more information or suggest them to switch to a different focus mode.
-You will be shared a context that can contain information from files user has uploaded to get answers from. You will have to generate answers upon that.
-
-You have to cite the answer using [number] notation. You must cite the sentences with their relevent context number. You must cite each and every part of the answer so the user can know where the information is coming from.
-Place these citations at the end of that particular sentence. You can cite the same sentence multiple times if it is relevant to the user's query like [number1][number2].
-However you do not need to cite it using the same number. You can use different numbers to cite the same sentence multiple times. The number refers to the number of the search result (passed in the context) used to generate that part of the answer.
-
-### User instructions
-These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
-{systemInstructions}
-
-
-{context}
-
-`;
diff --git a/src/lib/search/index.ts b/src/lib/search/index.ts
deleted file mode 100644
index 8eb8ab0..0000000
--- a/src/lib/search/index.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import MetaSearchAgent from '@/lib/search/metaSearchAgent';
-import prompts from '../prompts';
-
-export const searchHandlers: Record = {
- webSearch: new MetaSearchAgent({
- activeEngines: [],
- queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
- responsePrompt: prompts.webSearchResponsePrompt,
- queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
- rerank: true,
- rerankThreshold: 0.3,
- searchWeb: true,
- }),
- academicSearch: new MetaSearchAgent({
- activeEngines: ['arxiv', 'google scholar', 'pubmed'],
- queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
- responsePrompt: prompts.webSearchResponsePrompt,
- queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
- rerank: true,
- rerankThreshold: 0,
- searchWeb: true,
- }),
- writingAssistant: new MetaSearchAgent({
- activeEngines: [],
- queryGeneratorPrompt: '',
- queryGeneratorFewShots: [],
- responsePrompt: prompts.writingAssistantPrompt,
- rerank: true,
- rerankThreshold: 0,
- searchWeb: false,
- }),
- wolframAlphaSearch: new MetaSearchAgent({
- activeEngines: ['wolframalpha'],
- queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
- responsePrompt: prompts.webSearchResponsePrompt,
- queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
- rerank: false,
- rerankThreshold: 0,
- searchWeb: true,
- }),
- youtubeSearch: new MetaSearchAgent({
- activeEngines: ['youtube'],
- queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
- responsePrompt: prompts.webSearchResponsePrompt,
- queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
- rerank: true,
- rerankThreshold: 0.3,
- searchWeb: true,
- }),
- redditSearch: new MetaSearchAgent({
- activeEngines: ['reddit'],
- queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
- responsePrompt: prompts.webSearchResponsePrompt,
- queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
- rerank: true,
- rerankThreshold: 0.3,
- searchWeb: true,
- }),
-};
diff --git a/src/lib/search/metaSearchAgent.ts b/src/lib/search/metaSearchAgent.ts
deleted file mode 100644
index 1f72f79..0000000
--- a/src/lib/search/metaSearchAgent.ts
+++ /dev/null
@@ -1,514 +0,0 @@
-import { ChatOpenAI } from '@langchain/openai';
-import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import type { Embeddings } from '@langchain/core/embeddings';
-import {
- ChatPromptTemplate,
- MessagesPlaceholder,
- PromptTemplate,
-} from '@langchain/core/prompts';
-import {
- RunnableLambda,
- RunnableMap,
- RunnableSequence,
-} from '@langchain/core/runnables';
-import { BaseMessage, BaseMessageLike } from '@langchain/core/messages';
-import { StringOutputParser } from '@langchain/core/output_parsers';
-import LineListOutputParser from '../outputParsers/listLineOutputParser';
-import LineOutputParser from '../outputParsers/lineOutputParser';
-import { getDocumentsFromLinks } from '../utils/documents';
-import { Document } from '@langchain/core/documents';
-import { searchSearxng } from '../searxng';
-import path from 'node:path';
-import fs from 'node:fs';
-import computeSimilarity from '../utils/computeSimilarity';
-import formatChatHistoryAsString from '../utils/formatHistory';
-import eventEmitter from 'events';
-import { StreamEvent } from '@langchain/core/tracers/log_stream';
-
-export interface MetaSearchAgentType {
- searchAndAnswer: (
- message: string,
- history: BaseMessage[],
- llm: BaseChatModel,
- embeddings: Embeddings,
- optimizationMode: 'speed' | 'balanced' | 'quality',
- fileIds: string[],
- systemInstructions: string,
- ) => Promise;
-}
-
-interface Config {
- searchWeb: boolean;
- rerank: boolean;
- rerankThreshold: number;
- queryGeneratorPrompt: string;
- queryGeneratorFewShots: BaseMessageLike[];
- responsePrompt: string;
- activeEngines: string[];
-}
-
-type BasicChainInput = {
- chat_history: BaseMessage[];
- query: string;
-};
-
-class MetaSearchAgent implements MetaSearchAgentType {
- private config: Config;
- private strParser = new StringOutputParser();
-
- constructor(config: Config) {
- this.config = config;
- }
-
- private async createSearchRetrieverChain(llm: BaseChatModel) {
- (llm as unknown as ChatOpenAI).temperature = 0;
-
- return RunnableSequence.from([
- ChatPromptTemplate.fromMessages([
- ['system', this.config.queryGeneratorPrompt],
- ...this.config.queryGeneratorFewShots,
- [
- 'user',
- `
-
- {chat_history}
-
-
-
- {query}
-
- `,
- ],
- ]),
- llm,
- this.strParser,
- RunnableLambda.from(async (input: string) => {
- const linksOutputParser = new LineListOutputParser({
- key: 'links',
- });
-
- const questionOutputParser = new LineOutputParser({
- key: 'question',
- });
-
- const links = await linksOutputParser.parse(input);
- let question = (await questionOutputParser.parse(input)) ?? input;
-
- if (question === 'not_needed') {
- return { query: '', docs: [] };
- }
-
- if (links.length > 0) {
- if (question.length === 0) {
- question = 'summarize';
- }
-
- let docs: Document[] = [];
-
- const linkDocs = await getDocumentsFromLinks({ links });
-
- const docGroups: Document[] = [];
-
- linkDocs.map((doc) => {
- const URLDocExists = docGroups.find(
- (d) =>
- d.metadata.url === doc.metadata.url &&
- d.metadata.totalDocs < 10,
- );
-
- if (!URLDocExists) {
- docGroups.push({
- ...doc,
- metadata: {
- ...doc.metadata,
- totalDocs: 1,
- },
- });
- }
-
- const docIndex = docGroups.findIndex(
- (d) =>
- d.metadata.url === doc.metadata.url &&
- d.metadata.totalDocs < 10,
- );
-
- if (docIndex !== -1) {
- docGroups[docIndex].pageContent =
- docGroups[docIndex].pageContent + `\n\n` + doc.pageContent;
- docGroups[docIndex].metadata.totalDocs += 1;
- }
- });
-
- await Promise.all(
- docGroups.map(async (doc) => {
- const res = await llm.invoke(`
- You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the
- text into a detailed, 2-4 paragraph explanation that captures the main ideas and provides a comprehensive answer to the query.
- If the query is \"summarize\", you should provide a detailed summary of the text. If the query is a specific question, you should answer it in the summary.
-
- - **Journalistic tone**: The summary should sound professional and journalistic, not too casual or vague.
- - **Thorough and detailed**: Ensure that every key point from the text is captured and that the summary directly answers the query.
- - **Not too lengthy, but detailed**: The summary should be informative but not excessively long. Focus on providing detailed information in a concise format.
-
- The text will be shared inside the \`text\` XML tag, and the query inside the \`query\` XML tag.
-
-
- 1. \`
- Docker is a set of platform-as-a-service products that use OS-level virtualization to deliver software in packages called containers.
- It was first released in 2013 and is developed by Docker, Inc. Docker is designed to make it easier to create, deploy, and run applications
- by using containers.
-
-
-
- What is Docker and how does it work?
-
-
- Response:
- Docker is a revolutionary platform-as-a-service product developed by Docker, Inc., that uses container technology to make application
- deployment more efficient. It allows developers to package their software with all necessary dependencies, making it easier to run in
- any environment. Released in 2013, Docker has transformed the way applications are built, deployed, and managed.
- \`
- 2. \`
- The theory of relativity, or simply relativity, encompasses two interrelated theories of Albert Einstein: special relativity and general
- relativity. However, the word "relativity" is sometimes used in reference to Galilean invariance. The term "theory of relativity" was based
- on the expression "relative theory" used by Max Planck in 1906. The theory of relativity usually encompasses two interrelated theories by
- Albert Einstein: special relativity and general relativity. Special relativity applies to all physical phenomena in the absence of gravity.
- General relativity explains the law of gravitation and its relation to other forces of nature. It applies to the cosmological and astrophysical
- realm, including astronomy.
-
-
-
- summarize
-
-
- Response:
- The theory of relativity, developed by Albert Einstein, encompasses two main theories: special relativity and general relativity. Special
- relativity applies to all physical phenomena in the absence of gravity, while general relativity explains the law of gravitation and its
- relation to other forces of nature. The theory of relativity is based on the concept of "relative theory," as introduced by Max Planck in
- 1906. It is a fundamental theory in physics that has revolutionized our understanding of the universe.
- \`
-
-
- Everything below is the actual data you will be working with. Good luck!
-
-
- ${question}
-
-
-
- ${doc.pageContent}
-
-
- Make sure to answer the query in the summary.
- `);
-
- const document = new Document({
- pageContent: res.content as string,
- metadata: {
- title: doc.metadata.title,
- url: doc.metadata.url,
- },
- });
-
- docs.push(document);
- }),
- );
-
- return { query: question, docs: docs };
- } else {
- question = question.replace(/.*?<\/think>/g, '');
-
- const res = await searchSearxng(question, {
- language: 'en',
- engines: this.config.activeEngines,
- });
-
- const documents = res.results.map(
- (result) =>
- new Document({
- pageContent:
- result.content ||
- (this.config.activeEngines.includes('youtube')
- ? result.title
- : '') /* Todo: Implement transcript grabbing using Youtubei (source: https://www.npmjs.com/package/youtubei) */,
- metadata: {
- title: result.title,
- url: result.url,
- ...(result.img_src && { img_src: result.img_src }),
- },
- }),
- );
-
- return { query: question, docs: documents };
- }
- }),
- ]);
- }
-
- private async createAnsweringChain(
- llm: BaseChatModel,
- fileIds: string[],
- embeddings: Embeddings,
- optimizationMode: 'speed' | 'balanced' | 'quality',
- systemInstructions: string,
- ) {
- return RunnableSequence.from([
- RunnableMap.from({
- systemInstructions: () => systemInstructions,
- query: (input: BasicChainInput) => input.query,
- chat_history: (input: BasicChainInput) => input.chat_history,
- date: () => new Date().toISOString(),
- context: RunnableLambda.from(async (input: BasicChainInput) => {
- const processedHistory = formatChatHistoryAsString(
- input.chat_history,
- );
-
- let docs: Document[] | null = null;
- let query = input.query;
-
- if (this.config.searchWeb) {
- const searchRetrieverChain =
- await this.createSearchRetrieverChain(llm);
-
- const searchRetrieverResult = await searchRetrieverChain.invoke({
- chat_history: processedHistory,
- query,
- });
-
- query = searchRetrieverResult.query;
- docs = searchRetrieverResult.docs;
- }
-
- const sortedDocs = await this.rerankDocs(
- query,
- docs ?? [],
- fileIds,
- embeddings,
- optimizationMode,
- );
-
- return sortedDocs;
- })
- .withConfig({
- runName: 'FinalSourceRetriever',
- })
- .pipe(this.processDocs),
- }),
- ChatPromptTemplate.fromMessages([
- ['system', this.config.responsePrompt],
- new MessagesPlaceholder('chat_history'),
- ['user', '{query}'],
- ]),
- llm,
- this.strParser,
- ]).withConfig({
- runName: 'FinalResponseGenerator',
- });
- }
-
- private async rerankDocs(
- query: string,
- docs: Document[],
- fileIds: string[],
- embeddings: Embeddings,
- optimizationMode: 'speed' | 'balanced' | 'quality',
- ) {
- if (docs.length === 0 && fileIds.length === 0) {
- return docs;
- }
-
- const filesData = fileIds
- .map((file) => {
- const filePath = path.join(process.cwd(), 'uploads', file);
-
- const contentPath = filePath + '-extracted.json';
- const embeddingsPath = filePath + '-embeddings.json';
-
- const content = JSON.parse(fs.readFileSync(contentPath, 'utf8'));
- const embeddings = JSON.parse(fs.readFileSync(embeddingsPath, 'utf8'));
-
- const fileSimilaritySearchObject = content.contents.map(
- (c: string, i: number) => {
- return {
- fileName: content.title,
- content: c,
- embeddings: embeddings.embeddings[i],
- };
- },
- );
-
- return fileSimilaritySearchObject;
- })
- .flat();
-
- if (query.toLocaleLowerCase() === 'summarize') {
- return docs.slice(0, 15);
- }
-
- const docsWithContent = docs.filter(
- (doc) => doc.pageContent && doc.pageContent.length > 0,
- );
-
- if (optimizationMode === 'speed' || this.config.rerank === false) {
- if (filesData.length > 0) {
- const [queryEmbedding] = await Promise.all([
- embeddings.embedQuery(query),
- ]);
-
- const fileDocs = filesData.map((fileData) => {
- return new Document({
- pageContent: fileData.content,
- metadata: {
- title: fileData.fileName,
- url: `File`,
- },
- });
- });
-
- const similarity = filesData.map((fileData, i) => {
- const sim = computeSimilarity(queryEmbedding, fileData.embeddings);
-
- return {
- index: i,
- similarity: sim,
- };
- });
-
- let sortedDocs = similarity
- .filter(
- (sim) => sim.similarity > (this.config.rerankThreshold ?? 0.3),
- )
- .sort((a, b) => b.similarity - a.similarity)
- .slice(0, 15)
- .map((sim) => fileDocs[sim.index]);
-
- sortedDocs =
- docsWithContent.length > 0 ? sortedDocs.slice(0, 8) : sortedDocs;
-
- return [
- ...sortedDocs,
- ...docsWithContent.slice(0, 15 - sortedDocs.length),
- ];
- } else {
- return docsWithContent.slice(0, 15);
- }
- } else if (optimizationMode === 'balanced') {
- const [docEmbeddings, queryEmbedding] = await Promise.all([
- embeddings.embedDocuments(
- docsWithContent.map((doc) => doc.pageContent),
- ),
- embeddings.embedQuery(query),
- ]);
-
- docsWithContent.push(
- ...filesData.map((fileData) => {
- return new Document({
- pageContent: fileData.content,
- metadata: {
- title: fileData.fileName,
- url: `File`,
- },
- });
- }),
- );
-
- docEmbeddings.push(...filesData.map((fileData) => fileData.embeddings));
-
- const similarity = docEmbeddings.map((docEmbedding, i) => {
- const sim = computeSimilarity(queryEmbedding, docEmbedding);
-
- return {
- index: i,
- similarity: sim,
- };
- });
-
- const sortedDocs = similarity
- .filter((sim) => sim.similarity > (this.config.rerankThreshold ?? 0.3))
- .sort((a, b) => b.similarity - a.similarity)
- .slice(0, 15)
- .map((sim) => docsWithContent[sim.index]);
-
- return sortedDocs;
- }
-
- return [];
- }
-
- private processDocs(docs: Document[]) {
- return docs
- .map(
- (_, index) =>
- `${index + 1}. ${docs[index].metadata.title} ${docs[index].pageContent}`,
- )
- .join('\n');
- }
-
- private async handleStream(
- stream: AsyncGenerator,
- emitter: eventEmitter,
- ) {
- for await (const event of stream) {
- if (
- event.event === 'on_chain_end' &&
- event.name === 'FinalSourceRetriever'
- ) {
- emitter.emit(
- 'data',
- JSON.stringify({ type: 'sources', data: event.data.output }),
- );
- }
- if (
- event.event === 'on_chain_stream' &&
- event.name === 'FinalResponseGenerator'
- ) {
- emitter.emit(
- 'data',
- JSON.stringify({ type: 'response', data: event.data.chunk }),
- );
- }
- if (
- event.event === 'on_chain_end' &&
- event.name === 'FinalResponseGenerator'
- ) {
- emitter.emit('end');
- }
- }
- }
-
- async searchAndAnswer(
- message: string,
- history: BaseMessage[],
- llm: BaseChatModel,
- embeddings: Embeddings,
- optimizationMode: 'speed' | 'balanced' | 'quality',
- fileIds: string[],
- systemInstructions: string,
- ) {
- const emitter = new eventEmitter();
-
- const answeringChain = await this.createAnsweringChain(
- llm,
- fileIds,
- embeddings,
- optimizationMode,
- systemInstructions,
- );
-
- const stream = answeringChain.streamEvents(
- {
- chat_history: history,
- query: message,
- },
- {
- version: 'v1',
- },
- );
-
- this.handleStream(stream, emitter);
-
- return emitter;
- }
-}
-
-export default MetaSearchAgent;
From 8d04f636d05d014c86fe40cd13881476d9e08f5b Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sat, 22 Nov 2025 22:22:43 +0530
Subject: [PATCH 033/199] Delete index.ts
---
src/lib/prompts/index.ts | 13 -------------
1 file changed, 13 deletions(-)
delete mode 100644 src/lib/prompts/index.ts
diff --git a/src/lib/prompts/index.ts b/src/lib/prompts/index.ts
deleted file mode 100644
index fd1a85a..0000000
--- a/src/lib/prompts/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {
- webSearchResponsePrompt,
- webSearchRetrieverFewShots,
- webSearchRetrieverPrompt,
-} from './webSearch';
-import { writingAssistantPrompt } from './writingAssistant';
-
-export default {
- webSearchResponsePrompt,
- webSearchRetrieverPrompt,
- webSearchRetrieverFewShots,
- writingAssistantPrompt,
-};
From d6c364fdcbd5e10cdef2da961976e7952ee83990 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sat, 22 Nov 2025 22:23:10 +0530
Subject: [PATCH 034/199] feat(models): remove old providers
---
src/lib/models/providers/aiml.ts | 152 ----------------------
src/lib/models/providers/anthropic.ts | 115 -----------------
src/lib/models/providers/deepseek.ts | 107 ---------------
src/lib/models/providers/gemini.ts | 145 ---------------------
src/lib/models/providers/groq.ts | 118 -----------------
src/lib/models/providers/lemonade.ts | 158 -----------------------
src/lib/models/providers/lmstudio.ts | 148 ---------------------
src/lib/models/providers/transformers.ts | 87 -------------
8 files changed, 1030 deletions(-)
delete mode 100644 src/lib/models/providers/aiml.ts
delete mode 100644 src/lib/models/providers/anthropic.ts
delete mode 100644 src/lib/models/providers/deepseek.ts
delete mode 100644 src/lib/models/providers/gemini.ts
delete mode 100644 src/lib/models/providers/groq.ts
delete mode 100644 src/lib/models/providers/lemonade.ts
delete mode 100644 src/lib/models/providers/lmstudio.ts
delete mode 100644 src/lib/models/providers/transformers.ts
diff --git a/src/lib/models/providers/aiml.ts b/src/lib/models/providers/aiml.ts
deleted file mode 100644
index 35ccf79..0000000
--- a/src/lib/models/providers/aiml.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Model, ModelList, ProviderMetadata } from '../types';
-import BaseModelProvider from './baseProvider';
-import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
-import { Embeddings } from '@langchain/core/embeddings';
-import { UIConfigField } from '@/lib/config/types';
-import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
-
-interface AimlConfig {
- apiKey: string;
-}
-
-const providerConfigFields: UIConfigField[] = [
- {
- type: 'password',
- name: 'API Key',
- key: 'apiKey',
- description: 'Your AI/ML API key',
- required: true,
- placeholder: 'AI/ML API Key',
- env: 'AIML_API_KEY',
- scope: 'server',
- },
-];
-
-class AimlProvider extends BaseModelProvider {
- constructor(id: string, name: string, config: AimlConfig) {
- super(id, name, config);
- }
-
- async getDefaultModels(): Promise {
- try {
- const res = await fetch('https://api.aimlapi.com/models', {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${this.config.apiKey}`,
- },
- });
-
- const data = await res.json();
-
- const chatModels: Model[] = data.data
- .filter((m: any) => m.type === 'chat-completion')
- .map((m: any) => {
- return {
- name: m.id,
- key: m.id,
- };
- });
-
- const embeddingModels: Model[] = data.data
- .filter((m: any) => m.type === 'embedding')
- .map((m: any) => {
- return {
- name: m.id,
- key: m.id,
- };
- });
-
- return {
- embedding: embeddingModels,
- chat: chatModels,
- };
- } catch (err) {
- if (err instanceof TypeError) {
- throw new Error(
- 'Error connecting to AI/ML API. Please ensure your API key is correct and the service is available.',
- );
- }
-
- throw err;
- }
- }
-
- async getModelList(): Promise {
- const defaultModels = await this.getDefaultModels();
- const configProvider = getConfiguredModelProviderById(this.id)!;
-
- return {
- embedding: [
- ...defaultModels.embedding,
- ...configProvider.embeddingModels,
- ],
- chat: [...defaultModels.chat, ...configProvider.chatModels],
- };
- }
-
- async loadChatModel(key: string): Promise {
- const modelList = await this.getModelList();
-
- const exists = modelList.chat.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error(
- 'Error Loading AI/ML API Chat Model. Invalid Model Selected',
- );
- }
-
- return new ChatOpenAI({
- apiKey: this.config.apiKey,
- temperature: 0.7,
- model: key,
- configuration: {
- baseURL: 'https://api.aimlapi.com',
- },
- });
- }
-
- async loadEmbeddingModel(key: string): Promise {
- const modelList = await this.getModelList();
- const exists = modelList.embedding.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error(
- 'Error Loading AI/ML API Embedding Model. Invalid Model Selected.',
- );
- }
-
- return new OpenAIEmbeddings({
- apiKey: this.config.apiKey,
- model: key,
- configuration: {
- baseURL: 'https://api.aimlapi.com',
- },
- });
- }
-
- static parseAndValidate(raw: any): AimlConfig {
- if (!raw || typeof raw !== 'object')
- throw new Error('Invalid config provided. Expected object');
- if (!raw.apiKey)
- throw new Error('Invalid config provided. API key must be provided');
-
- return {
- apiKey: String(raw.apiKey),
- };
- }
-
- static getProviderConfigFields(): UIConfigField[] {
- return providerConfigFields;
- }
-
- static getProviderMetadata(): ProviderMetadata {
- return {
- key: 'aiml',
- name: 'AI/ML API',
- };
- }
-}
-
-export default AimlProvider;
diff --git a/src/lib/models/providers/anthropic.ts b/src/lib/models/providers/anthropic.ts
deleted file mode 100644
index e071159..0000000
--- a/src/lib/models/providers/anthropic.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Model, ModelList, ProviderMetadata } from '../types';
-import BaseModelProvider from './baseProvider';
-import { ChatAnthropic } from '@langchain/anthropic';
-import { Embeddings } from '@langchain/core/embeddings';
-import { UIConfigField } from '@/lib/config/types';
-import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
-
-interface AnthropicConfig {
- apiKey: string;
-}
-
-const providerConfigFields: UIConfigField[] = [
- {
- type: 'password',
- name: 'API Key',
- key: 'apiKey',
- description: 'Your Anthropic API key',
- required: true,
- placeholder: 'Anthropic API Key',
- env: 'ANTHROPIC_API_KEY',
- scope: 'server',
- },
-];
-
-class AnthropicProvider extends BaseModelProvider {
- constructor(id: string, name: string, config: AnthropicConfig) {
- super(id, name, config);
- }
-
- async getDefaultModels(): Promise {
- const res = await fetch('https://api.anthropic.com/v1/models?limit=999', {
- method: 'GET',
- headers: {
- 'x-api-key': this.config.apiKey,
- 'anthropic-version': '2023-06-01',
- 'Content-type': 'application/json',
- },
- });
-
- if (!res.ok) {
- throw new Error(`Failed to fetch Anthropic models: ${res.statusText}`);
- }
-
- const data = (await res.json()).data;
-
- const models: Model[] = data.map((m: any) => {
- return {
- key: m.id,
- name: m.display_name,
- };
- });
-
- return {
- embedding: [],
- chat: models,
- };
- }
-
- async getModelList(): Promise {
- const defaultModels = await this.getDefaultModels();
- const configProvider = getConfiguredModelProviderById(this.id)!;
-
- return {
- embedding: [],
- chat: [...defaultModels.chat, ...configProvider.chatModels],
- };
- }
-
- async loadChatModel(key: string): Promise {
- const modelList = await this.getModelList();
-
- const exists = modelList.chat.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error(
- 'Error Loading Anthropic Chat Model. Invalid Model Selected',
- );
- }
-
- return new ChatAnthropic({
- apiKey: this.config.apiKey,
- temperature: 0.7,
- model: key,
- });
- }
-
- async loadEmbeddingModel(key: string): Promise {
- throw new Error('Anthropic provider does not support embedding models.');
- }
-
- static parseAndValidate(raw: any): AnthropicConfig {
- if (!raw || typeof raw !== 'object')
- throw new Error('Invalid config provided. Expected object');
- if (!raw.apiKey)
- throw new Error('Invalid config provided. API key must be provided');
-
- return {
- apiKey: String(raw.apiKey),
- };
- }
-
- static getProviderConfigFields(): UIConfigField[] {
- return providerConfigFields;
- }
-
- static getProviderMetadata(): ProviderMetadata {
- return {
- key: 'anthropic',
- name: 'Anthropic',
- };
- }
-}
-
-export default AnthropicProvider;
diff --git a/src/lib/models/providers/deepseek.ts b/src/lib/models/providers/deepseek.ts
deleted file mode 100644
index 9b29d83..0000000
--- a/src/lib/models/providers/deepseek.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Model, ModelList, ProviderMetadata } from '../types';
-import BaseModelProvider from './baseProvider';
-import { ChatOpenAI } from '@langchain/openai';
-import { Embeddings } from '@langchain/core/embeddings';
-import { UIConfigField } from '@/lib/config/types';
-import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
-
-interface DeepSeekConfig {
- apiKey: string;
-}
-
-const defaultChatModels: Model[] = [
- {
- name: 'Deepseek Chat / DeepSeek V3.2 Exp',
- key: 'deepseek-chat',
- },
- {
- name: 'Deepseek Reasoner / DeepSeek V3.2 Exp',
- key: 'deepseek-reasoner',
- },
-];
-
-const providerConfigFields: UIConfigField[] = [
- {
- type: 'password',
- name: 'API Key',
- key: 'apiKey',
- description: 'Your DeepSeek API key',
- required: true,
- placeholder: 'DeepSeek API Key',
- env: 'DEEPSEEK_API_KEY',
- scope: 'server',
- },
-];
-
-class DeepSeekProvider extends BaseModelProvider {
- constructor(id: string, name: string, config: DeepSeekConfig) {
- super(id, name, config);
- }
-
- async getDefaultModels(): Promise {
- return {
- embedding: [],
- chat: defaultChatModels,
- };
- }
-
- async getModelList(): Promise {
- const defaultModels = await this.getDefaultModels();
- const configProvider = getConfiguredModelProviderById(this.id)!;
-
- return {
- embedding: [],
- chat: [...defaultModels.chat, ...configProvider.chatModels],
- };
- }
-
- async loadChatModel(key: string): Promise {
- const modelList = await this.getModelList();
-
- const exists = modelList.chat.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error(
- 'Error Loading DeepSeek Chat Model. Invalid Model Selected',
- );
- }
-
- return new ChatOpenAI({
- apiKey: this.config.apiKey,
- temperature: 0.7,
- model: key,
- configuration: {
- baseURL: 'https://api.deepseek.com',
- },
- });
- }
-
- async loadEmbeddingModel(key: string): Promise {
- throw new Error('DeepSeek provider does not support embedding models.');
- }
-
- static parseAndValidate(raw: any): DeepSeekConfig {
- if (!raw || typeof raw !== 'object')
- throw new Error('Invalid config provided. Expected object');
- if (!raw.apiKey)
- throw new Error('Invalid config provided. API key must be provided');
-
- return {
- apiKey: String(raw.apiKey),
- };
- }
-
- static getProviderConfigFields(): UIConfigField[] {
- return providerConfigFields;
- }
-
- static getProviderMetadata(): ProviderMetadata {
- return {
- key: 'deepseek',
- name: 'Deepseek AI',
- };
- }
-}
-
-export default DeepSeekProvider;
diff --git a/src/lib/models/providers/gemini.ts b/src/lib/models/providers/gemini.ts
deleted file mode 100644
index 6cfd913..0000000
--- a/src/lib/models/providers/gemini.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Model, ModelList, ProviderMetadata } from '../types';
-import BaseModelProvider from './baseProvider';
-import {
- ChatGoogleGenerativeAI,
- GoogleGenerativeAIEmbeddings,
-} from '@langchain/google-genai';
-import { Embeddings } from '@langchain/core/embeddings';
-import { UIConfigField } from '@/lib/config/types';
-import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
-
-interface GeminiConfig {
- apiKey: string;
-}
-
-const providerConfigFields: UIConfigField[] = [
- {
- type: 'password',
- name: 'API Key',
- key: 'apiKey',
- description: 'Your Google Gemini API key',
- required: true,
- placeholder: 'Google Gemini API Key',
- env: 'GEMINI_API_KEY',
- scope: 'server',
- },
-];
-
-class GeminiProvider extends BaseModelProvider {
- constructor(id: string, name: string, config: GeminiConfig) {
- super(id, name, config);
- }
-
- async getDefaultModels(): Promise {
- const res = await fetch(
- `https://generativelanguage.googleapis.com/v1beta/models?key=${this.config.apiKey}`,
- {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- },
- },
- );
-
- const data = await res.json();
-
- let defaultEmbeddingModels: Model[] = [];
- let defaultChatModels: Model[] = [];
-
- data.models.forEach((m: any) => {
- if (
- m.supportedGenerationMethods.some(
- (genMethod: string) =>
- genMethod === 'embedText' || genMethod === 'embedContent',
- )
- ) {
- defaultEmbeddingModels.push({
- key: m.name,
- name: m.displayName,
- });
- } else if (m.supportedGenerationMethods.includes('generateContent')) {
- defaultChatModels.push({
- key: m.name,
- name: m.displayName,
- });
- }
- });
-
- return {
- embedding: defaultEmbeddingModels,
- chat: defaultChatModels,
- };
- }
-
- async getModelList(): Promise {
- const defaultModels = await this.getDefaultModels();
- const configProvider = getConfiguredModelProviderById(this.id)!;
-
- return {
- embedding: [
- ...defaultModels.embedding,
- ...configProvider.embeddingModels,
- ],
- chat: [...defaultModels.chat, ...configProvider.chatModels],
- };
- }
-
- async loadChatModel(key: string): Promise {
- const modelList = await this.getModelList();
-
- const exists = modelList.chat.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error(
- 'Error Loading Gemini Chat Model. Invalid Model Selected',
- );
- }
-
- return new ChatGoogleGenerativeAI({
- apiKey: this.config.apiKey,
- temperature: 0.7,
- model: key,
- });
- }
-
- async loadEmbeddingModel(key: string): Promise {
- const modelList = await this.getModelList();
- const exists = modelList.embedding.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error(
- 'Error Loading Gemini Embedding Model. Invalid Model Selected.',
- );
- }
-
- return new GoogleGenerativeAIEmbeddings({
- apiKey: this.config.apiKey,
- model: key,
- });
- }
-
- static parseAndValidate(raw: any): GeminiConfig {
- if (!raw || typeof raw !== 'object')
- throw new Error('Invalid config provided. Expected object');
- if (!raw.apiKey)
- throw new Error('Invalid config provided. API key must be provided');
-
- return {
- apiKey: String(raw.apiKey),
- };
- }
-
- static getProviderConfigFields(): UIConfigField[] {
- return providerConfigFields;
- }
-
- static getProviderMetadata(): ProviderMetadata {
- return {
- key: 'gemini',
- name: 'Google Gemini',
- };
- }
-}
-
-export default GeminiProvider;
diff --git a/src/lib/models/providers/groq.ts b/src/lib/models/providers/groq.ts
deleted file mode 100644
index a87ea88..0000000
--- a/src/lib/models/providers/groq.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Model, ModelList, ProviderMetadata } from '../types';
-import BaseModelProvider from './baseProvider';
-import { ChatGroq } from '@langchain/groq';
-import { Embeddings } from '@langchain/core/embeddings';
-import { UIConfigField } from '@/lib/config/types';
-import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
-
-interface GroqConfig {
- apiKey: string;
-}
-
-const providerConfigFields: UIConfigField[] = [
- {
- type: 'password',
- name: 'API Key',
- key: 'apiKey',
- description: 'Your Groq API key',
- required: true,
- placeholder: 'Groq API Key',
- env: 'GROQ_API_KEY',
- scope: 'server',
- },
-];
-
-class GroqProvider extends BaseModelProvider {
- constructor(id: string, name: string, config: GroqConfig) {
- super(id, name, config);
- }
-
- async getDefaultModels(): Promise {
- try {
- const res = await fetch('https://api.groq.com/openai/v1/models', {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${this.config.apiKey}`,
- },
- });
-
- const data = await res.json();
-
- const models: Model[] = data.data.map((m: any) => {
- return {
- name: m.id,
- key: m.id,
- };
- });
-
- return {
- embedding: [],
- chat: models,
- };
- } catch (err) {
- if (err instanceof TypeError) {
- throw new Error(
- 'Error connecting to Groq API. Please ensure your API key is correct and the Groq service is available.',
- );
- }
-
- throw err;
- }
- }
-
- async getModelList(): Promise {
- const defaultModels = await this.getDefaultModels();
- const configProvider = getConfiguredModelProviderById(this.id)!;
-
- return {
- embedding: [],
- chat: [...defaultModels.chat, ...configProvider.chatModels],
- };
- }
-
- async loadChatModel(key: string): Promise {
- const modelList = await this.getModelList();
-
- const exists = modelList.chat.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error('Error Loading Groq Chat Model. Invalid Model Selected');
- }
-
- return new ChatGroq({
- apiKey: this.config.apiKey,
- temperature: 0.7,
- model: key,
- });
- }
-
- async loadEmbeddingModel(key: string): Promise {
- throw new Error('Groq provider does not support embedding models.');
- }
-
- static parseAndValidate(raw: any): GroqConfig {
- if (!raw || typeof raw !== 'object')
- throw new Error('Invalid config provided. Expected object');
- if (!raw.apiKey)
- throw new Error('Invalid config provided. API key must be provided');
-
- return {
- apiKey: String(raw.apiKey),
- };
- }
-
- static getProviderConfigFields(): UIConfigField[] {
- return providerConfigFields;
- }
-
- static getProviderMetadata(): ProviderMetadata {
- return {
- key: 'groq',
- name: 'Groq',
- };
- }
-}
-
-export default GroqProvider;
diff --git a/src/lib/models/providers/lemonade.ts b/src/lib/models/providers/lemonade.ts
deleted file mode 100644
index 20680a8..0000000
--- a/src/lib/models/providers/lemonade.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Model, ModelList, ProviderMetadata } from '../types';
-import BaseModelProvider from './baseProvider';
-import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
-import { Embeddings } from '@langchain/core/embeddings';
-import { UIConfigField } from '@/lib/config/types';
-import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
-
-interface LemonadeConfig {
- baseURL: string;
- apiKey?: string;
-}
-
-const providerConfigFields: UIConfigField[] = [
- {
- type: 'string',
- name: 'Base URL',
- key: 'baseURL',
- description: 'The base URL for Lemonade API',
- required: true,
- placeholder: 'https://api.lemonade.ai/v1',
- env: 'LEMONADE_BASE_URL',
- scope: 'server',
- },
- {
- type: 'password',
- name: 'API Key',
- key: 'apiKey',
- description: 'Your Lemonade API key (optional)',
- required: false,
- placeholder: 'Lemonade API Key',
- env: 'LEMONADE_API_KEY',
- scope: 'server',
- },
-];
-
-class LemonadeProvider extends BaseModelProvider {
- constructor(id: string, name: string, config: LemonadeConfig) {
- super(id, name, config);
- }
-
- async getDefaultModels(): Promise {
- try {
- const headers: Record = {
- 'Content-Type': 'application/json',
- };
-
- if (this.config.apiKey) {
- headers['Authorization'] = `Bearer ${this.config.apiKey}`;
- }
-
- const res = await fetch(`${this.config.baseURL}/models`, {
- method: 'GET',
- headers,
- });
-
- const data = await res.json();
-
- const models: Model[] = data.data.map((m: any) => {
- return {
- name: m.id,
- key: m.id,
- };
- });
-
- return {
- embedding: models,
- chat: models,
- };
- } catch (err) {
- if (err instanceof TypeError) {
- throw new Error(
- 'Error connecting to Lemonade API. Please ensure the base URL is correct and the service is available.',
- );
- }
-
- throw err;
- }
- }
-
- async getModelList(): Promise {
- const defaultModels = await this.getDefaultModels();
- const configProvider = getConfiguredModelProviderById(this.id)!;
-
- return {
- embedding: [
- ...defaultModels.embedding,
- ...configProvider.embeddingModels,
- ],
- chat: [...defaultModels.chat, ...configProvider.chatModels],
- };
- }
-
- async loadChatModel(key: string): Promise {
- const modelList = await this.getModelList();
-
- const exists = modelList.chat.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error(
- 'Error Loading Lemonade Chat Model. Invalid Model Selected',
- );
- }
-
- return new ChatOpenAI({
- apiKey: this.config.apiKey || 'not-needed',
- temperature: 0.7,
- model: key,
- configuration: {
- baseURL: this.config.baseURL,
- },
- });
- }
-
- async loadEmbeddingModel(key: string): Promise {
- const modelList = await this.getModelList();
- const exists = modelList.embedding.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error(
- 'Error Loading Lemonade Embedding Model. Invalid Model Selected.',
- );
- }
-
- return new OpenAIEmbeddings({
- apiKey: this.config.apiKey || 'not-needed',
- model: key,
- configuration: {
- baseURL: this.config.baseURL,
- },
- });
- }
-
- static parseAndValidate(raw: any): LemonadeConfig {
- if (!raw || typeof raw !== 'object')
- throw new Error('Invalid config provided. Expected object');
- if (!raw.baseURL)
- throw new Error('Invalid config provided. Base URL must be provided');
-
- return {
- baseURL: String(raw.baseURL),
- apiKey: raw.apiKey ? String(raw.apiKey) : undefined,
- };
- }
-
- static getProviderConfigFields(): UIConfigField[] {
- return providerConfigFields;
- }
-
- static getProviderMetadata(): ProviderMetadata {
- return {
- key: 'lemonade',
- name: 'Lemonade',
- };
- }
-}
-
-export default LemonadeProvider;
diff --git a/src/lib/models/providers/lmstudio.ts b/src/lib/models/providers/lmstudio.ts
deleted file mode 100644
index 3a73a34..0000000
--- a/src/lib/models/providers/lmstudio.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Model, ModelList, ProviderMetadata } from '../types';
-import BaseModelProvider from './baseProvider';
-import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
-import { Embeddings } from '@langchain/core/embeddings';
-import { UIConfigField } from '@/lib/config/types';
-import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
-
-interface LMStudioConfig {
- baseURL: string;
-}
-
-const providerConfigFields: UIConfigField[] = [
- {
- type: 'string',
- name: 'Base URL',
- key: 'baseURL',
- description: 'The base URL for LM Studio server',
- required: true,
- placeholder: 'http://localhost:1234',
- env: 'LM_STUDIO_BASE_URL',
- scope: 'server',
- },
-];
-
-class LMStudioProvider extends BaseModelProvider {
- constructor(id: string, name: string, config: LMStudioConfig) {
- super(id, name, config);
- }
-
- private normalizeBaseURL(url: string): string {
- const trimmed = url.trim().replace(/\/+$/, '');
- return trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`;
- }
-
- async getDefaultModels(): Promise {
- try {
- const baseURL = this.normalizeBaseURL(this.config.baseURL);
-
- const res = await fetch(`${baseURL}/models`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- },
- });
-
- const data = await res.json();
-
- const models: Model[] = data.data.map((m: any) => {
- return {
- name: m.id,
- key: m.id,
- };
- });
-
- return {
- embedding: models,
- chat: models,
- };
- } catch (err) {
- if (err instanceof TypeError) {
- throw new Error(
- 'Error connecting to LM Studio. Please ensure the base URL is correct and the LM Studio server is running.',
- );
- }
-
- throw err;
- }
- }
-
- async getModelList(): Promise {
- const defaultModels = await this.getDefaultModels();
- const configProvider = getConfiguredModelProviderById(this.id)!;
-
- return {
- embedding: [
- ...defaultModels.embedding,
- ...configProvider.embeddingModels,
- ],
- chat: [...defaultModels.chat, ...configProvider.chatModels],
- };
- }
-
- async loadChatModel(key: string): Promise {
- const modelList = await this.getModelList();
-
- const exists = modelList.chat.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error(
- 'Error Loading LM Studio Chat Model. Invalid Model Selected',
- );
- }
-
- return new ChatOpenAI({
- apiKey: 'lm-studio',
- temperature: 0.7,
- model: key,
- streaming: true,
- configuration: {
- baseURL: this.normalizeBaseURL(this.config.baseURL),
- },
- });
- }
-
- async loadEmbeddingModel(key: string): Promise {
- const modelList = await this.getModelList();
- const exists = modelList.embedding.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error(
- 'Error Loading LM Studio Embedding Model. Invalid Model Selected.',
- );
- }
-
- return new OpenAIEmbeddings({
- apiKey: 'lm-studio',
- model: key,
- configuration: {
- baseURL: this.normalizeBaseURL(this.config.baseURL),
- },
- });
- }
-
- static parseAndValidate(raw: any): LMStudioConfig {
- if (!raw || typeof raw !== 'object')
- throw new Error('Invalid config provided. Expected object');
- if (!raw.baseURL)
- throw new Error('Invalid config provided. Base URL must be provided');
-
- return {
- baseURL: String(raw.baseURL),
- };
- }
-
- static getProviderConfigFields(): UIConfigField[] {
- return providerConfigFields;
- }
-
- static getProviderMetadata(): ProviderMetadata {
- return {
- key: 'lmstudio',
- name: 'LM Studio',
- };
- }
-}
-
-export default LMStudioProvider;
diff --git a/src/lib/models/providers/transformers.ts b/src/lib/models/providers/transformers.ts
deleted file mode 100644
index afd6b9e..0000000
--- a/src/lib/models/providers/transformers.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Model, ModelList, ProviderMetadata } from '../types';
-import BaseModelProvider from './baseProvider';
-import { Embeddings } from '@langchain/core/embeddings';
-import { UIConfigField } from '@/lib/config/types';
-import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
-import { HuggingFaceTransformersEmbeddings } from '@langchain/community/embeddings/huggingface_transformers';
-interface TransformersConfig {}
-
-const defaultEmbeddingModels: Model[] = [
- {
- name: 'all-MiniLM-L6-v2',
- key: 'Xenova/all-MiniLM-L6-v2',
- },
- {
- name: 'mxbai-embed-large-v1',
- key: 'mixedbread-ai/mxbai-embed-large-v1',
- },
- {
- name: 'nomic-embed-text-v1',
- key: 'Xenova/nomic-embed-text-v1',
- },
-];
-
-const providerConfigFields: UIConfigField[] = [];
-
-class TransformersProvider extends BaseModelProvider {
- constructor(id: string, name: string, config: TransformersConfig) {
- super(id, name, config);
- }
-
- async getDefaultModels(): Promise {
- return {
- embedding: [...defaultEmbeddingModels],
- chat: [],
- };
- }
-
- async getModelList(): Promise {
- const defaultModels = await this.getDefaultModels();
- const configProvider = getConfiguredModelProviderById(this.id)!;
-
- return {
- embedding: [
- ...defaultModels.embedding,
- ...configProvider.embeddingModels,
- ],
- chat: [],
- };
- }
-
- async loadChatModel(key: string): Promise {
- throw new Error('Transformers Provider does not support chat models.');
- }
-
- async loadEmbeddingModel(key: string): Promise {
- const modelList = await this.getModelList();
- const exists = modelList.embedding.find((m) => m.key === key);
-
- if (!exists) {
- throw new Error(
- 'Error Loading OpenAI Embedding Model. Invalid Model Selected.',
- );
- }
-
- return new HuggingFaceTransformersEmbeddings({
- model: key,
- });
- }
-
- static parseAndValidate(raw: any): TransformersConfig {
- return {};
- }
-
- static getProviderConfigFields(): UIConfigField[] {
- return providerConfigFields;
- }
-
- static getProviderMetadata(): ProviderMetadata {
- return {
- key: 'transformers',
- name: 'Transformers',
- };
- }
-}
-
-export default TransformersProvider;
From 6d35d60b49c3921c1c8155d71c045997ae6e617d Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:21:16 +0530
Subject: [PATCH 035/199] Remove unused output parsers and document utility
---
src/lib/outputParsers/lineOutputParser.ts | 48 ---------
src/lib/outputParsers/listLineOutputParser.ts | 50 ----------
src/lib/utils/documents.ts | 99 -------------------
3 files changed, 197 deletions(-)
delete mode 100644 src/lib/outputParsers/lineOutputParser.ts
delete mode 100644 src/lib/outputParsers/listLineOutputParser.ts
delete mode 100644 src/lib/utils/documents.ts
diff --git a/src/lib/outputParsers/lineOutputParser.ts b/src/lib/outputParsers/lineOutputParser.ts
deleted file mode 100644
index 5c795f2..0000000
--- a/src/lib/outputParsers/lineOutputParser.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { BaseOutputParser } from '@langchain/core/output_parsers';
-
-interface LineOutputParserArgs {
- key?: string;
-}
-
-class LineOutputParser extends BaseOutputParser {
- private key = 'questions';
-
- constructor(args?: LineOutputParserArgs) {
- super();
- this.key = args?.key ?? this.key;
- }
-
- static lc_name() {
- return 'LineOutputParser';
- }
-
- lc_namespace = ['langchain', 'output_parsers', 'line_output_parser'];
-
- async parse(text: string): Promise {
- text = text.trim() || '';
-
- const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
- const startKeyIndex = text.indexOf(`<${this.key}>`);
- const endKeyIndex = text.indexOf(`${this.key}>`);
-
- if (startKeyIndex === -1 || endKeyIndex === -1) {
- return undefined;
- }
-
- const questionsStartIndex =
- startKeyIndex === -1 ? 0 : startKeyIndex + `<${this.key}>`.length;
- const questionsEndIndex = endKeyIndex === -1 ? text.length : endKeyIndex;
- const line = text
- .slice(questionsStartIndex, questionsEndIndex)
- .trim()
- .replace(regex, '');
-
- return line;
- }
-
- getFormatInstructions(): string {
- throw new Error('Not implemented.');
- }
-}
-
-export default LineOutputParser;
diff --git a/src/lib/outputParsers/listLineOutputParser.ts b/src/lib/outputParsers/listLineOutputParser.ts
deleted file mode 100644
index 6409db9..0000000
--- a/src/lib/outputParsers/listLineOutputParser.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { BaseOutputParser } from '@langchain/core/output_parsers';
-
-interface LineListOutputParserArgs {
- key?: string;
-}
-
-class LineListOutputParser extends BaseOutputParser {
- private key = 'questions';
-
- constructor(args?: LineListOutputParserArgs) {
- super();
- this.key = args?.key ?? this.key;
- }
-
- static lc_name() {
- return 'LineListOutputParser';
- }
-
- lc_namespace = ['langchain', 'output_parsers', 'line_list_output_parser'];
-
- async parse(text: string): Promise {
- text = text.trim() || '';
-
- const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
- const startKeyIndex = text.indexOf(`<${this.key}>`);
- const endKeyIndex = text.indexOf(`${this.key}>`);
-
- if (startKeyIndex === -1 || endKeyIndex === -1) {
- return [];
- }
-
- const questionsStartIndex =
- startKeyIndex === -1 ? 0 : startKeyIndex + `<${this.key}>`.length;
- const questionsEndIndex = endKeyIndex === -1 ? text.length : endKeyIndex;
- const lines = text
- .slice(questionsStartIndex, questionsEndIndex)
- .trim()
- .split('\n')
- .filter((line) => line.trim() !== '')
- .map((line) => line.replace(regex, ''));
-
- return lines;
- }
-
- getFormatInstructions(): string {
- throw new Error('Not implemented.');
- }
-}
-
-export default LineListOutputParser;
diff --git a/src/lib/utils/documents.ts b/src/lib/utils/documents.ts
deleted file mode 100644
index 51509ff..0000000
--- a/src/lib/utils/documents.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import axios from 'axios';
-import { htmlToText } from 'html-to-text';
-import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
-import { Document } from '@langchain/core/documents';
-import pdfParse from 'pdf-parse';
-
-export const getDocumentsFromLinks = async ({ links }: { links: string[] }) => {
- const splitter = new RecursiveCharacterTextSplitter();
-
- let docs: Document[] = [];
-
- await Promise.all(
- links.map(async (link) => {
- link =
- link.startsWith('http://') || link.startsWith('https://')
- ? link
- : `https://${link}`;
-
- try {
- const res = await axios.get(link, {
- responseType: 'arraybuffer',
- });
-
- const isPdf = res.headers['content-type'] === 'application/pdf';
-
- if (isPdf) {
- const pdfText = await pdfParse(res.data);
- const parsedText = pdfText.text
- .replace(/(\r\n|\n|\r)/gm, ' ')
- .replace(/\s+/g, ' ')
- .trim();
-
- const splittedText = await splitter.splitText(parsedText);
- const title = 'PDF Document';
-
- const linkDocs = splittedText.map((text) => {
- return new Document({
- pageContent: text,
- metadata: {
- title: title,
- url: link,
- },
- });
- });
-
- docs.push(...linkDocs);
- return;
- }
-
- const parsedText = htmlToText(res.data.toString('utf8'), {
- selectors: [
- {
- selector: 'a',
- options: {
- ignoreHref: true,
- },
- },
- ],
- })
- .replace(/(\r\n|\n|\r)/gm, ' ')
- .replace(/\s+/g, ' ')
- .trim();
-
- const splittedText = await splitter.splitText(parsedText);
- const title = res.data
- .toString('utf8')
- .match(/(.*?)<\/title>/)?.[1];
-
- const linkDocs = splittedText.map((text) => {
- return new Document({
- pageContent: text,
- metadata: {
- title: title || link,
- url: link,
- },
- });
- });
-
- docs.push(...linkDocs);
- } catch (err) {
- console.error(
- 'An error occurred while getting documents from links: ',
- err,
- );
- docs.push(
- new Document({
- pageContent: `Failed to retrieve content from the link: ${err}`,
- metadata: {
- title: 'Failed to retrieve content',
- url: link,
- },
- }),
- );
- }
- }),
- );
-
- return docs;
-};
From d7dd17c0695ab182fbf4117162e2c650fe40d49a Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:22:11 +0530
Subject: [PATCH 036/199] feat(app): fix type resolving issues
---
src/lib/agents/search/researcher/actions/webSearch.ts | 1 +
src/lib/agents/search/types.ts | 1 +
src/lib/db/schema.ts | 1 +
src/lib/models/base/embedding.ts | 2 ++
src/lib/models/providers/ollama/ollamaEmbedding.ts | 1 +
src/lib/models/providers/openai/openaiEmbedding.ts | 1 +
src/lib/models/registry.ts | 4 +---
src/lib/models/types.ts | 5 +++--
8 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts
index 943afff..5ceb2ed 100644
--- a/src/lib/agents/search/researcher/actions/webSearch.ts
+++ b/src/lib/agents/search/researcher/actions/webSearch.ts
@@ -1,6 +1,7 @@
import z from 'zod';
import { ResearchAction } from '../../types';
import { searchSearxng } from '@/lib/searxng';
+import { Chunk } from '@/lib/types';
const actionSchema = z.object({
type: z.literal('web_search'),
diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts
index fc0735d..0914503 100644
--- a/src/lib/agents/search/types.ts
+++ b/src/lib/agents/search/types.ts
@@ -2,6 +2,7 @@ import z from 'zod';
import BaseLLM from '../../models/base/llm';
import BaseEmbedding from '@/lib/models/base/embedding';
import SessionManager from '@/lib/session';
+import { ChatTurnMessage, Chunk } from '@/lib/types';
export type SearchSources = 'web' | 'discussions' | 'academic';
diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts
index 71d441f..50dd14c 100644
--- a/src/lib/db/schema.ts
+++ b/src/lib/db/schema.ts
@@ -1,5 +1,6 @@
import { sql } from 'drizzle-orm';
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';
+import { Block } from '../types';
export const messages = sqliteTable('messages', {
id: integer('id').primaryKey(),
diff --git a/src/lib/models/base/embedding.ts b/src/lib/models/base/embedding.ts
index 35413ed..a817605 100644
--- a/src/lib/models/base/embedding.ts
+++ b/src/lib/models/base/embedding.ts
@@ -1,3 +1,5 @@
+import { Chunk } from '@/lib/types';
+
abstract class BaseEmbedding {
constructor(protected config: CONFIG) {}
abstract embedText(texts: string[]): Promise;
diff --git a/src/lib/models/providers/ollama/ollamaEmbedding.ts b/src/lib/models/providers/ollama/ollamaEmbedding.ts
index 0fd306a..7bb00b8 100644
--- a/src/lib/models/providers/ollama/ollamaEmbedding.ts
+++ b/src/lib/models/providers/ollama/ollamaEmbedding.ts
@@ -1,5 +1,6 @@
import { Ollama } from 'ollama';
import BaseEmbedding from '../../base/embedding';
+import { Chunk } from '@/lib/types';
type OllamaConfig = {
model: string;
diff --git a/src/lib/models/providers/openai/openaiEmbedding.ts b/src/lib/models/providers/openai/openaiEmbedding.ts
index ea15680..4e137ad 100644
--- a/src/lib/models/providers/openai/openaiEmbedding.ts
+++ b/src/lib/models/providers/openai/openaiEmbedding.ts
@@ -1,5 +1,6 @@
import OpenAI from 'openai';
import BaseEmbedding from '../../base/embedding';
+import { Chunk } from '@/lib/types';
type OpenAIConfig = {
apiKey: string;
diff --git a/src/lib/models/registry.ts b/src/lib/models/registry.ts
index 5067b6d..687c84c 100644
--- a/src/lib/models/registry.ts
+++ b/src/lib/models/registry.ts
@@ -1,7 +1,5 @@
import { ConfigModelProvider } from '../config/types';
-import BaseModelProvider, {
- createProviderInstance,
-} from './providers/baseProvider';
+import BaseModelProvider, { createProviderInstance } from './base/provider';
import { getConfiguredModelProviders } from '../config/serverRegistry';
import { providers } from './providers';
import { MinimalProvider, ModelList } from './types';
diff --git a/src/lib/models/types.ts b/src/lib/models/types.ts
index ce77d3b..45560d1 100644
--- a/src/lib/models/types.ts
+++ b/src/lib/models/types.ts
@@ -1,4 +1,5 @@
import z from 'zod';
+import { ChatTurnMessage } from '../types';
type Model = {
name: string;
@@ -37,7 +38,7 @@ type GenerateOptions = {
};
type GenerateTextInput = {
- messages: Message[];
+ messages: ChatTurnMessage[];
options?: GenerateOptions;
};
@@ -54,7 +55,7 @@ type StreamTextOutput = {
type GenerateObjectInput = {
schema: z.ZodTypeAny;
- messages: Message[];
+ messages: ChatTurnMessage[];
options?: GenerateOptions;
};
From 74bc08d18960842d123db19189d32b738e8dae7a Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:22:27 +0530
Subject: [PATCH 037/199] Refactor types and imports for consistency
---
src/lib/session.ts | 1 +
src/lib/types.ts | 27 +++++++++++++++------------
src/lib/utils/formatHistory.ts | 4 +++-
3 files changed, 19 insertions(+), 13 deletions(-)
diff --git a/src/lib/session.ts b/src/lib/session.ts
index 834cbfd..e5a9bc1 100644
--- a/src/lib/session.ts
+++ b/src/lib/session.ts
@@ -1,5 +1,6 @@
import { EventEmitter } from 'stream';
import { applyPatch } from 'rfc6902';
+import { Block } from './types';
class SessionManager {
private static sessions = new Map();
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 824aea0..9793077 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -1,32 +1,32 @@
-type ChatTurnMessage = {
+export type ChatTurnMessage = {
role: 'user' | 'assistant' | 'system';
content: string;
};
-type Chunk = {
+export type Chunk = {
content: string;
metadata: Record;
};
-type TextBlock = {
+export type TextBlock = {
id: string;
type: 'text';
data: string;
};
-type SourceBlock = {
+export type SourceBlock = {
id: string;
type: 'source';
data: Chunk[];
};
-type SuggestionBlock = {
+export type SuggestionBlock = {
id: string;
type: 'suggestion';
data: string[];
};
-type WidgetBlock = {
+export type WidgetBlock = {
id: string;
type: 'widget';
data: {
@@ -35,27 +35,30 @@ type WidgetBlock = {
};
};
-type ReasoningResearchBlock = {
+export type ReasoningResearchBlock = {
id: string;
+ type: 'reasoning';
reasoning: string;
};
-type SearchingResearchBlock = {
+export type SearchingResearchBlock = {
id: string;
+ type: 'searching';
searching: string[];
};
-type ReadingResearchBlock = {
+export type ReadingResearchBlock = {
id: string;
+ type: 'reading';
reading: Chunk[];
};
-type ResearchBlockSubStep =
+export type ResearchBlockSubStep =
| ReasoningResearchBlock
| SearchingResearchBlock
| ReadingResearchBlock;
-type ResearchBlock = {
+export type ResearchBlock = {
id: string;
type: 'research';
data: {
@@ -63,7 +66,7 @@ type ResearchBlock = {
};
};
-type Block =
+export type Block =
| TextBlock
| SourceBlock
| SuggestionBlock
diff --git a/src/lib/utils/formatHistory.ts b/src/lib/utils/formatHistory.ts
index 8256ffa..7c5e4a0 100644
--- a/src/lib/utils/formatHistory.ts
+++ b/src/lib/utils/formatHistory.ts
@@ -1,4 +1,6 @@
-const formatChatHistoryAsString = (history: Message[]) => {
+import { ChatTurnMessage } from '../types';
+
+const formatChatHistoryAsString = (history: ChatTurnMessage[]) => {
return history
.map(
(message) =>
From 0ac8569a9ef763e4db07ec350fe000a4dd220c2e Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:23:18 +0530
Subject: [PATCH 038/199] feat(agents): update suggestion generator
---
src/app/api/suggestions/route.ts | 12 +-------
src/lib/agents/suggestions/index.ts | 41 ++++++++++++++++------------
src/lib/prompts/suggestions/index.ts | 18 ++++++------
3 files changed, 35 insertions(+), 36 deletions(-)
diff --git a/src/app/api/suggestions/route.ts b/src/app/api/suggestions/route.ts
index 2dc7248..0c70cba 100644
--- a/src/app/api/suggestions/route.ts
+++ b/src/app/api/suggestions/route.ts
@@ -19,19 +19,9 @@ export const POST = async (req: Request) => {
body.chatModel.key,
);
- const chatHistory = body.chatHistory
- .map((msg: any) => {
- if (msg.role === 'user') {
- return new HumanMessage(msg.content);
- } else if (msg.role === 'assistant') {
- return new AIMessage(msg.content);
- }
- })
- .filter((msg) => msg !== undefined) as BaseMessage[];
-
const suggestions = await generateSuggestions(
{
- chatHistory,
+ chatHistory: body.chatHistory,
},
llm,
);
diff --git a/src/lib/agents/suggestions/index.ts b/src/lib/agents/suggestions/index.ts
index 03302ac..050eac7 100644
--- a/src/lib/agents/suggestions/index.ts
+++ b/src/lib/agents/suggestions/index.ts
@@ -1,32 +1,39 @@
-import ListLineOutputParser from '@/lib/outputParsers/listLineOutputParser';
-import { ChatPromptTemplate, PromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
-import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { suggestionGeneratorPrompt } from '@/lib/prompts/suggestions';
+import { ChatTurnMessage } from '@/lib/types';
+import z from 'zod';
+import BaseLLM from '@/lib/models/base/llm';
+import { i } from 'mathjs';
type SuggestionGeneratorInput = {
- chatHistory: BaseMessage[];
+ chatHistory: ChatTurnMessage[];
};
-const outputParser = new ListLineOutputParser({
- key: 'suggestions',
+const schema = z.object({
+ suggestions: z
+ .array(z.string())
+ .describe('List of suggested questions or prompts'),
});
const generateSuggestions = async (
input: SuggestionGeneratorInput,
- llm: BaseChatModel,
+ llm: BaseLLM,
) => {
- const chatPrompt = await ChatPromptTemplate.fromMessages([
- new SystemMessage(suggestionGeneratorPrompt),
- new HumanMessage(`${formatChatHistoryAsString(input.chatHistory)} `)
- ]).formatMessages({})
+ const res = await llm.generateObject>({
+ messages: [
+ {
+ role: 'system',
+ content: suggestionGeneratorPrompt,
+ },
+ {
+ role: 'user',
+ content: `\n${formatChatHistoryAsString(input.chatHistory)}\n `,
+ },
+ ],
+ schema,
+ });
- const res = await llm.invoke(chatPrompt)
-
- const suggestions = await outputParser.invoke(res)
-
- return suggestions
+ return res.suggestions;
};
export default generateSuggestions;
diff --git a/src/lib/prompts/suggestions/index.ts b/src/lib/prompts/suggestions/index.ts
index daa99d4..18922ba 100644
--- a/src/lib/prompts/suggestions/index.ts
+++ b/src/lib/prompts/suggestions/index.ts
@@ -3,13 +3,15 @@ You are an AI suggestion generator for an AI powered search engine. You will be
You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information.
Make sure the suggestions are medium in length and are informative and relevant to the conversation.
-Provide these suggestions separated by newlines between the XML tags and . For example:
-
-
-Tell me more about SpaceX and their recent projects
-What is the latest news on SpaceX?
-Who is the CEO of SpaceX?
-
+Sample suggestions for a conversation about Elon Musk:
+{
+ "suggestions": [
+ "What are Elon Musk's plans for SpaceX in the next decade?",
+ "How has Tesla's stock performance been influenced by Elon Musk's leadership?",
+ "What are the key innovations introduced by Elon Musk in the electric vehicle industry?",
+ "How does Elon Musk's vision for renewable energy impact global sustainability efforts?"
+ ]
+}
Today's date is ${new Date().toISOString()}
-`;
\ No newline at end of file
+`;
From 6da6acbcd0261215699933747a6f854f8670974e Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:23:42 +0530
Subject: [PATCH 039/199] feat(agents): update media agents
---
src/lib/agents/media/image.ts | 59 +++++++++++++++++----------------
src/lib/agents/media/video.ts | 57 +++++++++++++++----------------
src/lib/prompts/media/image.ts | 41 ++++++++++++-----------
src/lib/prompts/media/videos.ts | 41 ++++++++++++-----------
4 files changed, 103 insertions(+), 95 deletions(-)
diff --git a/src/lib/agents/media/image.ts b/src/lib/agents/media/image.ts
index 648b5ce..f146824 100644
--- a/src/lib/agents/media/image.ts
+++ b/src/lib/agents/media/image.ts
@@ -1,21 +1,17 @@
/* I don't think can be classified as agents but to keep the structure consistent i guess ill keep it here */
-import {
- RunnableSequence,
- RunnableMap,
- RunnableLambda,
-} from '@langchain/core/runnables';
-import { ChatPromptTemplate } from '@langchain/core/prompts';
-import formatChatHistoryAsString from '@/lib/utils/formatHistory';
-import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
-import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '@/lib/searxng';
-import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import LineOutputParser from '@/lib/outputParsers/lineOutputParser';
-import { imageSearchFewShots, imageSearchPrompt } from '@/lib/prompts/media/image';
+import {
+ imageSearchFewShots,
+ imageSearchPrompt,
+} from '@/lib/prompts/media/image';
+import BaseLLM from '@/lib/models/base/llm';
+import z from 'zod';
+import { ChatTurnMessage } from '@/lib/types';
+import formatChatHistoryAsString from '@/lib/utils/formatHistory';
type ImageSearchChainInput = {
- chatHistory: BaseMessage[];
+ chatHistory: ChatTurnMessage[];
query: string;
};
@@ -23,27 +19,32 @@ type ImageSearchResult = {
img_src: string;
url: string;
title: string;
-}
-
-const outputParser = new LineOutputParser({
- key: 'query',
-})
+};
const searchImages = async (
input: ImageSearchChainInput,
- llm: BaseChatModel,
+ llm: BaseLLM,
) => {
- const chatPrompt = await ChatPromptTemplate.fromMessages([
- new SystemMessage(imageSearchPrompt),
- ...imageSearchFewShots,
- new HumanMessage(`\n${formatChatHistoryAsString(input.chatHistory)}\n \n\n${input.query}\n `)
- ]).formatMessages({})
+ const schema = z.object({
+ query: z.string().describe('The image search query.'),
+ });
- const res = await llm.invoke(chatPrompt)
+ const res = await llm.generateObject>({
+ messages: [
+ {
+ role: 'system',
+ content: imageSearchPrompt,
+ },
+ ...imageSearchFewShots,
+ {
+ role: 'user',
+ content: `\n${formatChatHistoryAsString(input.chatHistory)}\n \n\n${input.query}\n `,
+ },
+ ],
+ schema: schema,
+ });
- const query = await outputParser.invoke(res)
-
- const searchRes = await searchSearxng(query!, {
+ const searchRes = await searchSearxng(res.query, {
engines: ['bing images', 'google images'],
});
@@ -62,4 +63,4 @@ const searchImages = async (
return images.slice(0, 10);
};
-export default searchImages;
\ No newline at end of file
+export default searchImages;
diff --git a/src/lib/agents/media/video.ts b/src/lib/agents/media/video.ts
index 60fc04f..feac720 100644
--- a/src/lib/agents/media/video.ts
+++ b/src/lib/agents/media/video.ts
@@ -1,13 +1,15 @@
-import { ChatPromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
-import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
import { searchSearxng } from '@/lib/searxng';
-import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import LineOutputParser from '@/lib/outputParsers/lineOutputParser';
-import { videoSearchFewShots, videoSearchPrompt } from '@/lib/prompts/media/videos';
+import {
+ videoSearchFewShots,
+ videoSearchPrompt,
+} from '@/lib/prompts/media/videos';
+import { ChatTurnMessage } from '@/lib/types';
+import BaseLLM from '@/lib/models/base/llm';
+import z from 'zod';
type VideoSearchChainInput = {
- chatHistory: BaseMessage[];
+ chatHistory: ChatTurnMessage[];
query: string;
};
@@ -16,39 +18,39 @@ type VideoSearchResult = {
url: string;
title: string;
iframe_src: string;
-}
-
-const outputParser = new LineOutputParser({
- key: 'query',
-});
+};
const searchVideos = async (
input: VideoSearchChainInput,
- llm: BaseChatModel,
+ llm: BaseLLM,
) => {
- const chatPrompt = await ChatPromptTemplate.fromMessages([
- new SystemMessage(videoSearchPrompt),
- ...videoSearchFewShots,
- new HumanMessage(`${formatChatHistoryAsString(input.chatHistory)}\n \n\n${input.query}\n `)
- ]).formatMessages({})
+ const schema = z.object({
+ query: z.string().describe('The video search query.'),
+ });
- const res = await llm.invoke(chatPrompt)
+ const res = await llm.generateObject>({
+ messages: [
+ {
+ role: 'system',
+ content: videoSearchPrompt,
+ },
+ ...videoSearchFewShots,
+ {
+ role: 'user',
+ content: `\n${formatChatHistoryAsString(input.chatHistory)}\n \n\n${input.query}\n `,
+ },
+ ],
+ schema: schema,
+ });
- const query = await outputParser.invoke(res)
-
- const searchRes = await searchSearxng(query!, {
+ const searchRes = await searchSearxng(res.query, {
engines: ['youtube'],
});
const videos: VideoSearchResult[] = [];
searchRes.results.forEach((result) => {
- if (
- result.thumbnail &&
- result.url &&
- result.title &&
- result.iframe_src
- ) {
+ if (result.thumbnail && result.url && result.title && result.iframe_src) {
videos.push({
img_src: result.thumbnail,
url: result.url,
@@ -59,7 +61,6 @@ const searchVideos = async (
});
return videos.slice(0, 10);
-
};
export default searchVideos;
diff --git a/src/lib/prompts/media/image.ts b/src/lib/prompts/media/image.ts
index 5f707c1..d4584cb 100644
--- a/src/lib/prompts/media/image.ts
+++ b/src/lib/prompts/media/image.ts
@@ -1,26 +1,29 @@
-import { BaseMessageLike } from "@langchain/core/messages";
+import { ChatTurnMessage } from '@/lib/types';
export const imageSearchPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images.
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
-Output only the rephrased query wrapped in an XML element. Do not include any explanation or additional text.
+Output only the rephrased query in query key JSON format. Do not include any explanation or additional text.
`;
-export const imageSearchFewShots: BaseMessageLike[] = [
- [
- 'user',
- '\n \n\nWhat is a cat?\n ',
- ],
- ['assistant', 'A cat '],
+export const imageSearchFewShots: ChatTurnMessage[] = [
+ {
+ role: 'user',
+ content:
+ '\n \n\nWhat is a cat?\n ',
+ },
+ { role: 'assistant', content: '{"query":"A cat"}' },
- [
- 'user',
- '\n \n\nWhat is a car? How does it work?\n ',
- ],
- ['assistant', 'Car working '],
- [
- 'user',
- '\n \n\nHow does an AC work?\n ',
- ],
- ['assistant', 'AC working ']
-]
\ No newline at end of file
+ {
+ role: 'user',
+ content:
+ '\n \n\nWhat is a car? How does it work?\n ',
+ },
+ { role: 'assistant', content: '{"query":"Car working"}' },
+ {
+ role: 'user',
+ content:
+ '\n \n\nHow does an AC work?\n ',
+ },
+ { role: 'assistant', content: '{"query":"AC working"}' },
+];
diff --git a/src/lib/prompts/media/videos.ts b/src/lib/prompts/media/videos.ts
index b4a0d55..adaa7b5 100644
--- a/src/lib/prompts/media/videos.ts
+++ b/src/lib/prompts/media/videos.ts
@@ -1,25 +1,28 @@
-import { BaseMessageLike } from "@langchain/core/messages";
+import { ChatTurnMessage } from '@/lib/types';
export const videoSearchPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos.
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
-Output only the rephrased query wrapped in an XML element. Do not include any explanation or additional text.
+Output only the rephrased query in query key JSON format. Do not include any explanation or additional text.
`;
-export const videoSearchFewShots: BaseMessageLike[] = [
- [
- 'user',
- '\n \n\nHow does a car work?\n ',
- ],
- ['assistant', 'How does a car work? '],
- [
- 'user',
- '\n \n\nWhat is the theory of relativity?\n ',
- ],
- ['assistant', 'Theory of relativity '],
- [
- 'user',
- '\n \n\nHow does an AC work?\n ',
- ],
- ['assistant', 'AC working '],
-]
\ No newline at end of file
+export const videoSearchFewShots: ChatTurnMessage[] = [
+ {
+ role: 'user',
+ content:
+ '\n \n\nHow does a car work?\n ',
+ },
+ { role: 'assistant', content: '{"query":"How does a car work?"}' },
+ {
+ role: 'user',
+ content:
+ '\n \n\nWhat is the theory of relativity?\n ',
+ },
+ { role: 'assistant', content: '{"query":"Theory of relativity"}' },
+ {
+ role: 'user',
+ content:
+ '\n \n\nHow does an AC work?\n ',
+ },
+ { role: 'assistant', content: '{"query":"AC working"}' },
+];
From e22a39fd73c7534a6a459e2540c723a79aab59e1 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:24:17 +0530
Subject: [PATCH 040/199] feat(routes): update routes to handle new llm types
---
src/app/api/images/route.ts | 13 +--------
src/app/api/search/route.ts | 53 ++++++++++++++++++------------------
src/app/api/uploads/route.ts | 13 +++++++--
src/app/api/videos/route.ts | 13 +--------
4 files changed, 39 insertions(+), 53 deletions(-)
diff --git a/src/app/api/images/route.ts b/src/app/api/images/route.ts
index bc62a1d..9cfabb2 100644
--- a/src/app/api/images/route.ts
+++ b/src/app/api/images/route.ts
@@ -1,7 +1,6 @@
import searchImages from '@/lib/agents/media/image';
import ModelRegistry from '@/lib/models/registry';
import { ModelWithProvider } from '@/lib/models/types';
-import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
interface ImageSearchBody {
query: string;
@@ -20,19 +19,9 @@ export const POST = async (req: Request) => {
body.chatModel.key,
);
- const chatHistory = body.chatHistory
- .map((msg: any) => {
- if (msg.role === 'user') {
- return new HumanMessage(msg.content);
- } else if (msg.role === 'assistant') {
- return new AIMessage(msg.content);
- }
- })
- .filter((msg) => msg !== undefined) as BaseMessage[];
-
const images = await searchImages(
{
- chatHistory: chatHistory,
+ chatHistory: body.chatHistory,
query: body.query,
},
llm,
diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts
index f737a55..8f357cb 100644
--- a/src/app/api/search/route.ts
+++ b/src/app/api/search/route.ts
@@ -1,8 +1,8 @@
-import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
-import { MetaSearchAgentType } from '@/lib/search/metaSearchAgent';
-import { searchHandlers } from '@/lib/search';
import ModelRegistry from '@/lib/models/registry';
import { ModelWithProvider } from '@/lib/models/types';
+import SessionManager from '@/lib/session';
+import SearchAgent from '@/lib/agents/search';
+import { ChatTurnMessage } from '@/lib/types';
interface ChatRequestBody {
optimizationMode: 'speed' | 'balanced';
@@ -40,27 +40,26 @@ export const POST = async (req: Request) => {
),
]);
- const history: BaseMessage[] = body.history.map((msg) => {
+ const history: ChatTurnMessage[] = body.history.map((msg) => {
return msg[0] === 'human'
- ? new HumanMessage({ content: msg[1] })
- : new AIMessage({ content: msg[1] });
+ ? { role: 'user', content: msg[1] }
+ : { role: 'assistant', content: msg[1] };
});
- const searchHandler: MetaSearchAgentType = searchHandlers[body.focusMode];
+ const session = SessionManager.createSession();
- if (!searchHandler) {
- return Response.json({ message: 'Invalid focus mode' }, { status: 400 });
- }
+ const agent = new SearchAgent();
- const emitter = await searchHandler.searchAndAnswer(
- body.query,
- history,
- llm,
- embeddings,
- body.optimizationMode,
- [],
- body.systemInstructions || '',
- );
+ agent.searchAsync(session, {
+ chatHistory: history,
+ config: {
+ embedding: embeddings,
+ llm: llm,
+ sources: ['web', 'discussions', 'academic'],
+ mode: 'balanced',
+ },
+ followUp: body.query,
+ });
if (!body.stream) {
return new Promise(
@@ -71,7 +70,7 @@ export const POST = async (req: Request) => {
let message = '';
let sources: any[] = [];
- emitter.on('data', (data: string) => {
+ session.addListener('data', (data: string) => {
try {
const parsedData = JSON.parse(data);
if (parsedData.type === 'response') {
@@ -89,11 +88,11 @@ export const POST = async (req: Request) => {
}
});
- emitter.on('end', () => {
+ session.addListener('end', () => {
resolve(Response.json({ message, sources }, { status: 200 }));
});
- emitter.on('error', (error: any) => {
+ session.addListener('error', (error: any) => {
reject(
Response.json(
{ message: 'Search error', error },
@@ -124,14 +123,14 @@ export const POST = async (req: Request) => {
);
signal.addEventListener('abort', () => {
- emitter.removeAllListeners();
+ session.removeAllListeners();
try {
controller.close();
- } catch (error) { }
+ } catch (error) {}
});
- emitter.on('data', (data: string) => {
+ session.addListener('data', (data: string) => {
if (signal.aborted) return;
try {
@@ -162,7 +161,7 @@ export const POST = async (req: Request) => {
}
});
- emitter.on('end', () => {
+ session.addListener('end', () => {
if (signal.aborted) return;
controller.enqueue(
@@ -175,7 +174,7 @@ export const POST = async (req: Request) => {
controller.close();
});
- emitter.on('error', (error: any) => {
+ session.addListener('error', (error: any) => {
if (signal.aborted) return;
controller.error(error);
diff --git a/src/app/api/uploads/route.ts b/src/app/api/uploads/route.ts
index 2a275f4..dc9c202 100644
--- a/src/app/api/uploads/route.ts
+++ b/src/app/api/uploads/route.ts
@@ -7,6 +7,7 @@ import { DocxLoader } from '@langchain/community/document_loaders/fs/docx';
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
import { Document } from '@langchain/core/documents';
import ModelRegistry from '@/lib/models/registry';
+import { Chunk } from '@/lib/types';
interface FileRes {
fileName: string;
@@ -87,9 +88,17 @@ export async function POST(req: Request) {
}),
);
- const embeddings = await model.embedDocuments(
- splitted.map((doc) => doc.pageContent),
+ const chunks: Chunk[] = splitted.map((doc) => {
+ return {
+ content: doc.pageContent,
+ metadata: doc.metadata,
+ }
+ });
+
+ const embeddings = await model.embedChunks(
+ chunks
);
+
const embeddingsDataPath = filePath.replace(
/\.\w+$/,
'-embeddings.json',
diff --git a/src/app/api/videos/route.ts b/src/app/api/videos/route.ts
index 1417226..0d5e03c 100644
--- a/src/app/api/videos/route.ts
+++ b/src/app/api/videos/route.ts
@@ -1,7 +1,6 @@
import handleVideoSearch from '@/lib/agents/media/video';
import ModelRegistry from '@/lib/models/registry';
import { ModelWithProvider } from '@/lib/models/types';
-import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
interface VideoSearchBody {
query: string;
@@ -20,19 +19,9 @@ export const POST = async (req: Request) => {
body.chatModel.key,
);
- const chatHistory = body.chatHistory
- .map((msg: any) => {
- if (msg.role === 'user') {
- return new HumanMessage(msg.content);
- } else if (msg.role === 'assistant') {
- return new AIMessage(msg.content);
- }
- })
- .filter((msg) => msg !== undefined) as BaseMessage[];
-
const videos = await handleVideoSearch(
{
- chatHistory: chatHistory,
+ chatHistory: body.chatHistory,
query: body.query,
},
llm,
From b7b280637fafc9e86c38f7b9745be5a4a56102ae Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:26:47 +0530
Subject: [PATCH 041/199] feat(providers): update ollama context window, temp
---
src/lib/models/providers/ollama/ollamaLLM.ts | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/lib/models/providers/ollama/ollamaLLM.ts b/src/lib/models/providers/ollama/ollamaLLM.ts
index 0e64d7b..05869ca 100644
--- a/src/lib/models/providers/ollama/ollamaLLM.ts
+++ b/src/lib/models/providers/ollama/ollamaLLM.ts
@@ -45,6 +45,7 @@ class OllamaLLM extends BaseLLM {
top_p: this.config.options?.topP,
temperature: this.config.options?.temperature,
num_predict: this.config.options?.maxTokens,
+ num_ctx: 32000,
frequency_penalty: this.config.options?.frequencyPenalty,
presence_penalty: this.config.options?.presencePenalty,
stop: this.config.options?.stopSequences,
@@ -71,6 +72,7 @@ class OllamaLLM extends BaseLLM {
options: {
top_p: this.config.options?.topP,
temperature: this.config.options?.temperature,
+ num_ctx: 32000,
num_predict: this.config.options?.maxTokens,
frequency_penalty: this.config.options?.frequencyPenalty,
presence_penalty: this.config.options?.presencePenalty,
@@ -99,7 +101,7 @@ class OllamaLLM extends BaseLLM {
think: false,
options: {
top_p: this.config.options?.topP,
- temperature: 0,
+ temperature: 0.7,
num_predict: this.config.options?.maxTokens,
frequency_penalty: this.config.options?.frequencyPenalty,
presence_penalty: this.config.options?.presencePenalty,
@@ -127,7 +129,7 @@ class OllamaLLM extends BaseLLM {
think: false,
options: {
top_p: this.config.options?.topP,
- temperature: this.config.options?.temperature,
+ temperature: 0.7,
num_predict: this.config.options?.maxTokens,
frequency_penalty: this.config.options?.frequencyPenalty,
presence_penalty: this.config.options?.presencePenalty,
From d5f62f2dca6dbdf722dbe2465ae96faaa839a97e Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:46:02 +0530
Subject: [PATCH 042/199] feat(chat): prevent auto-scroll unless message sent
---
src/components/Chat.tsx | 38 ++++++++++++++++++++++----------------
1 file changed, 22 insertions(+), 16 deletions(-)
diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx
index 22e0a48..56c13e6 100644
--- a/src/components/Chat.tsx
+++ b/src/components/Chat.tsx
@@ -7,11 +7,12 @@ import MessageBoxLoading from './MessageBoxLoading';
import { useChat } from '@/lib/hooks/useChat';
const Chat = () => {
- const { sections, chatTurns, loading, messageAppeared } = useChat();
+ const { sections, loading, messageAppeared, messages } = useChat();
const [dividerWidth, setDividerWidth] = useState(0);
const dividerRef = useRef(null);
const messageEnd = useRef(null);
+ const lastScrolledRef = useRef(0);
useEffect(() => {
const updateDividerWidth = () => {
@@ -22,35 +23,40 @@ const Chat = () => {
updateDividerWidth();
+ const resizeObserver = new ResizeObserver(() => {
+ updateDividerWidth();
+ });
+
+ const currentRef = dividerRef.current;
+ if (currentRef) {
+ resizeObserver.observe(currentRef);
+ }
+
window.addEventListener('resize', updateDividerWidth);
return () => {
+ if (currentRef) {
+ resizeObserver.unobserve(currentRef);
+ }
+ resizeObserver.disconnect();
window.removeEventListener('resize', updateDividerWidth);
};
- }, []);
+ }, [sections.length]);
useEffect(() => {
const scroll = () => {
messageEnd.current?.scrollIntoView({ behavior: 'auto' });
};
- if (chatTurns.length === 1) {
- document.title = `${chatTurns[0].content.substring(0, 30)} - Perplexica`;
+ if (messages.length === 1) {
+ document.title = `${messages[0].query.substring(0, 30)} - Perplexica`;
}
- const messageEndBottom =
- messageEnd.current?.getBoundingClientRect().bottom ?? 0;
-
- const distanceFromMessageEnd = window.innerHeight - messageEndBottom;
-
- if (distanceFromMessageEnd >= -100) {
+ if (sections.length > lastScrolledRef.current) {
scroll();
+ lastScrolledRef.current = sections.length;
}
-
- if (chatTurns[chatTurns.length - 1]?.role === 'user') {
- scroll();
- }
- }, [chatTurns]);
+ }, [messages]);
return (
@@ -58,7 +64,7 @@ const Chat = () => {
const isLast = i === sections.length - 1;
return (
-
+
Date: Sun, 23 Nov 2025 19:46:42 +0530
Subject: [PATCH 043/199] feat(app): add initial widgets
---
src/components/WeatherWidget.tsx | 2 +-
src/components/Widgets/Calculation.tsx | 54 ++
src/components/Widgets/Renderer.tsx | 76 +++
src/components/Widgets/Stock.tsx | 517 ++++++++++++++++++
src/components/Widgets/Weather.tsx | 408 ++++++++++++++
.../classifier/intents/widgetResponse.ts | 40 +-
.../search/widgets/calculationWidget.ts | 65 +++
src/lib/agents/search/widgets/index.ts | 4 +
src/lib/agents/search/widgets/stockWidget.ts | 412 ++++++++++++++
.../agents/search/widgets/weatherWidget.ts | 205 ++++---
10 files changed, 1703 insertions(+), 80 deletions(-)
create mode 100644 src/components/Widgets/Calculation.tsx
create mode 100644 src/components/Widgets/Renderer.tsx
create mode 100644 src/components/Widgets/Stock.tsx
create mode 100644 src/components/Widgets/Weather.tsx
create mode 100644 src/lib/agents/search/widgets/calculationWidget.ts
create mode 100644 src/lib/agents/search/widgets/stockWidget.ts
diff --git a/src/components/WeatherWidget.tsx b/src/components/WeatherWidget.tsx
index a7ebcff..3ba0038 100644
--- a/src/components/WeatherWidget.tsx
+++ b/src/components/WeatherWidget.tsx
@@ -91,7 +91,7 @@ const WeatherWidget = () => {
setData({
temperature: data.temperature,
condition: data.condition,
- location: location.city,
+ location: 'Mars',
humidity: data.humidity,
windSpeed: data.windSpeed,
icon: data.icon,
diff --git a/src/components/Widgets/Calculation.tsx b/src/components/Widgets/Calculation.tsx
new file mode 100644
index 0000000..e671aab
--- /dev/null
+++ b/src/components/Widgets/Calculation.tsx
@@ -0,0 +1,54 @@
+'use client';
+
+import { Calculator, Equal } from 'lucide-react';
+
+type CalculationWidgetProps = {
+ expression: string;
+ result: number;
+};
+
+const Calculation = ({ expression, result }: CalculationWidgetProps) => {
+ return (
+
+
+
+
+
+
+ Calculation
+
+
+
+
+
+
+
+ Expression
+
+
+
+
+ {expression}
+
+
+
+
+
+
+
+
+ Result
+
+
+
+
+ {result.toLocaleString()}
+
+
+
+
+
+ );
+};
+
+export default Calculation;
diff --git a/src/components/Widgets/Renderer.tsx b/src/components/Widgets/Renderer.tsx
new file mode 100644
index 0000000..8456c8f
--- /dev/null
+++ b/src/components/Widgets/Renderer.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import { Widget } from '../ChatWindow';
+import Weather from './Weather';
+import Calculation from './Calculation';
+import Stock from './Stock';
+
+const Renderer = ({ widgets }: { widgets: Widget[] }) => {
+ return widgets.map((widget, index) => {
+ switch (widget.widgetType) {
+ case 'weather':
+ return (
+
+ );
+ case 'calculation_result':
+ return (
+
+ );
+ case 'stock':
+ return (
+
+ );
+ default:
+ return Unknown widget type: {widget.widgetType}
;
+ }
+ });
+};
+
+export default Renderer;
diff --git a/src/components/Widgets/Stock.tsx b/src/components/Widgets/Stock.tsx
new file mode 100644
index 0000000..57fba1a
--- /dev/null
+++ b/src/components/Widgets/Stock.tsx
@@ -0,0 +1,517 @@
+'use client';
+
+import { Clock, ArrowUpRight, ArrowDownRight, Minus } from 'lucide-react';
+import { useEffect, useRef, useState } from 'react';
+import {
+ createChart,
+ ColorType,
+ LineStyle,
+ BaselineSeries,
+ LineSeries,
+} from 'lightweight-charts';
+
+type StockWidgetProps = {
+ symbol: string;
+ shortName: string;
+ longName?: string;
+ exchange?: string;
+ currency?: string;
+ marketState?: string;
+ regularMarketPrice?: number;
+ regularMarketChange?: number;
+ regularMarketChangePercent?: number;
+ regularMarketPreviousClose?: number;
+ regularMarketOpen?: number;
+ regularMarketDayHigh?: number;
+ regularMarketDayLow?: number;
+ regularMarketVolume?: number;
+ averageDailyVolume3Month?: number;
+ marketCap?: number;
+ fiftyTwoWeekLow?: number;
+ fiftyTwoWeekHigh?: number;
+ trailingPE?: number;
+ forwardPE?: number;
+ dividendYield?: number;
+ earningsPerShare?: number;
+ website?: string;
+ postMarketPrice?: number;
+ postMarketChange?: number;
+ postMarketChangePercent?: number;
+ preMarketPrice?: number;
+ preMarketChange?: number;
+ preMarketChangePercent?: number;
+ chartData?: {
+ '1D'?: { timestamps: number[]; prices: number[] } | null;
+ '5D'?: { timestamps: number[]; prices: number[] } | null;
+ '1M'?: { timestamps: number[]; prices: number[] } | null;
+ '3M'?: { timestamps: number[]; prices: number[] } | null;
+ '6M'?: { timestamps: number[]; prices: number[] } | null;
+ '1Y'?: { timestamps: number[]; prices: number[] } | null;
+ MAX?: { timestamps: number[]; prices: number[] } | null;
+ } | null;
+ comparisonData?: Array<{
+ ticker: string;
+ name: string;
+ chartData: {
+ '1D'?: { timestamps: number[]; prices: number[] } | null;
+ '5D'?: { timestamps: number[]; prices: number[] } | null;
+ '1M'?: { timestamps: number[]; prices: number[] } | null;
+ '3M'?: { timestamps: number[]; prices: number[] } | null;
+ '6M'?: { timestamps: number[]; prices: number[] } | null;
+ '1Y'?: { timestamps: number[]; prices: number[] } | null;
+ MAX?: { timestamps: number[]; prices: number[] } | null;
+ };
+ }> | null;
+ error?: string;
+};
+
+const formatNumber = (num: number | undefined, decimals = 2): string => {
+ if (num === undefined || num === null) return 'N/A';
+ return num.toLocaleString(undefined, {
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals,
+ });
+};
+
+const formatLargeNumber = (num: number | undefined): string => {
+ if (num === undefined || num === null) return 'N/A';
+ if (num >= 1e12) return `$${(num / 1e12).toFixed(2)}T`;
+ if (num >= 1e9) return `$${(num / 1e9).toFixed(2)}B`;
+ if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`;
+ if (num >= 1e3) return `$${(num / 1e3).toFixed(2)}K`;
+ return `$${num.toFixed(2)}`;
+};
+
+const Stock = (props: StockWidgetProps) => {
+ const [isDarkMode, setIsDarkMode] = useState(false);
+ const [selectedTimeframe, setSelectedTimeframe] = useState<
+ '1D' | '5D' | '1M' | '3M' | '6M' | '1Y' | 'MAX'
+ >('1M');
+ const chartContainerRef = useRef(null);
+
+ useEffect(() => {
+ const checkDarkMode = () => {
+ setIsDarkMode(document.documentElement.classList.contains('dark'));
+ };
+
+ checkDarkMode();
+
+ const observer = new MutationObserver(checkDarkMode);
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['class'],
+ });
+
+ return () => observer.disconnect();
+ }, []);
+
+ useEffect(() => {
+ const currentChartData = props.chartData?.[selectedTimeframe];
+ if (
+ !chartContainerRef.current ||
+ !currentChartData ||
+ currentChartData.timestamps.length === 0
+ ) {
+ return;
+ }
+
+ const chart = createChart(chartContainerRef.current, {
+ width: chartContainerRef.current.clientWidth,
+ height: 280,
+ layout: {
+ background: { type: ColorType.Solid, color: 'transparent' },
+ textColor: isDarkMode ? '#6b7280' : '#9ca3af',
+ fontSize: 11,
+ attributionLogo: false,
+ },
+ grid: {
+ vertLines: {
+ color: isDarkMode ? '#21262d' : '#e8edf1',
+ style: LineStyle.Solid,
+ },
+ horzLines: {
+ color: isDarkMode ? '#21262d' : '#e8edf1',
+ style: LineStyle.Solid,
+ },
+ },
+ crosshair: {
+ vertLine: {
+ color: isDarkMode ? '#30363d' : '#d0d7de',
+ labelVisible: false,
+ },
+ horzLine: {
+ color: isDarkMode ? '#30363d' : '#d0d7de',
+ labelVisible: true,
+ },
+ },
+ rightPriceScale: {
+ borderVisible: false,
+ visible: false,
+ },
+ leftPriceScale: {
+ borderVisible: false,
+ visible: true,
+ },
+ timeScale: {
+ borderVisible: false,
+ timeVisible: false,
+ },
+ handleScroll: false,
+ handleScale: false,
+ });
+
+ const prices = currentChartData.prices;
+ let baselinePrice: number;
+
+ if (selectedTimeframe === '1D') {
+ baselinePrice = props.regularMarketPreviousClose ?? prices[0];
+ } else {
+ baselinePrice = prices[0];
+ }
+
+ const baselineSeries = chart.addSeries(BaselineSeries);
+
+ baselineSeries.applyOptions({
+ baseValue: { type: 'price', price: baselinePrice },
+ topLineColor: isDarkMode ? '#14b8a6' : '#0d9488',
+ topFillColor1: isDarkMode
+ ? 'rgba(20, 184, 166, 0.28)'
+ : 'rgba(13, 148, 136, 0.24)',
+ topFillColor2: isDarkMode
+ ? 'rgba(20, 184, 166, 0.05)'
+ : 'rgba(13, 148, 136, 0.05)',
+ bottomLineColor: isDarkMode ? '#f87171' : '#dc2626',
+ bottomFillColor1: isDarkMode
+ ? 'rgba(248, 113, 113, 0.05)'
+ : 'rgba(220, 38, 38, 0.05)',
+ bottomFillColor2: isDarkMode
+ ? 'rgba(248, 113, 113, 0.28)'
+ : 'rgba(220, 38, 38, 0.24)',
+ lineWidth: 2,
+ crosshairMarkerVisible: true,
+ crosshairMarkerRadius: 4,
+ crosshairMarkerBorderColor: '',
+ crosshairMarkerBackgroundColor: '',
+ });
+
+ const data = currentChartData.timestamps.map((timestamp, index) => {
+ const price = currentChartData.prices[index];
+ return {
+ time: (timestamp / 1000) as any,
+ value: price,
+ };
+ });
+
+ baselineSeries.setData(data);
+
+ const comparisonColors = ['#8b5cf6', '#f59e0b', '#ec4899'];
+ if (props.comparisonData && props.comparisonData.length > 0) {
+ props.comparisonData.forEach((comp, index) => {
+ const compChartData = comp.chartData[selectedTimeframe];
+ if (compChartData && compChartData.prices.length > 0) {
+ const compData = compChartData.timestamps.map((timestamp, i) => ({
+ time: (timestamp / 1000) as any,
+ value: compChartData.prices[i],
+ }));
+
+ const compSeries = chart.addSeries(LineSeries);
+ compSeries.applyOptions({
+ color: comparisonColors[index] || '#6b7280',
+ lineWidth: 2,
+ crosshairMarkerVisible: true,
+ crosshairMarkerRadius: 4,
+ priceScaleId: 'left',
+ });
+ compSeries.setData(compData);
+ }
+ });
+ }
+
+ chart.timeScale().fitContent();
+
+ const handleResize = () => {
+ if (chartContainerRef.current) {
+ chart.applyOptions({
+ width: chartContainerRef.current.clientWidth,
+ });
+ }
+ };
+
+ window.addEventListener('resize', handleResize);
+
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ chart.remove();
+ };
+ }, [
+ props.chartData,
+ props.comparisonData,
+ selectedTimeframe,
+ isDarkMode,
+ props.regularMarketPreviousClose,
+ ]);
+
+ const isPositive = (props.regularMarketChange ?? 0) >= 0;
+ const isMarketOpen = props.marketState === 'REGULAR';
+ const isPreMarket = props.marketState === 'PRE';
+ const isPostMarket = props.marketState === 'POST';
+
+ const displayPrice = isPostMarket
+ ? props.postMarketPrice ?? props.regularMarketPrice
+ : isPreMarket
+ ? props.preMarketPrice ?? props.regularMarketPrice
+ : props.regularMarketPrice;
+
+ const displayChange = isPostMarket
+ ? props.postMarketChange ?? props.regularMarketChange
+ : isPreMarket
+ ? props.preMarketChange ?? props.regularMarketChange
+ : props.regularMarketChange;
+
+ const displayChangePercent = isPostMarket
+ ? props.postMarketChangePercent ?? props.regularMarketChangePercent
+ : isPreMarket
+ ? props.preMarketChangePercent ?? props.regularMarketChangePercent
+ : props.regularMarketChangePercent;
+
+ const changeColor = isPositive
+ ? 'text-green-600 dark:text-green-400'
+ : 'text-red-600 dark:text-red-400';
+
+ if (props.error) {
+ return (
+
+
+ Error: {props.error}
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {props.website && (
+
{
+ (e.target as HTMLImageElement).style.display = 'none';
+ }}
+ />
+ )}
+
+ {props.symbol}
+
+ {props.exchange && (
+
+ {props.exchange}
+
+ )}
+ {isMarketOpen && (
+
+ )}
+ {isPreMarket && (
+
+
+
+ Pre-Market
+
+
+ )}
+ {isPostMarket && (
+
+
+
+ After Hours
+
+
+ )}
+
+
+ {props.longName || props.shortName}
+
+
+
+
+
+
+ {props.currency === 'USD' ? '$' : ''}
+ {formatNumber(displayPrice)}
+
+
+
+ {isPositive ? (
+
+ ) : displayChange === 0 ? (
+
+ ) : (
+
+ )}
+
+ {displayChange !== undefined && displayChange >= 0 ? '+' : ''}
+ {formatNumber(displayChange)}
+
+
+ (
+ {displayChangePercent !== undefined && displayChangePercent >= 0
+ ? '+'
+ : ''}
+ {formatNumber(displayChangePercent)}%)
+
+
+
+
+
+ {props.chartData && (
+
+
+
+ {(['1D', '5D', '1M', '3M', '6M', '1Y', 'MAX'] as const).map(
+ (timeframe) => (
+ setSelectedTimeframe(timeframe)}
+ disabled={!props.chartData?.[timeframe]}
+ className={`px-3 py-1.5 text-xs font-medium rounded transition-colors ${
+ selectedTimeframe === timeframe
+ ? 'bg-black/10 dark:bg-white/10 text-black dark:text-white'
+ : 'text-black/50 dark:text-white/50 hover:text-black/80 dark:hover:text-white/80'
+ } disabled:opacity-30 disabled:cursor-not-allowed`}
+ >
+ {timeframe}
+
+ ),
+ )}
+
+
+ {props.comparisonData && props.comparisonData.length > 0 && (
+
+
+ {props.symbol}
+
+ {props.comparisonData.map((comp, index) => {
+ const colors = ['#8b5cf6', '#f59e0b', '#ec4899'];
+ return (
+
+ );
+ })}
+
+ )}
+
+
+
+
+
+
+
+ Prev Close
+
+
+ ${formatNumber(props.regularMarketPreviousClose)}
+
+
+
+
+ 52W Range
+
+
+ ${formatNumber(props.fiftyTwoWeekLow, 2)}-$
+ {formatNumber(props.fiftyTwoWeekHigh, 2)}
+
+
+
+
+ Market Cap
+
+
+ {formatLargeNumber(props.marketCap)}
+
+
+
+
+ Open
+
+
+ ${formatNumber(props.regularMarketOpen)}
+
+
+
+
+ P/E Ratio
+
+
+ {props.trailingPE ? formatNumber(props.trailingPE, 2) : 'N/A'}
+
+
+
+
+ Dividend Yield
+
+
+ {props.dividendYield
+ ? `${formatNumber(props.dividendYield * 100, 2)}%`
+ : 'N/A'}
+
+
+
+
+ Day Range
+
+
+ ${formatNumber(props.regularMarketDayLow, 2)}-$
+ {formatNumber(props.regularMarketDayHigh, 2)}
+
+
+
+
+ Volume
+
+
+ {formatLargeNumber(props.regularMarketVolume)}
+
+
+
+
+ EPS
+
+
+ $
+ {props.earningsPerShare
+ ? formatNumber(props.earningsPerShare, 2)
+ : 'N/A'}
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default Stock;
diff --git a/src/components/Widgets/Weather.tsx b/src/components/Widgets/Weather.tsx
new file mode 100644
index 0000000..749d65b
--- /dev/null
+++ b/src/components/Widgets/Weather.tsx
@@ -0,0 +1,408 @@
+'use client';
+
+import { Wind, Droplets, Gauge } from 'lucide-react';
+import { useMemo, useEffect, useState } from 'react';
+
+type WeatherWidgetProps = {
+ location: string;
+ current: {
+ time: string;
+ temperature_2m: number;
+ relative_humidity_2m: number;
+ apparent_temperature: number;
+ is_day: number;
+ precipitation: number;
+ weather_code: number;
+ wind_speed_10m: number;
+ wind_direction_10m: number;
+ wind_gusts_10m?: number;
+ };
+ daily: {
+ time: string[];
+ weather_code: number[];
+ temperature_2m_max: number[];
+ temperature_2m_min: number[];
+ precipitation_probability_max: number[];
+ };
+ timezone: string;
+};
+
+const getWeatherInfo = (code: number, isDay: boolean, isDarkMode: boolean) => {
+ const dayNight = isDay ? 'day' : 'night';
+
+ const weatherMap: Record<
+ number,
+ { icon: string; description: string; gradient: string }
+ > = {
+ 0: {
+ icon: `clear-${dayNight}.svg`,
+ description: 'Clear',
+ gradient: isDarkMode
+ ? isDay
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E8F1FA, #7A9DBF 35%, #4A7BA8 60%, #2F5A88)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #5A6A7E, #3E4E63 40%, #2A3544 65%, #1A2230)'
+ : isDay
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #DBEAFE 30%, #93C5FD 60%, #60A5FA)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #7B8694, #475569 45%, #334155 70%, #1E293B)',
+ },
+ 1: {
+ icon: `clear-${dayNight}.svg`,
+ description: 'Mostly Clear',
+ gradient: isDarkMode
+ ? isDay
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E8F1FA, #7A9DBF 35%, #4A7BA8 60%, #2F5A88)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #5A6A7E, #3E4E63 40%, #2A3544 65%, #1A2230)'
+ : isDay
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #DBEAFE 30%, #93C5FD 60%, #60A5FA)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #7B8694, #475569 45%, #334155 70%, #1E293B)',
+ },
+ 2: {
+ icon: `cloudy-1-${dayNight}.svg`,
+ description: 'Partly Cloudy',
+ gradient: isDarkMode
+ ? isDay
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4E1ED, #8BA3B8 35%, #617A93 60%, #426070)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #6B7583, #4A5563 40%, #3A4450 65%, #2A3340)'
+ : isDay
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #E0F2FE 28%, #BFDBFE 58%, #93C5FD)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #8B99AB, #64748B 45%, #475569 70%, #334155)',
+ },
+ 3: {
+ icon: `cloudy-1-${dayNight}.svg`,
+ description: 'Cloudy',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8C3CF, #758190 38%, #546270 65%, #3D4A58)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #F5F8FA, #CBD5E1 32%, #94A3B8 65%, #64748B)',
+ },
+ 45: {
+ icon: `fog-${dayNight}.svg`,
+ description: 'Foggy',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #C5CDD8, #8892A0 38%, #697380 65%, #4F5A68)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #E2E8F0 30%, #CBD5E1 62%, #94A3B8)',
+ },
+ 48: {
+ icon: `fog-${dayNight}.svg`,
+ description: 'Rime Fog',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #C5CDD8, #8892A0 38%, #697380 65%, #4F5A68)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #E2E8F0 30%, #CBD5E1 62%, #94A3B8)',
+ },
+ 51: {
+ icon: `rainy-1-${dayNight}.svg`,
+ description: 'Light Drizzle',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8D4E5, #6FA4C5 35%, #4A85AC 60%, #356A8E)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5FBFF, #A5F3FC 28%, #67E8F9 60%, #22D3EE)',
+ },
+ 53: {
+ icon: `rainy-1-${dayNight}.svg`,
+ description: 'Drizzle',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8D4E5, #6FA4C5 35%, #4A85AC 60%, #356A8E)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5FBFF, #A5F3FC 28%, #67E8F9 60%, #22D3EE)',
+ },
+ 55: {
+ icon: `rainy-2-${dayNight}.svg`,
+ description: 'Heavy Drizzle',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #A5C5D8, #5E92B0 35%, #3F789D 60%, #2A5F82)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4F3FF, #7DD3FC 30%, #38BDF8 62%, #0EA5E9)',
+ },
+ 61: {
+ icon: `rainy-2-${dayNight}.svg`,
+ description: 'Light Rain',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #A5C5D8, #5E92B0 35%, #3F789D 60%, #2A5F82)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4F3FF, #7DD3FC 30%, #38BDF8 62%, #0EA5E9)',
+ },
+ 63: {
+ icon: `rainy-2-${dayNight}.svg`,
+ description: 'Rain',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #8DB3C8, #4D819F 38%, #326A87 65%, #215570)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8E8FF, #38BDF8 32%, #0EA5E9 65%, #0284C7)',
+ },
+ 65: {
+ icon: `rainy-3-${dayNight}.svg`,
+ description: 'Heavy Rain',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #7BA3B8, #3D6F8A 38%, #295973 65%, #1A455D)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #9CD9F5, #0EA5E9 32%, #0284C7 65%, #0369A1)',
+ },
+ 71: {
+ icon: `snowy-1-${dayNight}.svg`,
+ description: 'Light Snow',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5F0FA, #9BB5CE 32%, #7496B8 58%, #527A9E)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #F0F9FF 25%, #E0F2FE 55%, #BAE6FD)',
+ },
+ 73: {
+ icon: `snowy-2-${dayNight}.svg`,
+ description: 'Snow',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4E5F3, #85A1BD 35%, #6584A8 60%, #496A8E)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FAFEFF, #E0F2FE 28%, #BAE6FD 60%, #7DD3FC)',
+ },
+ 75: {
+ icon: `snowy-3-${dayNight}.svg`,
+ description: 'Heavy Snow',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #BDD8EB, #6F92AE 35%, #4F7593 60%, #365A78)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #F0FAFF, #BAE6FD 30%, #7DD3FC 62%, #38BDF8)',
+ },
+ 77: {
+ icon: `snowy-1-${dayNight}.svg`,
+ description: 'Snow Grains',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5F0FA, #9BB5CE 32%, #7496B8 58%, #527A9E)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #F0F9FF 25%, #E0F2FE 55%, #BAE6FD)',
+ },
+ 80: {
+ icon: `rainy-2-${dayNight}.svg`,
+ description: 'Light Showers',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #A5C5D8, #5E92B0 35%, #3F789D 60%, #2A5F82)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4F3FF, #7DD3FC 30%, #38BDF8 62%, #0EA5E9)',
+ },
+ 81: {
+ icon: `rainy-2-${dayNight}.svg`,
+ description: 'Showers',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #8DB3C8, #4D819F 38%, #326A87 65%, #215570)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8E8FF, #38BDF8 32%, #0EA5E9 65%, #0284C7)',
+ },
+ 82: {
+ icon: `rainy-3-${dayNight}.svg`,
+ description: 'Heavy Showers',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #7BA3B8, #3D6F8A 38%, #295973 65%, #1A455D)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #9CD9F5, #0EA5E9 32%, #0284C7 65%, #0369A1)',
+ },
+ 85: {
+ icon: `snowy-2-${dayNight}.svg`,
+ description: 'Light Snow Showers',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4E5F3, #85A1BD 35%, #6584A8 60%, #496A8E)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #FAFEFF, #E0F2FE 28%, #BAE6FD 60%, #7DD3FC)',
+ },
+ 86: {
+ icon: `snowy-3-${dayNight}.svg`,
+ description: 'Snow Showers',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #BDD8EB, #6F92AE 35%, #4F7593 60%, #365A78)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #F0FAFF, #BAE6FD 30%, #7DD3FC 62%, #38BDF8)',
+ },
+ 95: {
+ icon: `scattered-thunderstorms-${dayNight}.svg`,
+ description: 'Thunderstorm',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #8A95A3, #5F6A7A 38%, #475260 65%, #2F3A48)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #C8D1DD, #94A3B8 32%, #64748B 65%, #475569)',
+ },
+ 96: {
+ icon: 'severe-thunderstorm.svg',
+ description: 'Thunderstorm + Hail',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #7A8593, #515C6D 38%, #3A4552 65%, #242D3A)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #B0BBC8, #64748B 32%, #475569 65%, #334155)',
+ },
+ 99: {
+ icon: 'severe-thunderstorm.svg',
+ description: 'Severe Thunderstorm',
+ gradient: isDarkMode
+ ? 'radial-gradient(ellipse 150% 100% at 50% 100%, #6A7583, #434E5D 40%, #2F3A47 68%, #1C2530)'
+ : 'radial-gradient(ellipse 150% 100% at 50% 100%, #9BA8B8, #475569 35%, #334155 68%, #1E293B)',
+ },
+ };
+
+ return weatherMap[code] || weatherMap[0];
+};
+
+const Weather = ({
+ location,
+ current,
+ daily,
+ timezone,
+}: WeatherWidgetProps) => {
+ const [isDarkMode, setIsDarkMode] = useState(false);
+
+ useEffect(() => {
+ const checkDarkMode = () => {
+ setIsDarkMode(document.documentElement.classList.contains('dark'));
+ };
+
+ checkDarkMode();
+
+ const observer = new MutationObserver(checkDarkMode);
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['class'],
+ });
+
+ return () => observer.disconnect();
+ }, []);
+
+ const weatherInfo = useMemo(
+ () =>
+ getWeatherInfo(
+ current?.weather_code || 0,
+ current?.is_day === 1,
+ isDarkMode,
+ ),
+ [current?.weather_code, current?.is_day, isDarkMode],
+ );
+
+ const forecast = useMemo(() => {
+ if (!daily?.time || daily.time.length === 0) return [];
+
+ return daily.time.slice(1, 7).map((time, idx) => {
+ const date = new Date(time);
+ const dayName = date.toLocaleDateString('en-US', { weekday: 'short' });
+ const isDay = true;
+ const weatherCode = daily.weather_code[idx + 1];
+ const info = getWeatherInfo(weatherCode, isDay, isDarkMode);
+
+ return {
+ day: dayName,
+ icon: info.icon,
+ high: Math.round(daily.temperature_2m_max[idx + 1]),
+ low: Math.round(daily.temperature_2m_min[idx + 1]),
+ highF: Math.round((daily.temperature_2m_max[idx + 1] * 9) / 5 + 32),
+ lowF: Math.round((daily.temperature_2m_min[idx + 1] * 9) / 5 + 32),
+ precipitation: daily.precipitation_probability_max[idx + 1] || 0,
+ };
+ });
+ }, [daily, isDarkMode]);
+
+ if (!current || !daily || !daily.time || daily.time.length === 0) {
+ return (
+
+
+
Weather data unavailable for {location}
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {Math.round(current.temperature_2m)}°
+
+ F C
+
+
+ {weatherInfo.description}
+
+
+
+
+
+ {Math.round(daily.temperature_2m_max[0])}°{' '}
+ {Math.round(daily.temperature_2m_min[0])}°
+
+
+
+
+
+
{location}
+
+ {new Date(current.time).toLocaleString('en-US', {
+ weekday: 'short',
+ hour: 'numeric',
+ minute: '2-digit',
+ })}
+
+
+
+
+ {forecast.map((day, idx) => (
+
+
{day.day}
+
+
+ {day.high}°
+
+ {day.low}°
+
+
+ {day.precipitation > 0 && (
+
+
+
+ {day.precipitation}%
+
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+ Wind
+
+
+ {Math.round(current.wind_speed_10m)} km/h
+
+
+
+
+
+
+
+
+ Humidity
+
+
+ {Math.round(current.relative_humidity_2m)}%
+
+
+
+
+
+
+
+
+ Feels Like
+
+
+ {Math.round(current.apparent_temperature)}°C
+
+
+
+
+
+
+ );
+};
+
+export default Weather;
diff --git a/src/lib/agents/search/classifier/intents/widgetResponse.ts b/src/lib/agents/search/classifier/intents/widgetResponse.ts
index 0cfd58d..a5b0885 100644
--- a/src/lib/agents/search/classifier/intents/widgetResponse.ts
+++ b/src/lib/agents/search/classifier/intents/widgetResponse.ts
@@ -1,9 +1,45 @@
import { Intent } from '../../types';
+const description = `Use this intent when the user's query can be fully or partially answered using specialized widgets that provide structured, real-time data (weather, stocks, calculations, and more).
+
+#### When to use:
+1. The user is asking for specific information that a widget can provide (current weather, stock prices, mathematical calculations, unit conversions, etc.).
+2. A widget can completely answer the query without needing additional web search (use this intent alone and set skipSearch to true).
+3. A widget can provide part of the answer, but additional information from web search or other sources is needed (combine with other intents like 'web_search' and set skipSearch to false).
+
+#### Example use cases:
+Note: These are just examples - there are several other widgets available for use depending on the user's query.
+
+1. "What is the weather in New York?"
+ - The weather widget can fully answer this query.
+ - Intent: ['widget_response'] with skipSearch: true
+ - Widget: [{ type: 'weather', location: 'New York', lat: 0, lon: 0 }]
+
+2. "What's the weather in San Francisco today? Also tell me some popular events happening there this weekend."
+ - Weather widget provides current conditions, but events require web search.
+ - Intent: ['web_search', 'widget_response'] with skipSearch: false
+ - Widget: [{ type: 'weather', location: 'San Francisco', lat: 0, lon: 0 }]
+
+3. "Calculate 25% of 480"
+ - The calculator widget can fully answer this.
+ - Intent: ['widget_response'] with skipSearch: true
+ - Widget: [{ type: 'calculator', expression: '25% of 480' }]
+
+4. "AAPL stock price and recent Apple news"
+ - Stock widget provides price, but news requires web search.
+ - Intent: ['web_search', 'widget_response'] with skipSearch: false
+ - Widget: [{ type: 'stock', symbol: 'AAPL' }]
+
+5. "What's Tesla's stock doing and how does it compare to competitors?"
+ - Stock widget provides Tesla's price, but comparison analysis requires web search.
+ - Intent: ['web_search', 'widget_response'] with skipSearch: false
+ - Widget: [{ type: 'stock', symbol: 'TSLA' }]
+
+**IMPORTANT**: Set skipSearch to true ONLY if the widget(s) can completely answer the user's query without any additional information. If the user asks for anything beyond what the widget provides (context, explanations, comparisons, related information), combine this intent with 'web_search' and set skipSearch to false.`;
+
const widgetResponseIntent: Intent = {
name: 'widget_response',
- description:
- 'Use this intent to respond to user queries using available widgets when the required information can be obtained from them.',
+ description,
requiresSearch: false,
enabled: (config) => true,
};
diff --git a/src/lib/agents/search/widgets/calculationWidget.ts b/src/lib/agents/search/widgets/calculationWidget.ts
new file mode 100644
index 0000000..c613b40
--- /dev/null
+++ b/src/lib/agents/search/widgets/calculationWidget.ts
@@ -0,0 +1,65 @@
+import z from 'zod';
+import { Widget } from '../types';
+import { evaluate as mathEval } from 'mathjs';
+
+const schema = z.object({
+ type: z.literal('calculation'),
+ expression: z
+ .string()
+ .describe(
+ "A valid mathematical expression to be evaluated (e.g., '2 + 2', '3 * (4 + 5)').",
+ ),
+});
+
+const calculationWidget: Widget = {
+ name: 'calculation',
+ description: `Performs mathematical calculations and evaluates mathematical expressions. Supports arithmetic operations, algebraic equations, functions, and complex mathematical computations.
+
+**What it provides:**
+- Evaluates mathematical expressions and returns computed results
+- Handles basic arithmetic (+, -, *, /)
+- Supports functions (sqrt, sin, cos, log, etc.)
+- Can process complex expressions with parentheses and order of operations
+
+**When to use:**
+- User asks to calculate, compute, or evaluate a mathematical expression
+- Questions like "what is X", "calculate Y", "how much is Z" where X/Y/Z are math expressions
+- Any request involving numbers and mathematical operations
+
+**Example call:**
+{
+ "type": "calculation",
+ "expression": "25% of 480"
+}
+
+{
+ "type": "calculation",
+ "expression": "sqrt(144) + 5 * 2"
+}
+
+**Important:** The expression must be valid mathematical syntax that can be evaluated by mathjs. Format percentages as "0.25 * 480" or "25% of 480". Do not include currency symbols, units, or non-mathematical text in the expression.`,
+ schema: schema,
+ execute: async (params, _) => {
+ try {
+ const result = mathEval(params.expression);
+
+ return {
+ type: 'calculation_result',
+ data: {
+ expression: params.expression,
+ result: result,
+ },
+ };
+ } catch (error) {
+ return {
+ type: 'calculation_result',
+ data: {
+ expression: params.expression,
+ result: `Error evaluating expression: ${error}`,
+ },
+ };
+ }
+ },
+};
+
+export default calculationWidget;
diff --git a/src/lib/agents/search/widgets/index.ts b/src/lib/agents/search/widgets/index.ts
index 7ddc597..ff18d40 100644
--- a/src/lib/agents/search/widgets/index.ts
+++ b/src/lib/agents/search/widgets/index.ts
@@ -1,6 +1,10 @@
+import calculationWidget from './calculationWidget';
import WidgetRegistry from './registry';
import weatherWidget from './weatherWidget';
+import stockWidget from './stockWidget';
WidgetRegistry.register(weatherWidget);
+WidgetRegistry.register(calculationWidget);
+WidgetRegistry.register(stockWidget);
export { WidgetRegistry };
diff --git a/src/lib/agents/search/widgets/stockWidget.ts b/src/lib/agents/search/widgets/stockWidget.ts
new file mode 100644
index 0000000..b4f8b86
--- /dev/null
+++ b/src/lib/agents/search/widgets/stockWidget.ts
@@ -0,0 +1,412 @@
+import z from 'zod';
+import { Widget } from '../types';
+import YahooFinance from 'yahoo-finance2';
+
+const yf = new YahooFinance({
+ suppressNotices: ['yahooSurvey'],
+});
+
+const schema = z.object({
+ type: z.literal('stock'),
+ ticker: z
+ .string()
+ .describe(
+ "The stock ticker symbol in uppercase (e.g., 'AAPL' for Apple Inc., 'TSLA' for Tesla, 'GOOGL' for Google). Use the primary exchange ticker.",
+ ),
+ comparisonTickers: z
+ .array(z.string())
+ .max(3)
+ .describe(
+ "Optional array of up to 3 ticker symbols to compare against the base ticker (e.g., ['MSFT', 'GOOGL', 'META']). Charts will show percentage change comparison.",
+ ),
+});
+
+const stockWidget: Widget = {
+ name: 'stock',
+ description: `Provides comprehensive real-time stock market data and financial information for any publicly traded company. Returns detailed quote data, market status, trading metrics, and company fundamentals.
+
+You can set skipSearch to true if the stock widget can fully answer the user's query without needing additional web search.
+
+**What it provides:**
+- **Real-time Price Data**: Current price, previous close, open price, day's range (high/low)
+- **Market Status**: Whether market is currently open or closed, trading sessions
+- **Trading Metrics**: Volume, average volume, bid/ask prices and sizes
+- **Performance**: Price changes (absolute and percentage), 52-week high/low range
+- **Valuation**: Market capitalization, P/E ratio, earnings per share (EPS)
+- **Dividends**: Dividend rate, dividend yield, ex-dividend date
+- **Company Info**: Full company name, exchange, currency, sector/industry (when available)
+- **Advanced Metrics**: Beta, trailing/forward P/E, book value, price-to-book ratio
+- **Charts Data**: Historical price movements for visualization
+- **Comparison**: Compare up to 3 stocks side-by-side with percentage-based performance visualization
+
+**When to use:**
+- User asks about a stock price ("What's AAPL stock price?", "How is Tesla doing?")
+- Questions about company market performance ("Is Microsoft up or down today?")
+- Requests for stock market data, trading info, or company valuation
+- Queries about dividends, P/E ratio, market cap, or other financial metrics
+- Any stock/equity-related question for a specific company
+- Stock comparisons ("Compare AAPL vs MSFT", "How is TSLA doing vs RIVN and LCID?")
+
+**Example calls:**
+{
+ "type": "stock",
+ "ticker": "AAPL"
+}
+
+{
+ "type": "stock",
+ "ticker": "TSLA",
+ "comparisonTickers": ["RIVN", "LCID"]
+}
+
+{
+ "type": "stock",
+ "ticker": "GOOGL",
+ "comparisonTickers": ["MSFT", "META", "AMZN"]
+}
+
+**Important:**
+- Use the correct ticker symbol (uppercase preferred: AAPL not aapl)
+- For companies with multiple share classes, use the most common one (e.g., GOOGL for Google Class A shares)
+- The widget works for stocks listed on major exchanges (NYSE, NASDAQ, etc.)
+- Returns comprehensive data; the UI will display relevant metrics based on availability
+- Market data may be delayed by 15-20 minutes for free data sources during trading hours`,
+ schema: schema,
+ execute: async (params, _) => {
+ try {
+ const ticker = params.ticker.toUpperCase();
+
+ const quote: any = await yf.quote(ticker);
+
+ const chartPromises = {
+ '1D': yf
+ .chart(ticker, {
+ period1: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
+ period2: new Date(),
+ interval: '5m',
+ })
+ .catch(() => null),
+ '5D': yf
+ .chart(ticker, {
+ period1: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000),
+ period2: new Date(),
+ interval: '15m',
+ })
+ .catch(() => null),
+ '1M': yf
+ .chart(ticker, {
+ period1: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
+ interval: '1d',
+ })
+ .catch(() => null),
+ '3M': yf
+ .chart(ticker, {
+ period1: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
+ interval: '1d',
+ })
+ .catch(() => null),
+ '6M': yf
+ .chart(ticker, {
+ period1: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000),
+ interval: '1d',
+ })
+ .catch(() => null),
+ '1Y': yf
+ .chart(ticker, {
+ period1: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000),
+ interval: '1d',
+ })
+ .catch(() => null),
+ MAX: yf
+ .chart(ticker, {
+ period1: new Date(Date.now() - 10 * 365 * 24 * 60 * 60 * 1000),
+ interval: '1wk',
+ })
+ .catch(() => null),
+ };
+
+ const charts = await Promise.all([
+ chartPromises['1D'],
+ chartPromises['5D'],
+ chartPromises['1M'],
+ chartPromises['3M'],
+ chartPromises['6M'],
+ chartPromises['1Y'],
+ chartPromises['MAX'],
+ ]);
+
+ const [chart1D, chart5D, chart1M, chart3M, chart6M, chart1Y, chartMAX] =
+ charts;
+
+ if (!quote) {
+ throw new Error(`No data found for ticker: ${ticker}`);
+ }
+
+ let comparisonData: any = null;
+ if (params.comparisonTickers.length > 0) {
+ const comparisonPromises = params.comparisonTickers
+ .slice(0, 3)
+ .map(async (compTicker) => {
+ try {
+ const compQuote = await yf.quote(compTicker);
+ const compCharts = await Promise.all([
+ yf
+ .chart(compTicker, {
+ period1: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
+ period2: new Date(),
+ interval: '5m',
+ })
+ .catch(() => null),
+ yf
+ .chart(compTicker, {
+ period1: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000),
+ period2: new Date(),
+ interval: '15m',
+ })
+ .catch(() => null),
+ yf
+ .chart(compTicker, {
+ period1: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
+ interval: '1d',
+ })
+ .catch(() => null),
+ yf
+ .chart(compTicker, {
+ period1: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
+ interval: '1d',
+ })
+ .catch(() => null),
+ yf
+ .chart(compTicker, {
+ period1: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000),
+ interval: '1d',
+ })
+ .catch(() => null),
+ yf
+ .chart(compTicker, {
+ period1: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000),
+ interval: '1d',
+ })
+ .catch(() => null),
+ yf
+ .chart(compTicker, {
+ period1: new Date(
+ Date.now() - 10 * 365 * 24 * 60 * 60 * 1000,
+ ),
+ interval: '1wk',
+ })
+ .catch(() => null),
+ ]);
+ return {
+ ticker: compTicker,
+ name: compQuote.shortName || compTicker,
+ charts: compCharts,
+ };
+ } catch (error) {
+ console.error(
+ `Failed to fetch comparison ticker ${compTicker}:`,
+ error,
+ );
+ return null;
+ }
+ });
+ const compResults = await Promise.all(comparisonPromises);
+ comparisonData = compResults.filter((r) => r !== null);
+ }
+
+ const stockData = {
+ symbol: quote.symbol,
+ shortName: quote.shortName || quote.longName || ticker,
+ longName: quote.longName,
+ exchange: quote.fullExchangeName || quote.exchange,
+ currency: quote.currency,
+ quoteType: quote.quoteType,
+
+ marketState: quote.marketState,
+ regularMarketTime: quote.regularMarketTime,
+ postMarketTime: quote.postMarketTime,
+ preMarketTime: quote.preMarketTime,
+
+ regularMarketPrice: quote.regularMarketPrice,
+ regularMarketChange: quote.regularMarketChange,
+ regularMarketChangePercent: quote.regularMarketChangePercent,
+ regularMarketPreviousClose: quote.regularMarketPreviousClose,
+ regularMarketOpen: quote.regularMarketOpen,
+ regularMarketDayHigh: quote.regularMarketDayHigh,
+ regularMarketDayLow: quote.regularMarketDayLow,
+
+ postMarketPrice: quote.postMarketPrice,
+ postMarketChange: quote.postMarketChange,
+ postMarketChangePercent: quote.postMarketChangePercent,
+ preMarketPrice: quote.preMarketPrice,
+ preMarketChange: quote.preMarketChange,
+ preMarketChangePercent: quote.preMarketChangePercent,
+
+ regularMarketVolume: quote.regularMarketVolume,
+ averageDailyVolume3Month: quote.averageDailyVolume3Month,
+ averageDailyVolume10Day: quote.averageDailyVolume10Day,
+ bid: quote.bid,
+ bidSize: quote.bidSize,
+ ask: quote.ask,
+ askSize: quote.askSize,
+
+ fiftyTwoWeekLow: quote.fiftyTwoWeekLow,
+ fiftyTwoWeekHigh: quote.fiftyTwoWeekHigh,
+ fiftyTwoWeekChange: quote.fiftyTwoWeekChange,
+ fiftyTwoWeekChangePercent: quote.fiftyTwoWeekChangePercent,
+
+ marketCap: quote.marketCap,
+ trailingPE: quote.trailingPE,
+ forwardPE: quote.forwardPE,
+ priceToBook: quote.priceToBook,
+ bookValue: quote.bookValue,
+ earningsPerShare: quote.epsTrailingTwelveMonths,
+ epsForward: quote.epsForward,
+
+ dividendRate: quote.dividendRate,
+ dividendYield: quote.dividendYield,
+ exDividendDate: quote.exDividendDate,
+ trailingAnnualDividendRate: quote.trailingAnnualDividendRate,
+ trailingAnnualDividendYield: quote.trailingAnnualDividendYield,
+
+ beta: quote.beta,
+
+ fiftyDayAverage: quote.fiftyDayAverage,
+ fiftyDayAverageChange: quote.fiftyDayAverageChange,
+ fiftyDayAverageChangePercent: quote.fiftyDayAverageChangePercent,
+ twoHundredDayAverage: quote.twoHundredDayAverage,
+ twoHundredDayAverageChange: quote.twoHundredDayAverageChange,
+ twoHundredDayAverageChangePercent:
+ quote.twoHundredDayAverageChangePercent,
+
+ sector: quote.sector,
+ industry: quote.industry,
+ website: quote.website,
+
+ chartData: {
+ '1D': chart1D
+ ? {
+ timestamps: chart1D.quotes.map((q: any) => q.date.getTime()),
+ prices: chart1D.quotes.map((q: any) => q.close),
+ }
+ : null,
+ '5D': chart5D
+ ? {
+ timestamps: chart5D.quotes.map((q: any) => q.date.getTime()),
+ prices: chart5D.quotes.map((q: any) => q.close),
+ }
+ : null,
+ '1M': chart1M
+ ? {
+ timestamps: chart1M.quotes.map((q: any) => q.date.getTime()),
+ prices: chart1M.quotes.map((q: any) => q.close),
+ }
+ : null,
+ '3M': chart3M
+ ? {
+ timestamps: chart3M.quotes.map((q: any) => q.date.getTime()),
+ prices: chart3M.quotes.map((q: any) => q.close),
+ }
+ : null,
+ '6M': chart6M
+ ? {
+ timestamps: chart6M.quotes.map((q: any) => q.date.getTime()),
+ prices: chart6M.quotes.map((q: any) => q.close),
+ }
+ : null,
+ '1Y': chart1Y
+ ? {
+ timestamps: chart1Y.quotes.map((q: any) => q.date.getTime()),
+ prices: chart1Y.quotes.map((q: any) => q.close),
+ }
+ : null,
+ MAX: chartMAX
+ ? {
+ timestamps: chartMAX.quotes.map((q: any) => q.date.getTime()),
+ prices: chartMAX.quotes.map((q: any) => q.close),
+ }
+ : null,
+ },
+ comparisonData: comparisonData
+ ? comparisonData.map((comp: any) => ({
+ ticker: comp.ticker,
+ name: comp.name,
+ chartData: {
+ '1D': comp.charts[0]
+ ? {
+ timestamps: comp.charts[0].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[0].quotes.map((q: any) => q.close),
+ }
+ : null,
+ '5D': comp.charts[1]
+ ? {
+ timestamps: comp.charts[1].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[1].quotes.map((q: any) => q.close),
+ }
+ : null,
+ '1M': comp.charts[2]
+ ? {
+ timestamps: comp.charts[2].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[2].quotes.map((q: any) => q.close),
+ }
+ : null,
+ '3M': comp.charts[3]
+ ? {
+ timestamps: comp.charts[3].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[3].quotes.map((q: any) => q.close),
+ }
+ : null,
+ '6M': comp.charts[4]
+ ? {
+ timestamps: comp.charts[4].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[4].quotes.map((q: any) => q.close),
+ }
+ : null,
+ '1Y': comp.charts[5]
+ ? {
+ timestamps: comp.charts[5].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[5].quotes.map((q: any) => q.close),
+ }
+ : null,
+ MAX: comp.charts[6]
+ ? {
+ timestamps: comp.charts[6].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[6].quotes.map((q: any) => q.close),
+ }
+ : null,
+ },
+ }))
+ : null,
+ };
+
+ return {
+ type: 'stock',
+ data: stockData,
+ };
+ } catch (error: any) {
+ return {
+ type: 'stock',
+ data: {
+ error: `Error fetching stock data: ${error.message || error}`,
+ ticker: params.ticker,
+ },
+ };
+ }
+ },
+};
+
+export default stockWidget;
diff --git a/src/lib/agents/search/widgets/weatherWidget.ts b/src/lib/agents/search/widgets/weatherWidget.ts
index b9d048c..4b2dcf9 100644
--- a/src/lib/agents/search/widgets/weatherWidget.ts
+++ b/src/lib/agents/search/widgets/weatherWidget.ts
@@ -20,104 +20,155 @@ const WeatherWidgetSchema = z.object({
),
});
-const weatherWidget = {
+const weatherWidget: Widget = {
name: 'weather',
- description:
- 'Provides current weather information for a specified location. It can return details such as temperature, humidity, wind speed, and weather conditions. It needs either a location name or latitude/longitude coordinates to function.',
+ description: `Provides comprehensive current weather information and forecasts for any location worldwide. Returns real-time weather data including temperature, conditions, humidity, wind, and multi-day forecasts.
+
+You can set skipSearch to true if the weather widget can fully answer the user's query without needing additional web search.
+
+**What it provides:**
+- Current weather conditions (temperature, feels-like, humidity, precipitation)
+- Wind speed, direction, and gusts
+- Weather codes/conditions (clear, cloudy, rainy, etc.)
+- Hourly forecast for next 24 hours
+- Daily forecast for next 7 days (high/low temps, precipitation probability)
+- Timezone information
+
+**When to use:**
+- User asks about weather in a location ("weather in X", "is it raining in Y")
+- Questions about temperature, conditions, or forecast
+- Any weather-related query for a specific place
+
+**Example call:**
+{
+ "type": "weather",
+ "location": "San Francisco, CA, USA",
+ "lat": 0,
+ "lon": 0
+}
+
+**Important:** Provide EITHER a location name OR latitude/longitude coordinates, never both. If using location name, set lat/lon to 0. Location should be specific (city, state/region, country) for best results.`,
schema: WeatherWidgetSchema,
execute: async (params, _) => {
- if (
- params.location === '' &&
- (params.lat === undefined || params.lon === undefined)
- ) {
- throw new Error(
- 'Either location name or both latitude and longitude must be provided.',
- );
- }
-
- if (params.location !== '') {
- const openStreetMapUrl = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(params.location)}&format=json&limit=1`;
-
- const locationRes = await fetch(openStreetMapUrl, {
- headers: {
- 'User-Agent': 'Perplexica',
- 'Content-Type': 'application/json',
- },
- });
-
- const data = await locationRes.json();
-
- const location = data[0];
-
- if (!location) {
+ try {
+ if (
+ params.location === '' &&
+ (params.lat === undefined || params.lon === undefined)
+ ) {
throw new Error(
- `Could not find coordinates for location: ${params.location}`,
+ 'Either location name or both latitude and longitude must be provided.',
);
}
- const weatherRes = await fetch(
- `https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lon}¤t_weather=true`,
- {
+ if (params.location !== '') {
+ const openStreetMapUrl = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(params.location)}&format=json&limit=1`;
+
+ const locationRes = await fetch(openStreetMapUrl, {
headers: {
'User-Agent': 'Perplexica',
'Content-Type': 'application/json',
},
- },
- );
+ });
- const weatherData = await weatherRes.json();
+ const data = await locationRes.json();
+
+ const location = data[0];
+
+ if (!location) {
+ throw new Error(
+ `Could not find coordinates for location: ${params.location}`,
+ );
+ }
+
+ const weatherRes = await fetch(
+ `https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max&timezone=auto&forecast_days=7`,
+ {
+ headers: {
+ 'User-Agent': 'Perplexica',
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ const weatherData = await weatherRes.json();
+
+ return {
+ type: 'weather',
+ data: {
+ location: params.location,
+ latitude: location.lat,
+ longitude: location.lon,
+ current: weatherData.current,
+ hourly: {
+ time: weatherData.hourly.time.slice(0, 24),
+ temperature_2m: weatherData.hourly.temperature_2m.slice(0, 24),
+ precipitation_probability:
+ weatherData.hourly.precipitation_probability.slice(0, 24),
+ precipitation: weatherData.hourly.precipitation.slice(0, 24),
+ weather_code: weatherData.hourly.weather_code.slice(0, 24),
+ },
+ daily: weatherData.daily,
+ timezone: weatherData.timezone,
+ },
+ };
+ } else if (params.lat !== undefined && params.lon !== undefined) {
+ const [weatherRes, locationRes] = await Promise.all([
+ fetch(
+ `https://api.open-meteo.com/v1/forecast?latitude=${params.lat}&longitude=${params.lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max&timezone=auto&forecast_days=7`,
+ {
+ headers: {
+ 'User-Agent': 'Perplexica',
+ 'Content-Type': 'application/json',
+ },
+ },
+ ),
+ fetch(
+ `https://nominatim.openstreetmap.org/reverse?lat=${params.lat}&lon=${params.lon}&format=json`,
+ {
+ headers: {
+ 'User-Agent': 'Perplexica',
+ 'Content-Type': 'application/json',
+ },
+ },
+ ),
+ ]);
+
+ const weatherData = await weatherRes.json();
+ const locationData = await locationRes.json();
+
+ return {
+ type: 'weather',
+ data: {
+ location: locationData.display_name,
+ latitude: params.lat,
+ longitude: params.lon,
+ current: weatherData.current,
+ hourly: {
+ time: weatherData.hourly.time.slice(0, 24),
+ temperature_2m: weatherData.hourly.temperature_2m.slice(0, 24),
+ precipitation_probability:
+ weatherData.hourly.precipitation_probability.slice(0, 24),
+ precipitation: weatherData.hourly.precipitation.slice(0, 24),
+ weather_code: weatherData.hourly.weather_code.slice(0, 24),
+ },
+ daily: weatherData.daily,
+ timezone: weatherData.timezone,
+ },
+ };
+ }
- /* this is like a very simple implementation just to see the bacckend works, when we're working on the frontend, we'll return more data i guess? */
return {
type: 'weather',
- data: {
- location: params.location,
- latitude: location.lat,
- longitude: location.lon,
- weather: weatherData.current_weather,
- },
+ data: null,
};
- } else if (params.lat !== undefined && params.lon !== undefined) {
- const [weatherRes, locationRes] = await Promise.all([
- fetch(
- `https://api.open-meteo.com/v1/forecast?latitude=${params.lat}&longitude=${params.lon}¤t_weather=true`,
- {
- headers: {
- 'User-Agent': 'Perplexica',
- 'Content-Type': 'application/json',
- },
- },
- ),
- fetch(
- `https://nominatim.openstreetmap.org/reverse?lat=${params.lat}&lon=${params.lon}&format=json`,
- {
- headers: {
- 'User-Agent': 'Perplexica',
- 'Content-Type': 'application/json',
- },
- },
- ),
- ]);
-
- const weatherData = await weatherRes.json();
- const locationData = await locationRes.json();
-
+ } catch (err) {
return {
type: 'weather',
data: {
- location: locationData.display_name,
- latitude: params.lat,
- longitude: params.lon,
- weather: weatherData.current_weather,
+ error: `Error fetching weather data: ${err}`,
},
};
}
-
- return {
- type: 'weather',
- data: null,
- };
},
-} satisfies Widget;
-
+};
export default weatherWidget;
From 7c9258cfc978e2ec311e99999e45d3599e7a9bbd Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:47:11 +0530
Subject: [PATCH 044/199] feat(intents): update intent prompt
---
.../classifier/intents/academicSearch.ts | 45 ++++++++++++++++-
.../classifier/intents/discussionSearch.ts | 50 +++++++++++++++++--
.../agents/search/classifier/intents/index.ts | 2 +
.../search/classifier/intents/registry.ts | 8 +--
.../search/classifier/intents/webSearch.ts | 24 ++++++++-
.../search/classifier/intents/writingTask.ts | 46 ++++++++++++++++-
6 files changed, 163 insertions(+), 12 deletions(-)
diff --git a/src/lib/agents/search/classifier/intents/academicSearch.ts b/src/lib/agents/search/classifier/intents/academicSearch.ts
index b6da377..199f583 100644
--- a/src/lib/agents/search/classifier/intents/academicSearch.ts
+++ b/src/lib/agents/search/classifier/intents/academicSearch.ts
@@ -1,9 +1,50 @@
import { Intent } from '../../types';
+const description = `Use this intent to search for scholarly articles, research papers, scientific studies, and academic resources when the user explicitly requests credible, peer-reviewed, or authoritative information from academic sources.
+
+#### When to use:
+1. User explicitly mentions academic keywords: research papers, scientific studies, scholarly articles, peer-reviewed, journal articles.
+2. User asks for scientific evidence or academic research on a topic.
+3. User needs authoritative, citation-worthy sources for research or academic purposes.
+
+#### When NOT to use:
+1. General questions that don't specifically request academic sources - use 'web_search' instead.
+2. User just wants general information without specifying academic sources.
+3. Casual queries about facts or current events.
+
+#### Example use cases:
+1. "Find scientific papers on climate change effects"
+ - User explicitly wants scientific papers.
+ - Intent: ['academic_search'] with skipSearch: false
+
+2. "What does the research say about meditation benefits?"
+ - User is asking for research-based information.
+ - Intent: ['academic_search', 'web_search'] with skipSearch: false
+
+3. "Show me peer-reviewed articles on CRISPR technology"
+ - User specifically wants peer-reviewed academic content.
+ - Intent: ['academic_search'] with skipSearch: false
+
+4. "I need scholarly sources about renewable energy for my thesis"
+ - User explicitly needs scholarly/academic sources.
+ - Intent: ['academic_search'] with skipSearch: false
+
+5. "Explain quantum computing" (WRONG to use academic_search alone)
+ - This is a general question, not specifically requesting academic papers.
+ - Correct intent: ['web_search'] with skipSearch: false
+ - Could combine: ['web_search', 'academic_search'] if you want both general and academic sources
+
+6. "What's the latest study on sleep patterns?"
+ - User mentions "study" - combine academic and web search for comprehensive results.
+ - Intent: ['academic_search', 'web_search'] with skipSearch: false
+
+**IMPORTANT**: This intent can be combined with 'web_search' to provide both academic papers and general web information. Always set skipSearch to false when using this intent.
+
+**NOTE**: This intent is only available if academic search sources are enabled in the configuration.`;
+
const academicSearchIntent: Intent = {
name: 'academic_search',
- description:
- 'Use this intent to find scholarly articles, research papers, and academic resources when the user is seeking credible and authoritative information on a specific topic.',
+ description,
requiresSearch: true,
enabled: (config) => config.sources.includes('academic'),
};
diff --git a/src/lib/agents/search/classifier/intents/discussionSearch.ts b/src/lib/agents/search/classifier/intents/discussionSearch.ts
index 76b3b01..b7e2cfd 100644
--- a/src/lib/agents/search/classifier/intents/discussionSearch.ts
+++ b/src/lib/agents/search/classifier/intents/discussionSearch.ts
@@ -1,9 +1,53 @@
import { Intent } from '../../types';
+const description = `Use this intent to search through discussion forums, community boards, and social platforms (Reddit, forums, etc.) when the user explicitly wants opinions, personal experiences, community discussions, or crowd-sourced information.
+
+#### When to use:
+1. User explicitly mentions: Reddit, forums, discussion boards, community opinions, "what do people think", "user experiences".
+2. User is asking for opinions, reviews, or personal experiences about a product, service, or topic.
+3. User wants to know what communities or people are saying about something.
+
+#### When NOT to use:
+1. General questions that don't specifically ask for opinions or discussions - use 'web_search' instead.
+2. User wants factual information or official sources.
+3. Casual queries about facts, news, or current events without requesting community input.
+
+#### Example use cases:
+1. "What do people on Reddit think about the new iPhone?"
+ - User explicitly wants Reddit/community opinions.
+ - Intent: ['discussions_search'] with skipSearch: false
+
+2. "User experiences with Tesla Model 3"
+ - User is asking for personal experiences from users.
+ - Intent: ['discussions_search'] with skipSearch: false
+
+3. "Best gaming laptop according to forums"
+ - User wants forum/community recommendations.
+ - Intent: ['discussions_search'] with skipSearch: false
+
+4. "What are people saying about the new AI regulations?"
+ - User wants community discussions/opinions.
+ - Intent: ['discussions_search', 'web_search'] with skipSearch: false
+
+5. "Reviews and user opinions on the Framework laptop"
+ - Combines user opinions with general reviews.
+ - Intent: ['discussions_search', 'web_search'] with skipSearch: false
+
+6. "What's the price of iPhone 15?" (WRONG to use discussions_search)
+ - This is a factual question, not asking for opinions.
+ - Correct intent: ['web_search'] with skipSearch: false
+
+7. "Explain how OAuth works" (WRONG to use discussions_search)
+ - This is asking for information, not community opinions.
+ - Correct intent: ['web_search'] with skipSearch: false
+
+**IMPORTANT**: This intent can be combined with 'web_search' to provide both community discussions and official/factual information. Always set skipSearch to false when using this intent.
+
+**NOTE**: This intent is only available if discussion search sources are enabled in the configuration.`;
+
const discussionSearchIntent: Intent = {
- name: 'discussion_search',
- description:
- 'Use this intent to search through discussion forums, community boards, or social media platforms when the user is looking for opinions, experiences, or community-driven information on a specific topic.',
+ name: 'discussions_search',
+ description,
requiresSearch: true,
enabled: (config) => config.sources.includes('discussions'),
};
diff --git a/src/lib/agents/search/classifier/intents/index.ts b/src/lib/agents/search/classifier/intents/index.ts
index feefd2d..fcab1c7 100644
--- a/src/lib/agents/search/classifier/intents/index.ts
+++ b/src/lib/agents/search/classifier/intents/index.ts
@@ -1,5 +1,6 @@
import academicSearchIntent from './academicSearch';
import discussionSearchIntent from './discussionSearch';
+import privateSearchIntent from './privateSearch';
import IntentRegistry from './registry';
import webSearchIntent from './webSearch';
import widgetResponseIntent from './widgetResponse';
@@ -10,5 +11,6 @@ IntentRegistry.register(academicSearchIntent);
IntentRegistry.register(discussionSearchIntent);
IntentRegistry.register(widgetResponseIntent);
IntentRegistry.register(writingTaskIntent);
+IntentRegistry.register(privateSearchIntent);
export { IntentRegistry };
diff --git a/src/lib/agents/search/classifier/intents/registry.ts b/src/lib/agents/search/classifier/intents/registry.ts
index bc3464b..4efdbc4 100644
--- a/src/lib/agents/search/classifier/intents/registry.ts
+++ b/src/lib/agents/search/classifier/intents/registry.ts
@@ -18,10 +18,12 @@ class IntentRegistry {
}
static getDescriptions(config: { sources: SearchSources[] }): string {
- const availableintnets = this.getAvailableIntents(config);
+ const availableintents = this.getAvailableIntents(config);
- return availableintnets
- .map((intent) => `${intent.name}: ${intent.description}`)
+ return availableintents
+ .map(
+ (intent) => `-------\n\n###${intent.name}: ${intent.description}\n\n`,
+ )
.join('\n\n');
}
}
diff --git a/src/lib/agents/search/classifier/intents/webSearch.ts b/src/lib/agents/search/classifier/intents/webSearch.ts
index 9fccd2f..3f795e4 100644
--- a/src/lib/agents/search/classifier/intents/webSearch.ts
+++ b/src/lib/agents/search/classifier/intents/webSearch.ts
@@ -1,9 +1,29 @@
import { Intent } from '../../types';
+const description = `
+Use this intent to find current information from the web when the user is asking a question or needs up-to-date information that cannot be provided by widgets or other intents.
+
+#### When to use:
+1. Simple user questions about current events, news, weather, or general knowledge that require the latest information and there is no specific better intent to use.
+2. When the user explicitly requests information from the web or indicates they want the most recent data (and still there's no other better intent).
+3. When no widgets can fully satisfy the user's request for information nor any other specialized search intent applies.
+
+#### Examples use cases:
+1. "What is the weather in San Francisco today? ALso tell me some popular events happening there this weekend."
+ - In this case, the weather widget can provide the current weather, but for popular events, a web search is needed. So the intent should include a 'web_search' & a 'widget_response'.
+
+2. "Who won the Oscar for Best Picture in 2024?"
+ - This is a straightforward question that requires current information from the web.
+
+3. "Give me the latest news on AI regulations."
+ - The user is asking for up-to-date news, which necessitates a web search.
+
+**IMPORTANT**: If this intent is given then skip search should be false.
+`;
+
const webSearchIntent: Intent = {
name: 'web_search',
- description:
- 'Use this intent to find current information from the web when the user is asking a question or needs up-to-date information that cannot be provided by widgets or other intents.',
+ description: description,
requiresSearch: true,
enabled: (config) => config.sources.includes('web'),
};
diff --git a/src/lib/agents/search/classifier/intents/writingTask.ts b/src/lib/agents/search/classifier/intents/writingTask.ts
index 95b5af6..3edc0b3 100644
--- a/src/lib/agents/search/classifier/intents/writingTask.ts
+++ b/src/lib/agents/search/classifier/intents/writingTask.ts
@@ -1,9 +1,51 @@
import { Intent } from '../../types';
+const description = `Use this intent for simple writing or greeting tasks that do not require any external information or facts. This is ONLY for greetings and straightforward creative writing that needs no factual verification.
+
+#### When to use:
+1. User greetings or simple social interactions (hello, hi, thanks, goodbye).
+2. Creative writing tasks that require NO factual information (poems, birthday messages, thank you notes).
+3. Simple drafting tasks where the user provides all necessary information.
+
+#### When NOT to use:
+1. ANY question that starts with "what", "how", "why", "when", "where", "who" - these need web_search.
+2. Requests for explanations, definitions, or information about anything.
+3. Code-related questions or technical help - these need web_search.
+4. Writing tasks that require facts, data, or current information.
+5. When you're uncertain about any information needed - default to web_search.
+
+#### Example use cases:
+1. "Hello" or "Hi there"
+ - Simple greeting, no information needed.
+ - Intent: ['writing_task'] with skipSearch: true
+
+2. "Write me a birthday message for my friend"
+ - Creative writing, no facts needed.
+ - Intent: ['writing_task'] with skipSearch: true
+
+3. "Draft a thank you email for a job interview"
+ - Simple writing task, no external information required.
+ - Intent: ['writing_task'] with skipSearch: true
+
+4. "What is React?" (WRONG to use writing_task)
+ - This is a QUESTION asking for information.
+ - Correct intent: ['web_search'] with skipSearch: false
+
+5. "How do I fix this error in Python?" (WRONG to use writing_task)
+ - This is asking for technical help.
+ - Correct intent: ['web_search'] with skipSearch: false
+
+6. "Write an email about the latest AI developments" (WRONG to use writing_task alone)
+ - This requires current information about AI developments.
+ - Correct intent: ['web_search'] with skipSearch: false
+
+**CRITICAL RULE**: When in doubt, DO NOT use this intent. Default to web_search. This intent should be rare - only use it for greetings and purely creative writing tasks that need absolutely no facts or information.
+
+**IMPORTANT**: If this intent is used alone, skipSearch should be true. Never combine this with other search intents unless you're absolutely certain both are needed.`;
+
const writingTaskIntent: Intent = {
name: 'writing_task',
- description:
- 'Use this intent to assist users with writing tasks such as drafting emails, creating documents, or generating content based on their instructions or greetings.',
+ description,
requiresSearch: false,
enabled: (config) => true,
};
From 730ee0ff4173355368838c82c900e9bfa8b70f20 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:47:18 +0530
Subject: [PATCH 045/199] feat(intents): add private search
---
.../classifier/intents/privateSearch.ts | 47 +++++++++++++++++++
1 file changed, 47 insertions(+)
create mode 100644 src/lib/agents/search/classifier/intents/privateSearch.ts
diff --git a/src/lib/agents/search/classifier/intents/privateSearch.ts b/src/lib/agents/search/classifier/intents/privateSearch.ts
new file mode 100644
index 0000000..8ebc0df
--- /dev/null
+++ b/src/lib/agents/search/classifier/intents/privateSearch.ts
@@ -0,0 +1,47 @@
+import { Intent } from '../../types';
+
+const description = `Use this intent to search through the user's uploaded documents or provided web page links when they ask questions about their personal files or specific URLs.
+
+#### When to use:
+1. User explicitly asks about uploaded documents ("tell me about the document I uploaded", "summarize this file").
+2. User provides specific URLs/links and asks questions about them ("tell me about example.com", "what's on this page: url.com").
+3. User references "my documents", "the file I shared", "this link" when files or URLs are available.
+
+#### When NOT to use:
+1. User asks generic questions like "summarize" without providing context or files (later the system will ask what they want summarized).
+2. No files have been uploaded and no URLs provided - use web_search or other intents instead.
+3. User is asking general questions unrelated to their uploaded content.
+
+#### Example use cases:
+1. "Tell me about the PDF I uploaded"
+ - Files are uploaded, user wants information from them.
+ - Intent: ['private_search'] with skipSearch: false
+
+2. "What's the main point from example.com?"
+ - User provided a specific URL to analyze.
+ - Intent: ['private_search'] with skipSearch: false
+
+3. "Summarize the research paper I shared"
+ - User references a shared document.
+ - Intent: ['private_search'] with skipSearch: false
+
+4. "Summarize" (WRONG to use private_search if no files/URLs)
+ - No context provided, no files uploaded.
+ - Correct: Skip this intent, let the answer agent ask what to summarize
+
+5. "What does my document say about climate change and also search the web for recent updates?"
+ - Combine private document search with web search.
+ - Intent: ['private_search', 'web_search'] with skipSearch: false
+
+**IMPORTANT**: Only use this intent if files are actually uploaded or URLs are explicitly provided in the query. Check the context for uploaded files before selecting this intent. Always set skipSearch to false when using this intent.
+
+**NOTE**: This intent can be combined with other search intents when the user wants both personal document information and external sources.`;
+
+const privateSearchIntent: Intent = {
+ name: 'private_search',
+ description,
+ enabled: (config) => true,
+ requiresSearch: true,
+};
+
+export default privateSearchIntent;
From 8dec689a451ae7698bee42d8cc898b9c7e8ceb5f Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:47:28 +0530
Subject: [PATCH 046/199] feat(prompts): update classifier prompt
---
src/lib/prompts/search/classifier.ts | 298 +++++++++++++++------------
1 file changed, 162 insertions(+), 136 deletions(-)
diff --git a/src/lib/prompts/search/classifier.ts b/src/lib/prompts/search/classifier.ts
index 40e3203..9301b84 100644
--- a/src/lib/prompts/search/classifier.ts
+++ b/src/lib/prompts/search/classifier.ts
@@ -4,7 +4,7 @@ export const getClassifierPrompt = (input: {
}) => {
return `
-You are an expert query classifier for an intelligent search agent. Your task is to analyze user queries and determine the optimal way to answer them—selecting the right intent(s) and widgets.
+You are an expert query classifier for an AI-powered search engine. Your task is to analyze user queries and determine the optimal strategy to answer them—selecting the right search intent(s) and widgets that will render in the UI.
@@ -12,165 +12,191 @@ Given a conversation history and follow-up question, you must:
1. Determine if search should be skipped (skipSearch: boolean)
2. Generate a standalone, self-contained version of the question (standaloneFollowUp: string)
3. Identify the intent(s) that describe how to fulfill the query (intent: array)
-4. Select appropriate widgets (widgets: array)
+4. Select appropriate widgets that will enhance the UI response (widgets: array)
-
-**THE MOST IMPORTANT RULE**: skipSearch should be TRUE only in TWO cases:
-1. Widget-only queries (weather, stocks, calculator)
-2. Greetings or simple writing tasks (NOT questions)
+## Understanding Your Tools
-**DEFAULT TO skipSearch: false** for everything else, including:
-- Any question ("what is", "how does", "explain", "tell me about")
-- Any request for information or facts
-- Anything you're unsure about
+**Intents** define HOW to find or generate information:
+- Different search methods: web search, forum discussions, academic papers, personal documents
+- Generation methods: direct response for greetings, creative writing
+- Each intent represents a different approach to answering the query
+- Multiple intents can be combined for comprehensive answers
-Ask yourself: "Is the user ASKING about something or requesting INFORMATION?"
-- YES → skipSearch: false (use web_search)
-- NO (just greeting or simple writing) → skipSearch: true
-
+**Widgets** are UI components that render structured, real-time data:
+- They display specific types of information (weather forecasts, calculations, stock prices, etc.)
+- They provide interactive, visual elements that enhance the text response
+- They fetch data independently and render directly in the interface
+- They can work alone (widget-only answers) or alongside search results
+
+**Key distinction:** Intents determine the search/generation strategy, while widgets provide visual data enhancements in the UI.
+
+## The Philosophy of skipSearch
+
+Search connects you to external knowledge sources. Skip it only when external knowledge isn't needed.
+
+**Skip search (TRUE) when:**
+- Widgets alone can fully answer the query with their structured data
+- Simple greetings or social pleasantries
+- Pure creative writing requiring absolutely zero facts
+
+**Use search (FALSE) when:**
+- User is asking a question (what, how, why, when, where, who)
+- Any facts, explanations, or information are requested
+- Technical help, code, or learning content is needed
+- Current events, news, or time-sensitive information required
+- Widgets provide partial data but context/explanation needed
+- Uncertain - always default to searching
+
+**Critical rule:** If the user is ASKING about something or requesting INFORMATION, they need search. Question words (what, how, why, explain, tell me) strongly indicate skipSearch should be FALSE.
+
+## How Intents Work
+
+Available intent options:
+${input.intentDesc}
+
+**Understanding intent descriptions:**
+- Each intent description explains what it does and when to use it
+- Read the descriptions carefully to understand their purpose
+- Match user needs to the appropriate intent(s)
+- Can select multiple intents for comprehensive coverage
+
+**Selection strategy:**
+1. Identify what the user is asking for
+2. Review intent descriptions to find matches
+3. Select all relevant intents (can combine multiple)
+4. If user explicitly mentions a source (Reddit, research papers), use that specific intent
+5. Default to general web search for broad questions
+
+## How Widgets Work
+
+Available widget options:
+${input.widgetDesc}
+
+**Understanding widget descriptions:**
+- Each widget description explains what data it provides and how to use it
+- Widgets render as UI components alongside the text response
+- They enhance answers with visual, structured information
+- Review descriptions to identify applicable widgets
+
+**Selection strategy:**
+1. Identify if query needs any structured/real-time data
+2. Check widget descriptions for matches
+3. Include ALL applicable widgets (each type only once)
+4. Widgets work independently - include them even when also searching
+
+**Important widget behaviors:**
+- If widget fully answers query → skipSearch: TRUE, include widget, use widget_response intent
+- If widget provides partial data → skipSearch: FALSE, include widget + appropriate search intent(s)
+- Widgets and search intents coexist - they serve different purposes
+
+## Making Queries Standalone
+
+Transform follow-up questions to be understandable without conversation history:
+
+**Replace vague references:**
+- "it", "that", "this" → specific subjects from context
+- "they", "those" → actual entities being discussed
+- "the previous one" → the actual item from history
+
+**Add necessary context:**
+- Include the topic being discussed
+- Reference specific subjects mentioned earlier
+- Preserve original meaning and scope
+- Don't over-elaborate or change intent
+
+**Example transformations:**
+- Context: Discussing React framework
+- Follow-up: "How does it work?" → Standalone: "How does React work?"
+- Follow-up: "What about hooks?" → Standalone: "What about React hooks?"
+
+## Critical Decision Framework
-
Follow this decision tree IN ORDER:
-1. **Widget-Only Queries** → skipSearch: TRUE, intent: ['widget_response']
- - Weather queries: "weather in NYC", "temperature in Paris", "is it raining in Seattle"
- - Stock queries: "AAPL stock price", "how is Tesla doing", "MSFT stock"
- - Calculator queries: "what is 25% of 80", "calculate 15*23", "sqrt(144)"
- - These are COMPLETE answers—no search needed
+### 1. Widget-Only Queries
+**When:** Query can be fully answered by widget data alone
+**Then:** skipSearch: TRUE, intent: ['widget_response'], include widget(s)
+**Pattern:** Weather requests, calculations, unit conversions, stock prices (when no additional info needed)
-2. **Writing/Greeting Tasks** → skipSearch: TRUE, intent: ['writing_task']
- - ONLY for greetings and simple writing:
- - Greetings: "hello", "hi", "how are you", "thanks", "goodbye"
- - Simple writing needing NO facts: "write a thank you email", "draft a birthday message", "compose a poem"
- - NEVER for: questions, "what is X", "how does X work", explanations, definitions, facts, code help
- - If user is ASKING about something (not requesting writing), use web_search
+### 2. Greeting/Simple Writing Tasks
+**When:** Just greetings OR pure creative writing with zero factual requirements
+**Then:** skipSearch: TRUE, intent: ['writing_task']
+**Pattern:** "hello", "hi", "write a birthday message", "compose a poem"
+**NEVER for:** Questions, explanations, definitions, facts, code help
-3. **Image Display Queries** → skipSearch: FALSE, intent: ['image_preview']
- - "Show me images of cats"
- - "Pictures of the Eiffel Tower"
- - "Visual examples of modern architecture"
- - Requests for images to visualize something
+### 3. Widget + Additional Information
+**When:** Widget provides data but user wants more context/explanation
+**Then:** skipSearch: FALSE, intent: ['appropriate_search', 'widget_response'], include widget(s)
+**Pattern:** "weather in NYC and things to do", "AAPL stock and recent news"
-4. **Widget + Additional Info** → skipSearch: FALSE, intent: ['web_search', 'widget_response']
- - "weather in NYC and best things to do there"
- - "AAPL stock and recent Apple news"
- - "calculate my mortgage and explain how interest works"
+### 4. Pure Search Queries
+**When:** No widgets apply, just information/facts needed
+**Then:** skipSearch: FALSE, select appropriate search intent(s)
+**Strategy:**
+- Default to general web search
+- Use discussion search when user mentions Reddit, forums, opinions
+- Use academic search when user mentions research, papers, studies
+- Use private search when user references uploaded files/URLs
+- Can combine multiple search intents
-5. **Pure Search Queries** → skipSearch: FALSE
- - Default to web_search for general questions
- - Use discussions_search when user explicitly mentions Reddit, forums, opinions, experiences
- - Use academic_search when user explicitly mentions research, papers, studies, scientific
- - Can combine multiple search intents when appropriate
+### 5. Think Before Setting skipSearch to TRUE
+**Ask yourself:**
+- Is the user ASKING about something? → FALSE
+- Is the user requesting INFORMATION? → FALSE
+- Is there ANY factual component? → FALSE
+- Am I uncertain? → FALSE (default to search)
-6. **Fallback when web_search unavailable** → skipSearch: TRUE, intent: ['writing_task'] or []
- - If no search intents are available and no widgets apply
- - Set skipSearch to true and use writing_task or empty intent
-
+## Intent Selection Rules
-
-Example 1: Widget-only query
-Query: "What is the weather in New York?"
-Reasoning: User wants current weather → weather widget provides this completely
-Output: skipSearch: true, intent: ['widget_response'], widgets: [weather widget for New York]
-
-Example 2: Widget-only query
-Query: "AAPL stock price"
-Reasoning: User wants stock price → stock_ticker widget provides this completely
-Output: skipSearch: true, intent: ['widget_response'], widgets: [stock_ticker for AAPL]
-
-Example 3: Widget + search query
-Query: "What's the weather in NYC and what are some good outdoor activities?"
-Reasoning: Weather widget handles weather, but outdoor activities need web search
-Output: skipSearch: false, intent: ['web_search', 'widget_response'], widgets: [weather widget for NYC]
-
-Example 4: Pure search query
-Query: "What are the latest developments in AI?"
-Reasoning: No widget applies, needs current web information
-Output: skipSearch: false, intent: ['web_search'], widgets: []
-
-Example 5: Writing task (greeting/simple writing only)
-Query: "Write me a thank you email for a job interview"
-Reasoning: Simple writing task needing no external facts → writing_task
-Output: skipSearch: true, intent: ['writing_task'], widgets: []
-
-Example 5b: Question about something - ALWAYS needs search
-Query: "What is Kimi K2?"
-Reasoning: User is ASKING about something → needs web search for accurate info
-Output: skipSearch: false, intent: ['web_search'], widgets: []
-
-Example 5c: Another question - needs search
-Query: "Explain how photosynthesis works"
-Reasoning: User is ASKING for explanation → needs web search
-Output: skipSearch: false, intent: ['web_search'], widgets: []
-
-Example 6: Image display
-Query: "Show me images of cats"
-Reasoning: User wants to see images → requires image search
-Output: skipSearch: false, intent: ['image_preview'], widgets: []
-
-Example 7: Multiple search sources
-Query: "What does the research say about meditation benefits?"
-Reasoning: Benefits from both academic papers and web articles
-Output: skipSearch: false, intent: ['academic_search', 'web_search'], widgets: []
-
-Example 8: Discussions search
-Query: "What do people on Reddit think about the new iPhone?"
-Reasoning: User explicitly wants forum/community opinions → discussions_search
-Output: skipSearch: false, intent: ['discussions_search'], widgets: []
-
-Example 9: Academic search only
-Query: "Find scientific papers on climate change effects"
-Reasoning: User explicitly wants academic/research papers
-Output: skipSearch: false, intent: ['academic_search'], widgets: []
-
-
-
-Transform the follow-up into a self-contained question:
-- Include ALL necessary context from chat history
-- Replace pronouns (it, they, this, that) with specific nouns
-- Replace references ("the previous one", "what you mentioned") with actual content
-- Preserve the original complexity—don't over-elaborate simple questions
-- The question should be answerable without seeing the conversation
-
-
-
Available intents:
${input.intentDesc}
-Rules:
+**Rules:**
- Include at least one intent when applicable
-- For questions/information requests:
- - Default to web_search unless user explicitly requests another source
- - Use discussions_search when user mentions: Reddit, forums, opinions, experiences, "what do people think"
- - Use academic_search when user mentions: research, papers, studies, scientific, scholarly
- - Can combine intents (e.g., ['academic_search', 'web_search'])
-- If web_search is NOT in available intents and query needs search:
- - Check if discussions_search or academic_search applies
- - If no search intent available and no widgets: use writing_task or empty array []
-- private_search: ONLY when user provides specific URLs/documents
-- widget_response: when widgets fully answer the query
-- writing_task: ONLY for greetings and simple writing (never for questions)
-
+- For information requests: default to general web search unless user specifies otherwise
+- Use specialized search intents when explicitly requested (discussions, academic, private)
+- Can combine multiple intents: ['academic_search', 'web_search']
+- widget_response: when widgets fully satisfy the query
+- writing_task: ONLY for greetings and simple creative writing (never for questions)
+
+## Widget Selection Rules
-
Available widgets:
${input.widgetDesc}
-Rules:
+**Rules:**
- Include ALL applicable widgets regardless of skipSearch value
-- Each widget type can only be included once
-- Widgets provide structured, real-time data that enhances any response
-
+- Each widget type can only be included once per query
+- Widgets render in the UI to enhance responses with structured data
+- Follow widget descriptions for proper parameter formatting
-
-Your classification must be precise and consistent:
+## Output Format
+
+Your classification must be valid JSON:
+\`\`\`json
{
"skipSearch": ,
- "standaloneFollowUp": "",
- "intent": [],
- "widgets": []
+ "standaloneFollowUp": "",
+ "intent": ["", ""],
+ "widgets": [
+ {
+ "type": "",
+ "": "",
+ "": ""
+ }
+ ]
}
-
- `;
+\`\`\`
+
+## Final Reminders
+
+- **Intents** = HOW to answer (search strategy, generation type)
+- **Widgets** = WHAT to display in UI (structured visual data)
+- **skipSearch** = Can answer without external search? (widgets alone, greetings, pure creativity)
+- **Default to FALSE** = When uncertain, search - better to search unnecessarily than miss information
+- **Read descriptions** = Intent and widget descriptions contain all the information you need to select them properly
+
+Your goal is to understand user intent and route requests through the optimal combination of search methods (intents) and UI enhancements (widgets). Pay close attention to what the user is actually asking for, not just pattern matching keywords.
+`;
};
From f15802b6889cd9f120bf17193a374c8c0b7779f4 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:48:05 +0530
Subject: [PATCH 047/199] feat(prompts): update research prompt
---
src/lib/prompts/search/researcher.ts | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/lib/prompts/search/researcher.ts b/src/lib/prompts/search/researcher.ts
index 2c58b9b..f77559f 100644
--- a/src/lib/prompts/search/researcher.ts
+++ b/src/lib/prompts/search/researcher.ts
@@ -1,6 +1,8 @@
export const getResearcherPrompt = (
actionDesc: string,
- mode: 'fast' | 'balanced' | 'deep_research',
+ mode: 'speed' | 'balanced' | 'quality',
+ i: number,
+ maxIteration: number,
) => {
const today = new Date().toLocaleDateString('en-US', {
year: 'numeric',
@@ -10,17 +12,20 @@ export const getResearcherPrompt = (
return `
You are an action orchestrator. Your job is to fulfill user requests by selecting and executing appropriate actions - whether that's searching for information, creating calendar events, sending emails, or any other available action.
+You will be shared with the conversation history between user and AI, along with the user's latest follow-up question and your previous actions' results (if any. Note that they're per conversation so if they contain any previous actions it was executed for the last follow up (the one you're currently handling)). Based on this, you must decide the best next action(s) to take to fulfill the user's request.
Today's date: ${today}
You are operating in "${mode}" mode. ${
- mode === 'fast'
+ mode === 'speed'
? 'Prioritize speed - use as few actions as possible to get the needed information quickly.'
: mode === 'balanced'
? 'Balance speed and depth - use a moderate number of actions to get good information efficiently. Never stop at the first action unless there is no action available or the query is simple.'
: 'Conduct deep research - use multiple actions to gather comprehensive information, even if it takes longer.'
}
+You are currently on iteration ${i + 1} of your research process and have ${maxIteration} total iterations so please take action accordingly. After max iterations, the done action would get called automatically so you don't have to worry about that unless you want to end the research early.
+
${actionDesc}
@@ -236,6 +241,15 @@ Actions: web_search ["best Italian restaurant near me", "top rated Italian resta
Reasoning should be 2-3 natural sentences showing your thought process and plan. Then select and configure the appropriate action(s).
+
+Always respond in the following JSON format and never deviate from it or output any extra text:
+{
+ "reasoning": "",
+ "actions": [
+ {"type": "", "param1": "value1", "...": "..."},
+ ...
+ ]
+}
`;
};
From 1b4e883f574bbdfb931827eb5f67024259498b7f Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:48:12 +0530
Subject: [PATCH 048/199] feat(prompts): add writer prompt
---
src/lib/prompts/search/writer.ts | 58 ++++++++++++++++++++++++++++++++
1 file changed, 58 insertions(+)
create mode 100644 src/lib/prompts/search/writer.ts
diff --git a/src/lib/prompts/search/writer.ts b/src/lib/prompts/search/writer.ts
new file mode 100644
index 0000000..69a99c3
--- /dev/null
+++ b/src/lib/prompts/search/writer.ts
@@ -0,0 +1,58 @@
+export const getWriterPrompt = (context: string) => {
+ return `
+ You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
+
+ Your task is to provide answers that are:
+ - **Informative and relevant**: Thoroughly address the user's query using the given context.
+ - **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
+ - **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
+ - **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
+ - **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
+
+ ### Formatting Instructions
+ - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate.
+ - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience.
+ - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
+ - **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience.
+ - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
+ - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
+ - **No references or source list at the end**: Do not include a seperate references or sources section at the end of your response. All references are sent to user by the system automatically.
+ - **Do not give the mapping of citations to sources**: Only use the [number] notation in the text. Never return the mapping of citations to sources.
+
+ ### Citation Requirements
+ - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
+ - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
+ - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
+ - Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
+ - Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
+ - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
+ - Avoid citing widget data but you can use it to directly answer without citation.
+ - Never return the mapping of citations to sources; only use the [number] notation in the text. Never return a references or sources section seperately.
+
+ ### Widget Data Usage
+ - Widget data provided in the context can be used directly to answer specific queries (e.g., current weather, stock prices, calculations) without citations.
+ - The widget data is already displayed to the user in a beautiful format (via cards, tables, etc) just before your response so you don't need to generate a very detailed response for widget data. Provide concise answers for such queries.
+ - You can also mention that for more information you can look at the widget displayed above.
+ - For weather data, only provide current weather conditions not forecasts unless explicitly asked for forecasts by the user.
+ - You don't need to cite widget data you can directly use it to answer the user query. NEVER CITE widget OR (any other notation) TO CITE WIDGET DATA.
+
+ ### Special Instructions
+ - If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
+ - If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
+ - If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
+ - If its a simple query (like weather, calculations, definitions), provide concise answers and not a long article.
+
+ ### Example Output
+ - Begin with a brief introduction summarizing the event or query topic.
+ - Follow with detailed sections under clear headings, covering all aspects of the query if possible.
+ - Provide explanations or historical context as needed to enhance understanding.
+ - End with a conclusion or overall perspective if relevant.
+ - For simpler queries like weather, calculations, or definitions, provide concise answers and not a long article.
+
+
+ ${context}
+
+
+ Current date & time in ISO format (UTC timezone) is: ${new Date().toISOString()}.
+ `;
+};
From ec06a2b9ff0aaa86b5092443a07e753e04292149 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:48:44 +0530
Subject: [PATCH 049/199] feat(researcher): use patching, streaming
---
src/lib/agents/search/researcher/index.ts | 170 ++++++++++++++++++----
1 file changed, 144 insertions(+), 26 deletions(-)
diff --git a/src/lib/agents/search/researcher/index.ts b/src/lib/agents/search/researcher/index.ts
index 300de72..3eb9478 100644
--- a/src/lib/agents/search/researcher/index.ts
+++ b/src/lib/agents/search/researcher/index.ts
@@ -8,6 +8,8 @@ import {
import { ActionRegistry } from './actions';
import { getResearcherPrompt } from '@/lib/prompts/search/researcher';
import SessionManager from '@/lib/session';
+import { ReasoningResearchBlock } from '@/lib/types';
+import formatChatHistoryAsString from '@/lib/utils/formatHistory';
class Researcher {
async research(
@@ -17,7 +19,7 @@ class Researcher {
let findings: string = '';
let actionOutput: ActionOutput[] = [];
let maxIteration =
- input.config.mode === 'fast'
+ input.config.mode === 'speed'
? 1
: input.config.mode === 'balanced'
? 3
@@ -41,45 +43,130 @@ class Researcher {
classification: input.classification,
});
- for (let i = 0; i < maxIteration; i++) {
- const researcherPrompt = getResearcherPrompt(availableActionsDescription);
+ const researchBlockId = crypto.randomUUID();
- const res = await input.config.llm.generateObject>(
- {
- messages: [
- {
- role: 'system',
- content: researcherPrompt,
- },
- {
- role: 'user',
- content: `
-
- ${input.classification.standaloneFollowUp}
-
+ session.emitBlock({
+ id: researchBlockId,
+ type: 'research',
+ data: {
+ subSteps: [],
+ },
+ });
+
+ for (let i = 0; i < maxIteration; i++) {
+ const researcherPrompt = getResearcherPrompt(
+ availableActionsDescription,
+ input.config.mode,
+ i,
+ maxIteration,
+ );
+
+ const actionStream = input.config.llm.streamObject<
+ z.infer
+ >({
+ messages: [
+ {
+ role: 'system',
+ content: researcherPrompt,
+ },
+ {
+ role: 'user',
+ content: `
+
+ ${formatChatHistoryAsString(input.chatHistory.slice(-10))}
+ User: ${input.followUp} (Standalone question: ${input.classification.standaloneFollowUp})
+
${findings}
`,
- },
- ],
- schema,
- },
- );
+ },
+ ],
+ schema,
+ });
+ const block = session.getBlock(researchBlockId);
- if (res.action.type === 'done') {
- console.log('Research complete - "done" action selected');
+ let reasoningEmitted = false;
+ let reasoningId = crypto.randomUUID();
+
+ let finalActionRes: any;
+
+ for await (const partialRes of actionStream) {
+ try {
+ if (
+ partialRes.reasoning &&
+ !reasoningEmitted &&
+ block &&
+ block.type === 'research'
+ ) {
+ reasoningEmitted = true;
+ block.data.subSteps.push({
+ id: reasoningId,
+ type: 'reasoning',
+ reasoning: partialRes.reasoning,
+ });
+ session.updateBlock(researchBlockId, [
+ {
+ op: 'replace',
+ path: '/data/subSteps',
+ value: block.data.subSteps,
+ },
+ ]);
+ } else if (
+ partialRes.reasoning &&
+ reasoningEmitted &&
+ block &&
+ block.type === 'research'
+ ) {
+ const subStepIndex = block.data.subSteps.findIndex(
+ (step: any) => step.id === reasoningId,
+ );
+ if (subStepIndex !== -1) {
+ const subStep = block.data.subSteps[
+ subStepIndex
+ ] as ReasoningResearchBlock;
+ subStep.reasoning = partialRes.reasoning;
+ session.updateBlock(researchBlockId, [
+ {
+ op: 'replace',
+ path: '/data/subSteps',
+ value: block.data.subSteps,
+ },
+ ]);
+ }
+ }
+
+ finalActionRes = partialRes;
+ } catch (e) {
+ // nothing
+ }
+ }
+
+ if (finalActionRes.action.type === 'done') {
break;
}
const actionConfig: ActionConfig = {
- type: res.action.type as string,
- params: res.action,
+ type: finalActionRes.action.type as string,
+ params: finalActionRes.action,
};
- findings += 'Reasoning: ' + res.reasoning + '\n';
+ const queries = actionConfig.params.queries || [];
+ if (block && block.type === 'research') {
+ block.data.subSteps.push({
+ id: crypto.randomUUID(),
+ type: 'searching',
+ searching: queries,
+ });
+ session.updateBlock(researchBlockId, [
+ { op: 'replace', path: '/data/subSteps', value: block.data.subSteps },
+ ]);
+ }
+
+ findings += `\n---\nIteration ${i + 1}:\n`;
+ findings += 'Reasoning: ' + finalActionRes.reasoning + '\n';
findings += `Executing Action: ${actionConfig.type} with params ${JSON.stringify(actionConfig.params)}\n`;
const actionResult = await ActionRegistry.execute(
@@ -95,6 +182,21 @@ class Researcher {
actionOutput.push(actionResult);
if (actionResult.type === 'search_results') {
+ if (block && block.type === 'research') {
+ block.data.subSteps.push({
+ id: crypto.randomUUID(),
+ type: 'reading',
+ reading: actionResult.results,
+ });
+ session.updateBlock(researchBlockId, [
+ {
+ op: 'replace',
+ path: '/data/subSteps',
+ value: block.data.subSteps,
+ },
+ ]);
+ }
+
findings += actionResult.results
.map(
(r) =>
@@ -102,8 +204,24 @@ class Researcher {
)
.join('\n');
}
+
+ findings += '\n---------\n';
}
+ const searchResults = actionOutput.filter(
+ (a) => a.type === 'search_results',
+ );
+
+ session.emit('data', {
+ type: 'sources',
+ data: searchResults
+ .flatMap((a) => a.results)
+ .map((r) => ({
+ content: r.content,
+ metadata: r.metadata,
+ })),
+ });
+
return {
findings: actionOutput,
};
From cba3f43b19bbda5ecb6b996b06556bc5d49713ac Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:49:36 +0530
Subject: [PATCH 050/199] feat(search-agent): add search agent flow
---
src/lib/agents/search/index.ts | 67 +++++++++++++++++--
.../search/researcher/actions/webSearch.ts | 5 +-
src/lib/agents/search/types.ts | 2 +-
3 files changed, 66 insertions(+), 8 deletions(-)
diff --git a/src/lib/agents/search/index.ts b/src/lib/agents/search/index.ts
index 141ea7f..bacdb88 100644
--- a/src/lib/agents/search/index.ts
+++ b/src/lib/agents/search/index.ts
@@ -3,6 +3,8 @@ import SessionManager from '@/lib/session';
import Classifier from './classifier';
import { WidgetRegistry } from './widgets';
import Researcher from './researcher';
+import { getWriterPrompt } from '@/lib/prompts/search/writer';
+import fs from 'fs';
class SearchAgent {
async searchAsync(session: SessionManager, input: SearchAgentInput) {
@@ -15,15 +17,22 @@ class SearchAgent {
llm: input.config.llm,
});
- session.emit('data', {
- type: 'classification',
- classification: classification,
- });
-
const widgetPromise = WidgetRegistry.executeAll(classification.widgets, {
llm: input.config.llm,
embedding: input.config.embedding,
session: session,
+ }).then((widgetOutputs) => {
+ widgetOutputs.forEach((o) => {
+ session.emitBlock({
+ id: crypto.randomUUID(),
+ type: 'widget',
+ data: {
+ widgetType: o.type,
+ params: o.data,
+ },
+ });
+ });
+ return widgetOutputs;
});
let searchPromise: Promise | null = null;
@@ -42,6 +51,54 @@ class SearchAgent {
widgetPromise,
searchPromise,
]);
+
+ session.emit('data', {
+ type: 'researchComplete',
+ });
+
+ const finalContext =
+ searchResults?.findings
+ .filter((f) => f.type === 'search_results')
+ .flatMap((f) => f.results)
+ .map((f) => `${f.metadata.title}: ${f.content}`)
+ .join('\n') || '';
+
+ const widgetContext = widgetOutputs
+ .map((o) => {
+ return `${o.type}: ${JSON.stringify(o.data)}`;
+ })
+ .join('\n-------------\n');
+
+ const finalContextWithWidgets = `${finalContext} \n${widgetContext} `;
+
+ const writerPrompt = getWriterPrompt(finalContextWithWidgets);
+
+ const answerStream = input.config.llm.streamText({
+ messages: [
+ {
+ role: 'system',
+ content: writerPrompt,
+ },
+ ...input.chatHistory,
+ {
+ role: 'user',
+ content: input.followUp,
+ },
+ ],
+ });
+
+ let accumulatedText = '';
+
+ for await (const chunk of answerStream) {
+ accumulatedText += chunk.contentChunk;
+
+ session.emit('data', {
+ type: 'response',
+ data: chunk.contentChunk,
+ });
+ }
+
+ session.emit('end', {});
}
}
diff --git a/src/lib/agents/search/researcher/actions/webSearch.ts b/src/lib/agents/search/researcher/actions/webSearch.ts
index 5ceb2ed..e5ffdd3 100644
--- a/src/lib/agents/search/researcher/actions/webSearch.ts
+++ b/src/lib/agents/search/researcher/actions/webSearch.ts
@@ -15,9 +15,9 @@ You have to use this action aggressively to find relevant information from the w
When this action is present, you must use it to obtain current information from the web.
### How to use:
-1. For fast search mode, you can use this action once. Make sure to cover all aspects of the user's query in that single search.
+1. For speed search mode, you can use this action once. Make sure to cover all aspects of the user's query in that single search.
2. If you're on quality mode, you'll get to use this action up to two times. Use the first search to gather general information, and the second search to fill in any gaps or get more specific details based on the initial findings.
-3. If you're set on Deep research mode, then you will get to use this action multiple times to gather more information. Use your judgment to decide when additional searches are necessary to provide a thorough and accurate response.
+3. If you're set on quality mode, then you will get to use this action multiple times to gather more information. Use your judgment to decide when additional searches are necessary to provide a thorough and accurate response.
Input: An array of search queries. Make sure the queries are relevant to the user's request and cover different aspects if necessary. You can include a maximum of 3 queries. Make sure the queries are SEO friendly and not sentences rather keywords which can be used to search a search engine like Google, Bing, etc.
`;
@@ -32,6 +32,7 @@ const webSearchAction: ResearchAction = {
const search = async (q: string) => {
const res = await searchSearxng(q);
+
res.results.forEach((r) => {
results.push({
content: r.content || r.title,
diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts
index 0914503..421ee7f 100644
--- a/src/lib/agents/search/types.ts
+++ b/src/lib/agents/search/types.ts
@@ -10,7 +10,7 @@ export type SearchAgentConfig = {
sources: SearchSources[];
llm: BaseLLM;
embedding: BaseEmbedding;
- mode: 'fast' | 'balanced' | 'deep_research';
+ mode: 'speed' | 'balanced' | 'quality';
};
export type SearchAgentInput = {
From e0ba476ca442520314e1ed4ff38329890758027a Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:49:54 +0530
Subject: [PATCH 051/199] feat(optimization): enable quality
---
src/components/MessageInputActions/Optimization.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/components/MessageInputActions/Optimization.tsx b/src/components/MessageInputActions/Optimization.tsx
index fe04190..1cdded8 100644
--- a/src/components/MessageInputActions/Optimization.tsx
+++ b/src/components/MessageInputActions/Optimization.tsx
@@ -24,7 +24,7 @@ const OptimizationModes = [
},
{
key: 'quality',
- title: 'Quality (Soon)',
+ title: 'Quality',
description: 'Get the most thorough and accurate answer',
icon: (
{
setOptimizationMode(mode.key)}
key={i}
- disabled={mode.key === 'quality'}
className={cn(
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition focus:outline-none',
optimizationMode === mode.key
? 'bg-light-secondary dark:bg-dark-secondary'
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
- mode.key === 'quality' && 'opacity-50 cursor-not-allowed',
)}
>
From 956a768a86daca0600c25bf6bae3a2de67d5b08c Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Sun, 23 Nov 2025 19:58:46 +0530
Subject: [PATCH 052/199] feat(app): handle new architecture
---
package.json | 3 +
src/app/api/chat/route.ts | 315 ++++++----------
src/components/AssistantSteps.tsx | 197 ++++++++++
src/components/ChatWindow.tsx | 31 +-
src/components/MessageActions/Copy.tsx | 9 +-
src/components/MessageBox.tsx | 94 +++--
src/components/MessageSources.tsx | 4 +-
src/components/Navbar.tsx | 133 +++----
src/lib/actions.ts | 12 +-
src/lib/agents/search/classifier/index.ts | 5 +-
src/lib/hooks/useChat.tsx | 419 ++++++++++++----------
src/lib/models/base/provider.ts | 2 -
src/lib/models/providers/openai/index.ts | 2 -
yarn.lock | 227 +++++++++++-
14 files changed, 945 insertions(+), 508 deletions(-)
create mode 100644 src/components/AssistantSteps.tsx
diff --git a/package.json b/package.json
index 5752b9f..11a9ce4 100644
--- a/package.json
+++ b/package.json
@@ -35,9 +35,11 @@
"html-to-text": "^9.0.5",
"jspdf": "^3.0.1",
"langchain": "^1.0.4",
+ "lightweight-charts": "^5.0.9",
"lucide-react": "^0.363.0",
"mammoth": "^1.9.1",
"markdown-to-jsx": "^7.7.2",
+ "mathjs": "^15.1.0",
"next": "^15.2.2",
"next-themes": "^0.3.0",
"ollama": "^0.6.3",
@@ -52,6 +54,7 @@
"sonner": "^1.4.41",
"tailwind-merge": "^2.2.2",
"winston": "^3.17.0",
+ "yahoo-finance2": "^3.10.2",
"yet-another-react-lightbox": "^3.17.2",
"zod": "^4.1.12"
},
diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts
index 25b8104..2ea0bf5 100644
--- a/src/app/api/chat/route.ts
+++ b/src/app/api/chat/route.ts
@@ -1,14 +1,10 @@
import crypto from 'crypto';
-import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
-import { EventEmitter } from 'stream';
-import db from '@/lib/db';
-import { chats, messages as messagesSchema } from '@/lib/db/schema';
-import { and, eq, gt } from 'drizzle-orm';
-import { getFileDetails } from '@/lib/utils/files';
-import { searchHandlers } from '@/lib/search';
import { z } from 'zod';
import ModelRegistry from '@/lib/models/registry';
import { ModelWithProvider } from '@/lib/models/types';
+import SearchAgent from '@/lib/agents/search';
+import SessionManager from '@/lib/session';
+import { ChatTurnMessage } from '@/lib/types';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
@@ -20,47 +16,25 @@ const messageSchema = z.object({
});
const chatModelSchema: z.ZodType
= z.object({
- providerId: z.string({
- errorMap: () => ({
- message: 'Chat model provider id must be provided',
- }),
- }),
- key: z.string({
- errorMap: () => ({
- message: 'Chat model key must be provided',
- }),
- }),
+ providerId: z.string({ message: 'Chat model provider id must be provided' }),
+ key: z.string({ message: 'Chat model key must be provided' }),
});
const embeddingModelSchema: z.ZodType = z.object({
providerId: z.string({
- errorMap: () => ({
- message: 'Embedding model provider id must be provided',
- }),
- }),
- key: z.string({
- errorMap: () => ({
- message: 'Embedding model key must be provided',
- }),
+ message: 'Embedding model provider id must be provided',
}),
+ key: z.string({ message: 'Embedding model key must be provided' }),
});
const bodySchema = z.object({
message: messageSchema,
optimizationMode: z.enum(['speed', 'balanced', 'quality'], {
- errorMap: () => ({
- message: 'Optimization mode must be one of: speed, balanced, quality',
- }),
+ message: 'Optimization mode must be one of: speed, balanced, quality',
}),
focusMode: z.string().min(1, 'Focus mode is required'),
history: z
- .array(
- z.tuple([z.string(), z.string()], {
- errorMap: () => ({
- message: 'History items must be tuples of two strings',
- }),
- }),
- )
+ .array(z.tuple([z.string(), z.string()]))
.optional()
.default([]),
files: z.array(z.string()).optional().default([]),
@@ -78,7 +52,7 @@ const safeValidateBody = (data: unknown) => {
if (!result.success) {
return {
success: false,
- error: result.error.errors.map((e) => ({
+ error: result.error.issues.map((e: any) => ({
path: e.path.join('.'),
message: e.message,
})),
@@ -91,151 +65,12 @@ const safeValidateBody = (data: unknown) => {
};
};
-const handleEmitterEvents = async (
- stream: EventEmitter,
- writer: WritableStreamDefaultWriter,
- encoder: TextEncoder,
- chatId: string,
-) => {
- let receivedMessage = '';
- const aiMessageId = crypto.randomBytes(7).toString('hex');
-
- stream.on('data', (data) => {
- const parsedData = JSON.parse(data);
- if (parsedData.type === 'response') {
- writer.write(
- encoder.encode(
- JSON.stringify({
- type: 'message',
- data: parsedData.data,
- messageId: aiMessageId,
- }) + '\n',
- ),
- );
-
- receivedMessage += parsedData.data;
- } else if (parsedData.type === 'sources') {
- writer.write(
- encoder.encode(
- JSON.stringify({
- type: 'sources',
- data: parsedData.data,
- messageId: aiMessageId,
- }) + '\n',
- ),
- );
-
- const sourceMessageId = crypto.randomBytes(7).toString('hex');
-
- db.insert(messagesSchema)
- .values({
- chatId: chatId,
- messageId: sourceMessageId,
- role: 'source',
- sources: parsedData.data,
- createdAt: new Date().toString(),
- })
- .execute();
- }
- });
- stream.on('end', () => {
- writer.write(
- encoder.encode(
- JSON.stringify({
- type: 'messageEnd',
- }) + '\n',
- ),
- );
- writer.close();
-
- db.insert(messagesSchema)
- .values({
- content: receivedMessage,
- chatId: chatId,
- messageId: aiMessageId,
- role: 'assistant',
- createdAt: new Date().toString(),
- })
- .execute();
- });
- stream.on('error', (data) => {
- const parsedData = JSON.parse(data);
- writer.write(
- encoder.encode(
- JSON.stringify({
- type: 'error',
- data: parsedData.data,
- }),
- ),
- );
- writer.close();
- });
-};
-
-const handleHistorySave = async (
- message: Message,
- humanMessageId: string,
- focusMode: string,
- files: string[],
-) => {
- const chat = await db.query.chats.findFirst({
- where: eq(chats.id, message.chatId),
- });
-
- const fileData = files.map(getFileDetails);
-
- if (!chat) {
- await db
- .insert(chats)
- .values({
- id: message.chatId,
- title: message.content,
- createdAt: new Date().toString(),
- focusMode: focusMode,
- files: fileData,
- })
- .execute();
- } else if (JSON.stringify(chat.files ?? []) != JSON.stringify(fileData)) {
- db.update(chats)
- .set({
- files: files.map(getFileDetails),
- })
- .where(eq(chats.id, message.chatId));
- }
-
- const messageExists = await db.query.messages.findFirst({
- where: eq(messagesSchema.messageId, humanMessageId),
- });
-
- if (!messageExists) {
- await db
- .insert(messagesSchema)
- .values({
- content: message.content,
- chatId: message.chatId,
- messageId: humanMessageId,
- role: 'user',
- createdAt: new Date().toString(),
- })
- .execute();
- } else {
- await db
- .delete(messagesSchema)
- .where(
- and(
- gt(messagesSchema.id, messageExists.id),
- eq(messagesSchema.chatId, message.chatId),
- ),
- )
- .execute();
- }
-};
-
export const POST = async (req: Request) => {
try {
const reqBody = (await req.json()) as Body;
const parseBody = safeValidateBody(reqBody);
+
if (!parseBody.success) {
return Response.json(
{ message: 'Invalid request body', error: parseBody.error },
@@ -265,48 +100,116 @@ export const POST = async (req: Request) => {
),
]);
- const humanMessageId =
- message.messageId ?? crypto.randomBytes(7).toString('hex');
-
- const history: BaseMessage[] = body.history.map((msg) => {
+ const history: ChatTurnMessage[] = body.history.map((msg) => {
if (msg[0] === 'human') {
- return new HumanMessage({
+ return {
+ role: 'user',
content: msg[1],
- });
+ };
} else {
- return new AIMessage({
+ return {
+ role: 'assistant',
content: msg[1],
- });
+ };
}
});
- const handler = searchHandlers[body.focusMode];
-
- if (!handler) {
- return Response.json(
- {
- message: 'Invalid focus mode',
- },
- { status: 400 },
- );
- }
-
- const stream = await handler.searchAndAnswer(
- message.content,
- history,
- llm,
- embedding,
- body.optimizationMode,
- body.files,
- body.systemInstructions as string,
- );
+ const agent = new SearchAgent();
+ const session = SessionManager.createSession();
const responseStream = new TransformStream();
const writer = responseStream.writable.getWriter();
const encoder = new TextEncoder();
- handleEmitterEvents(stream, writer, encoder, message.chatId);
- handleHistorySave(message, humanMessageId, body.focusMode, body.files);
+ let receivedMessage = '';
+
+ session.addListener('data', (data: any) => {
+ if (data.type === 'response') {
+ writer.write(
+ encoder.encode(
+ JSON.stringify({
+ type: 'message',
+ data: data.data,
+ }) + '\n',
+ ),
+ );
+ receivedMessage += data.data;
+ } else if (data.type === 'sources') {
+ writer.write(
+ encoder.encode(
+ JSON.stringify({
+ type: 'sources',
+ data: data.data,
+ }) + '\n',
+ ),
+ );
+ } else if (data.type === 'block') {
+ writer.write(
+ encoder.encode(
+ JSON.stringify({
+ type: 'block',
+ block: data.block,
+ }) + '\n',
+ ),
+ );
+ } else if (data.type === 'updateBlock') {
+ writer.write(
+ encoder.encode(
+ JSON.stringify({
+ type: 'updateBlock',
+ blockId: data.blockId,
+ patch: data.patch,
+ }) + '\n',
+ ),
+ );
+ } else if (data.type === 'researchComplete') {
+ writer.write(
+ encoder.encode(
+ JSON.stringify({
+ type: 'researchComplete',
+ }) + '\n',
+ ),
+ );
+ }
+ });
+
+ session.addListener('end', () => {
+ writer.write(
+ encoder.encode(
+ JSON.stringify({
+ type: 'messageEnd',
+ }) + '\n',
+ ),
+ );
+ writer.close();
+ session.removeAllListeners();
+ });
+
+ session.addListener('error', (data: any) => {
+ writer.write(
+ encoder.encode(
+ JSON.stringify({
+ type: 'error',
+ data: data.data,
+ }) + '\n',
+ ),
+ );
+ writer.close();
+ session.removeAllListeners();
+ });
+
+ agent.searchAsync(session, {
+ chatHistory: history,
+ followUp: message.content,
+ config: {
+ llm,
+ embedding: embedding,
+ sources: ['web'],
+ mode: body.optimizationMode,
+ },
+ });
+
+ /* handleHistorySave(message, humanMessageId, body.focusMode, body.files); */
return new Response(responseStream.readable, {
headers: {
diff --git a/src/components/AssistantSteps.tsx b/src/components/AssistantSteps.tsx
new file mode 100644
index 0000000..c688880
--- /dev/null
+++ b/src/components/AssistantSteps.tsx
@@ -0,0 +1,197 @@
+'use client';
+
+import { Brain, Search, FileText, ChevronDown, ChevronUp } from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { useEffect, useState } from 'react';
+import { ResearchBlock, ResearchBlockSubStep } from '@/lib/types';
+import { useChat } from '@/lib/hooks/useChat';
+
+const getStepIcon = (step: ResearchBlockSubStep) => {
+ if (step.type === 'reasoning') {
+ return ;
+ } else if (step.type === 'searching') {
+ return ;
+ } else if (step.type === 'reading') {
+ return ;
+ }
+ return null;
+};
+
+const getStepTitle = (
+ step: ResearchBlockSubStep,
+ isStreaming: boolean,
+): string => {
+ if (step.type === 'reasoning') {
+ return isStreaming && !step.reasoning ? 'Thinking...' : 'Thinking';
+ } else if (step.type === 'searching') {
+ return `Searching ${step.searching.length} ${step.searching.length === 1 ? 'query' : 'queries'}`;
+ } else if (step.type === 'reading') {
+ return `Found ${step.reading.length} ${step.reading.length === 1 ? 'result' : 'results'}`;
+ }
+ return 'Processing';
+};
+
+const AssistantSteps = ({
+ block,
+ status,
+}: {
+ block: ResearchBlock;
+ status: 'answering' | 'completed' | 'error';
+}) => {
+ const [isExpanded, setIsExpanded] = useState(true);
+ const { researchEnded, loading } = useChat();
+
+ useEffect(() => {
+ if (researchEnded) {
+ setIsExpanded(false);
+ } else if (status === 'answering') {
+ setIsExpanded(true);
+ }
+ }, [researchEnded, status]);
+
+ if (!block || block.data.subSteps.length === 0) return null;
+
+ return (
+
+
setIsExpanded(!isExpanded)}
+ className="w-full flex items-center justify-between p-3 hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200"
+ >
+
+
+
+ Research Progress ({block.data.subSteps.length}{' '}
+ {block.data.subSteps.length === 1 ? 'step' : 'steps'})
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isExpanded && (
+
+
+ {block.data.subSteps.map((step, index) => {
+ const isLastStep = index === block.data.subSteps.length - 1;
+ const isStreaming = loading && isLastStep && !researchEnded;
+
+ return (
+
+ {/* Timeline connector */}
+
+
+ {getStepIcon(step)}
+
+ {index < block.data.subSteps.length - 1 && (
+
+ )}
+
+
+ {/* Step content */}
+
+
+ {getStepTitle(step, isStreaming)}
+
+
+ {step.type === 'reasoning' && (
+ <>
+ {step.reasoning && (
+
+ {step.reasoning}
+
+ )}
+ {isStreaming && !step.reasoning && (
+
+ )}
+ >
+ )}
+
+ {step.type === 'searching' &&
+ step.searching.length > 0 && (
+
+ {step.searching.map((query, idx) => (
+
+ {query}
+
+ ))}
+
+ )}
+
+ {step.type === 'reading' && step.reading.length > 0 && (
+
+ {step.reading.slice(0, 4).map((result, idx) => {
+ const url = result.metadata.url || '';
+ const title = result.metadata.title || 'Untitled';
+ const domain = url ? new URL(url).hostname : '';
+ const faviconUrl = domain
+ ? `https://s2.googleusercontent.com/s2/favicons?domain=${domain}&sz=128`
+ : '';
+
+ return (
+
+ {faviconUrl && (
+ {
+ e.currentTarget.style.display = 'none';
+ }}
+ />
+ )}
+ {title}
+
+ );
+ })}
+
+ )}
+
+
+ );
+ })}
+
+
+ )}
+
+
+ );
+};
+
+export default AssistantSteps;
diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx
index dc6ab01..9489219 100644
--- a/src/components/ChatWindow.tsx
+++ b/src/components/ChatWindow.tsx
@@ -1,14 +1,12 @@
'use client';
-import { Document } from '@langchain/core/documents';
import Navbar from './Navbar';
import Chat from './Chat';
import EmptyChat from './EmptyChat';
-import { Settings } from 'lucide-react';
-import Link from 'next/link';
import NextError from 'next/error';
import { useChat } from '@/lib/hooks/useChat';
import SettingsButtonMobile from './Settings/SettingsButtonMobile';
+import { Block, Chunk } from '@/lib/types';
export interface BaseMessage {
chatId: string;
@@ -16,20 +14,27 @@ export interface BaseMessage {
createdAt: Date;
}
+export interface Message extends BaseMessage {
+ backendId: string;
+ query: string;
+ responseBlocks: Block[];
+ status: 'answering' | 'completed' | 'error';
+}
+
+export interface UserMessage extends BaseMessage {
+ role: 'user';
+ content: string;
+}
+
export interface AssistantMessage extends BaseMessage {
role: 'assistant';
content: string;
suggestions?: string[];
}
-export interface UserMessage extends BaseMessage {
- role: 'user';
- content: string;
-}
-
export interface SourceMessage extends BaseMessage {
role: 'source';
- sources: Document[];
+ sources: Chunk[];
}
export interface SuggestionMessage extends BaseMessage {
@@ -37,11 +42,12 @@ export interface SuggestionMessage extends BaseMessage {
suggestions: string[];
}
-export type Message =
+export type LegacyMessage =
| AssistantMessage
| UserMessage
| SourceMessage
| SuggestionMessage;
+
export type ChatTurn = UserMessage | AssistantMessage;
export interface File {
@@ -50,6 +56,11 @@ export interface File {
fileId: string;
}
+export interface Widget {
+ widgetType: string;
+ params: Record;
+}
+
const ChatWindow = () => {
const { hasError, notFound, messages } = useChat();
if (hasError) {
diff --git a/src/components/MessageActions/Copy.tsx b/src/components/MessageActions/Copy.tsx
index f74b9f3..38ed71a 100644
--- a/src/components/MessageActions/Copy.tsx
+++ b/src/components/MessageActions/Copy.tsx
@@ -15,7 +15,14 @@ const Copy = ({
return (
{
- const contentToCopy = `${initialMessage}${section?.sourceMessage?.sources && section.sourceMessage.sources.length > 0 && `\n\nCitations:\n${section.sourceMessage.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`;
+ const contentToCopy = `${initialMessage}${
+ section?.message.responseBlocks.filter((b) => b.type === 'source')
+ ?.length > 0 &&
+ `\n\nCitations:\n${section.message.responseBlocks
+ .filter((b) => b.type === 'source')
+ ?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`)
+ .join(`\n`)}`
+ }`;
navigator.clipboard.writeText(contentToCopy);
setCopied(true);
setTimeout(() => setCopied(false), 1000);
diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx
index 30e181e..4f5766d 100644
--- a/src/components/MessageBox.tsx
+++ b/src/components/MessageBox.tsx
@@ -22,6 +22,9 @@ import { useSpeech } from 'react-text-to-speech';
import ThinkBox from './ThinkBox';
import { useChat, Section } from '@/lib/hooks/useChat';
import Citation from './Citation';
+import AssistantSteps from './AssistantSteps';
+import { ResearchBlock } from '@/lib/types';
+import Renderer from './Widgets/Renderer';
const ThinkTagProcessor = ({
children,
@@ -46,12 +49,21 @@ const MessageBox = ({
dividerRef?: MutableRefObject;
isLast: boolean;
}) => {
- const { loading, chatTurns, sendMessage, rewrite } = useChat();
+ const { loading, sendMessage, rewrite, messages, researchEnded } = useChat();
- const parsedMessage = section.parsedAssistantMessage || '';
+ const parsedMessage = section.parsedTextBlocks.join('\n\n');
const speechMessage = section.speechMessage || '';
const thinkingEnded = section.thinkingEnded;
+ const sourceBlocks = section.message.responseBlocks.filter(
+ (block): block is typeof block & { type: 'source' } =>
+ block.type === 'source',
+ );
+
+ const sources = sourceBlocks.flatMap((block) => block.data);
+
+ const hasContent = section.parsedTextBlocks.length > 0;
+
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
const markdownOverrides: MarkdownToJSX.Options = {
@@ -72,7 +84,7 @@ const MessageBox = ({
- {section.userMessage.content}
+ {section.message.query}
@@ -81,21 +93,50 @@ const MessageBox = ({
ref={dividerRef}
className="flex flex-col space-y-6 w-full lg:w-9/12"
>
- {section.sourceMessage &&
- section.sourceMessage.sources.length > 0 && (
-
-
-
-
- Sources
-
-
-
+ {sources.length > 0 && (
+
+ )}
+
+ {section.message.responseBlocks
+ .filter(
+ (block): block is ResearchBlock =>
+ block.type === 'research' && block.data.subSteps.length > 0,
+ )
+ .map((researchBlock) => (
+
+ ))}
+
+ {section.widgets.length > 0 &&
}
+
+ {isLast &&
+ loading &&
+ !researchEnded &&
+ !section.message.responseBlocks.some(
+ (b) => b.type === 'research' && b.data.subSteps.length > 0,
+ ) && (
+
+
+
+ Brainstorming...
+
)}
- {section.sourceMessage && (
+ {sources.length > 0 && (
)}
- {section.assistantMessage && (
+ {hasContent && (
<>
-
+
{
if (speechStatus === 'started') {
@@ -158,7 +196,7 @@ const MessageBox = ({
{isLast &&
section.suggestions &&
section.suggestions.length > 0 &&
- section.assistantMessage &&
+ hasContent &&
!loading && (
@@ -206,17 +244,17 @@ const MessageBox = ({
- {section.assistantMessage && (
+ {hasContent && (
)}
diff --git a/src/components/MessageSources.tsx b/src/components/MessageSources.tsx
index fb2b5bb..ea5fa79 100644
--- a/src/components/MessageSources.tsx
+++ b/src/components/MessageSources.tsx
@@ -6,11 +6,11 @@ import {
Transition,
TransitionChild,
} from '@headlessui/react';
-import { Document } from '@langchain/core/documents';
import { File } from 'lucide-react';
import { Fragment, useState } from 'react';
+import { Chunk } from '@/lib/types';
-const MessageSources = ({ sources }: { sources: Document[] }) => {
+const MessageSources = ({ sources }: { sources: Chunk[] }) => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const closeModal = () => {
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index bbcd470..ae32685 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -11,6 +11,7 @@ import {
} from '@headlessui/react';
import jsPDF from 'jspdf';
import { useChat, Section } from '@/lib/hooks/useChat';
+import { SourceBlock } from '@/lib/types';
const downloadFile = (filename: string, content: string, type: string) => {
const blob = new Blob([content], { type });
@@ -28,35 +29,41 @@ const downloadFile = (filename: string, content: string, type: string) => {
const exportAsMarkdown = (sections: Section[], title: string) => {
const date = new Date(
- sections[0]?.userMessage?.createdAt || Date.now(),
+ sections[0].message.createdAt || Date.now(),
).toLocaleString();
let md = `# 💬 Chat Export: ${title}\n\n`;
md += `*Exported on: ${date}*\n\n---\n`;
sections.forEach((section, idx) => {
- if (section.userMessage) {
- md += `\n---\n`;
- md += `**🧑 User**
+ md += `\n---\n`;
+ md += `**🧑 User**
`;
- md += `*${new Date(section.userMessage.createdAt).toLocaleString()}*\n\n`;
- md += `> ${section.userMessage.content.replace(/\n/g, '\n> ')}\n`;
- }
+ md += `*${new Date(section.message.createdAt).toLocaleString()}*\n\n`;
+ md += `> ${section.message.query.replace(/\n/g, '\n> ')}\n`;
- if (section.assistantMessage) {
+ if (section.message.responseBlocks.length > 0) {
md += `\n---\n`;
md += `**🤖 Assistant**
`;
- md += `*${new Date(section.assistantMessage.createdAt).toLocaleString()}*\n\n`;
- md += `> ${section.assistantMessage.content.replace(/\n/g, '\n> ')}\n`;
+ md += `*${new Date(section.message.createdAt).toLocaleString()}*\n\n`;
+ md += `> ${section.message.responseBlocks
+ .filter((b) => b.type === 'text')
+ .map((block) => block.data)
+ .join('\n')
+ .replace(/\n/g, '\n> ')}\n`;
}
+ const sourceResponseBlock = section.message.responseBlocks.find(
+ (block) => block.type === 'source',
+ ) as SourceBlock | undefined;
+
if (
- section.sourceMessage &&
- section.sourceMessage.sources &&
- section.sourceMessage.sources.length > 0
+ sourceResponseBlock &&
+ sourceResponseBlock.data &&
+ sourceResponseBlock.data.length > 0
) {
md += `\n**Citations:**\n`;
- section.sourceMessage.sources.forEach((src: any, i: number) => {
+ sourceResponseBlock.data.forEach((src: any, i: number) => {
const url = src.metadata?.url || '';
md += `- [${i + 1}] [${url}](${url})\n`;
});
@@ -69,7 +76,7 @@ const exportAsMarkdown = (sections: Section[], title: string) => {
const exportAsPDF = (sections: Section[], title: string) => {
const doc = new jsPDF();
const date = new Date(
- sections[0]?.userMessage?.createdAt || Date.now(),
+ sections[0]?.message?.createdAt || Date.now(),
).toLocaleString();
let y = 15;
const pageHeight = doc.internal.pageSize.height;
@@ -86,44 +93,38 @@ const exportAsPDF = (sections: Section[], title: string) => {
doc.setTextColor(30);
sections.forEach((section, idx) => {
- if (section.userMessage) {
- if (y > pageHeight - 30) {
- doc.addPage();
- y = 15;
- }
- doc.setFont('helvetica', 'bold');
- doc.text('User', 10, y);
- doc.setFont('helvetica', 'normal');
- doc.setFontSize(10);
- doc.setTextColor(120);
- doc.text(
- `${new Date(section.userMessage.createdAt).toLocaleString()}`,
- 40,
- y,
- );
- y += 6;
- doc.setTextColor(30);
- doc.setFontSize(12);
- const userLines = doc.splitTextToSize(section.userMessage.content, 180);
- for (let i = 0; i < userLines.length; i++) {
- if (y > pageHeight - 20) {
- doc.addPage();
- y = 15;
- }
- doc.text(userLines[i], 12, y);
- y += 6;
- }
- y += 6;
- doc.setDrawColor(230);
- if (y > pageHeight - 10) {
- doc.addPage();
- y = 15;
- }
- doc.line(10, y, 200, y);
- y += 4;
+ if (y > pageHeight - 30) {
+ doc.addPage();
+ y = 15;
}
+ doc.setFont('helvetica', 'bold');
+ doc.text('User', 10, y);
+ doc.setFont('helvetica', 'normal');
+ doc.setFontSize(10);
+ doc.setTextColor(120);
+ doc.text(`${new Date(section.message.createdAt).toLocaleString()}`, 40, y);
+ y += 6;
+ doc.setTextColor(30);
+ doc.setFontSize(12);
+ const userLines = doc.splitTextToSize(section.message.query, 180);
+ for (let i = 0; i < userLines.length; i++) {
+ if (y > pageHeight - 20) {
+ doc.addPage();
+ y = 15;
+ }
+ doc.text(userLines[i], 12, y);
+ y += 6;
+ }
+ y += 6;
+ doc.setDrawColor(230);
+ if (y > pageHeight - 10) {
+ doc.addPage();
+ y = 15;
+ }
+ doc.line(10, y, 200, y);
+ y += 4;
- if (section.assistantMessage) {
+ if (section.message.responseBlocks.length > 0) {
if (y > pageHeight - 30) {
doc.addPage();
y = 15;
@@ -134,7 +135,7 @@ const exportAsPDF = (sections: Section[], title: string) => {
doc.setFontSize(10);
doc.setTextColor(120);
doc.text(
- `${new Date(section.assistantMessage.createdAt).toLocaleString()}`,
+ `${new Date(section.message.createdAt).toLocaleString()}`,
40,
y,
);
@@ -142,7 +143,7 @@ const exportAsPDF = (sections: Section[], title: string) => {
doc.setTextColor(30);
doc.setFontSize(12);
const assistantLines = doc.splitTextToSize(
- section.assistantMessage.content,
+ section.parsedTextBlocks.join('\n'),
180,
);
for (let i = 0; i < assistantLines.length; i++) {
@@ -154,10 +155,14 @@ const exportAsPDF = (sections: Section[], title: string) => {
y += 6;
}
+ const sourceResponseBlock = section.message.responseBlocks.find(
+ (block) => block.type === 'source',
+ ) as SourceBlock | undefined;
+
if (
- section.sourceMessage &&
- section.sourceMessage.sources &&
- section.sourceMessage.sources.length > 0
+ sourceResponseBlock &&
+ sourceResponseBlock.data &&
+ sourceResponseBlock.data.length > 0
) {
doc.setFontSize(11);
doc.setTextColor(80);
@@ -167,7 +172,7 @@ const exportAsPDF = (sections: Section[], title: string) => {
}
doc.text('Citations:', 12, y);
y += 5;
- section.sourceMessage.sources.forEach((src: any, i: number) => {
+ sourceResponseBlock.data.forEach((src: any, i: number) => {
const url = src.metadata?.url || '';
if (y > pageHeight - 15) {
doc.addPage();
@@ -198,15 +203,15 @@ const Navbar = () => {
const { sections, chatId } = useChat();
useEffect(() => {
- if (sections.length > 0 && sections[0].userMessage) {
+ if (sections.length > 0 && sections[0].message) {
const newTitle =
- sections[0].userMessage.content.length > 20
- ? `${sections[0].userMessage.content.substring(0, 20).trim()}...`
- : sections[0].userMessage.content;
+ sections[0].message.query.substring(0, 30) + '...' ||
+ 'New Conversation';
+
setTitle(newTitle);
const newTimeAgo = formatTimeDifference(
new Date(),
- sections[0].userMessage.createdAt,
+ sections[0].message.createdAt,
);
setTimeAgo(newTimeAgo);
}
@@ -214,10 +219,10 @@ const Navbar = () => {
useEffect(() => {
const intervalId = setInterval(() => {
- if (sections.length > 0 && sections[0].userMessage) {
+ if (sections.length > 0 && sections[0].message) {
const newTimeAgo = formatTimeDifference(
new Date(),
- sections[0].userMessage.createdAt,
+ sections[0].message.createdAt,
);
setTimeAgo(newTimeAgo);
}
diff --git a/src/lib/actions.ts b/src/lib/actions.ts
index cb75d88..f9d04b2 100644
--- a/src/lib/actions.ts
+++ b/src/lib/actions.ts
@@ -1,6 +1,14 @@
import { Message } from '@/components/ChatWindow';
-export const getSuggestions = async (chatHistory: Message[]) => {
+export const getSuggestions = async (chatHistory: [string, string][]) => {
+ const chatTurns = chatHistory.map(([role, content]) => {
+ if (role === 'human') {
+ return { role: 'user', content };
+ } else {
+ return { role: 'assistant', content };
+ }
+ });
+
const chatModel = localStorage.getItem('chatModelKey');
const chatModelProvider = localStorage.getItem('chatModelProviderId');
@@ -10,7 +18,7 @@ export const getSuggestions = async (chatHistory: Message[]) => {
'Content-Type': 'application/json',
},
body: JSON.stringify({
- chatHistory: chatHistory,
+ chatHistory: chatTurns,
chatModel: {
providerId: chatModelProvider,
key: chatModel,
diff --git a/src/lib/agents/search/classifier/index.ts b/src/lib/agents/search/classifier/index.ts
index 9b2bf6b..6b768a6 100644
--- a/src/lib/agents/search/classifier/index.ts
+++ b/src/lib/agents/search/classifier/index.ts
@@ -10,6 +10,7 @@ class Classifier {
const availableIntents = IntentRegistry.getAvailableIntents({
sources: input.enabledSources,
});
+
const availableWidgets = WidgetRegistry.getAll();
const classificationSchema = z.object({
@@ -21,12 +22,12 @@ class Classifier {
standaloneFollowUp: z
.string()
.describe(
- 'A self-contained, context-independent reformulation of the user\'s question. Must include all necessary context from chat history, replace pronouns with specific nouns, and be clear enough to answer without seeing the conversation. Keep the same complexity as the original question.',
+ "A self-contained, context-independent reformulation of the user's question. Must include all necessary context from chat history, replace pronouns with specific nouns, and be clear enough to answer without seeing the conversation. Keep the same complexity as the original question.",
),
intents: z
.array(z.enum(availableIntents.map((i) => i.name)))
.describe(
- 'The intent(s) that best describe how to fulfill the user\'s query. Can include multiple intents (e.g., [\'web_search\', \'widget_response\'] for \'weather in NYC and recent news\'). Always include at least one intent when applicable.',
+ "The intent(s) that best describe how to fulfill the user's query. Can include multiple intents (e.g., ['web_search', 'widget_response'] for 'weather in NYC and recent news'). Always include at least one intent when applicable.",
),
widgets: z
.array(z.union(availableWidgets.map((w) => w.schema)))
diff --git a/src/lib/hooks/useChat.tsx b/src/lib/hooks/useChat.tsx
index ee7e9c7..34c7095 100644
--- a/src/lib/hooks/useChat.tsx
+++ b/src/lib/hooks/useChat.tsx
@@ -1,13 +1,7 @@
'use client';
-import {
- AssistantMessage,
- ChatTurn,
- Message,
- SourceMessage,
- SuggestionMessage,
- UserMessage,
-} from '@/components/ChatWindow';
+import { Message } from '@/components/ChatWindow';
+import { Block } from '@/lib/types';
import {
createContext,
useContext,
@@ -22,20 +16,20 @@ import { toast } from 'sonner';
import { getSuggestions } from '../actions';
import { MinimalProvider } from '../models/types';
import { getAutoMediaSearch } from '../config/clientRegistry';
+import { applyPatch } from 'rfc6902';
+import { Widget } from '@/components/ChatWindow';
export type Section = {
- userMessage: UserMessage;
- assistantMessage: AssistantMessage | undefined;
- parsedAssistantMessage: string | undefined;
- speechMessage: string | undefined;
- sourceMessage: SourceMessage | undefined;
+ message: Message;
+ widgets: Widget[];
+ parsedTextBlocks: string[];
+ speechMessage: string;
thinkingEnded: boolean;
suggestions?: string[];
};
type ChatContext = {
messages: Message[];
- chatTurns: ChatTurn[];
sections: Section[];
chatHistory: [string, string][];
files: File[];
@@ -51,6 +45,8 @@ type ChatContext = {
hasError: boolean;
chatModelProvider: ChatModelProvider;
embeddingModelProvider: EmbeddingModelProvider;
+ researchEnded: boolean;
+ setResearchEnded: (ended: boolean) => void;
setOptimizationMode: (mode: string) => void;
setFocusMode: (mode: string) => void;
setFiles: (files: File[]) => void;
@@ -204,18 +200,26 @@ const loadMessages = async (
setMessages(messages);
- const chatTurns = messages.filter(
- (msg): msg is ChatTurn => msg.role === 'user' || msg.role === 'assistant',
- );
+ const history: [string, string][] = [];
+ messages.forEach((msg) => {
+ history.push(['human', msg.query]);
- const history = chatTurns.map((msg) => {
- return [msg.role, msg.content];
- }) as [string, string][];
+ const textBlocks = msg.responseBlocks
+ .filter(
+ (block): block is Block & { type: 'text' } => block.type === 'text',
+ )
+ .map((block) => block.data)
+ .join('\n');
+
+ if (textBlocks) {
+ history.push(['assistant', textBlocks]);
+ }
+ });
console.debug(new Date(), 'app:messages_loaded');
- if (chatTurns.length > 0) {
- document.title = chatTurns[0].content;
+ if (messages.length > 0) {
+ document.title = messages[0].query;
}
const files = data.chat.files.map((file: any) => {
@@ -246,12 +250,12 @@ export const chatContext = createContext({
loading: false,
messageAppeared: false,
messages: [],
- chatTurns: [],
sections: [],
notFound: false,
optimizationMode: '',
chatModelProvider: { key: '', providerId: '' },
embeddingModelProvider: { key: '', providerId: '' },
+ researchEnded: false,
rewrite: () => {},
sendMessage: async () => {},
setFileIds: () => {},
@@ -260,6 +264,7 @@ export const chatContext = createContext({
setOptimizationMode: () => {},
setChatModelProvider: () => {},
setEmbeddingModelProvider: () => {},
+ setResearchEnded: () => {},
});
export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
@@ -273,6 +278,8 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
const [loading, setLoading] = useState(false);
const [messageAppeared, setMessageAppeared] = useState(false);
+ const [researchEnded, setResearchEnded] = useState(false);
+
const [chatHistory, setChatHistory] = useState<[string, string][]>([]);
const [messages, setMessages] = useState([]);
@@ -305,66 +312,44 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
const messagesRef = useRef([]);
- const chatTurns = useMemo((): ChatTurn[] => {
- return messages.filter(
- (msg): msg is ChatTurn => msg.role === 'user' || msg.role === 'assistant',
- );
- }, [messages]);
-
const sections = useMemo(() => {
- const sections: Section[] = [];
+ return messages.map((msg) => {
+ const textBlocks: string[] = [];
+ let speechMessage = '';
+ let thinkingEnded = false;
+ let suggestions: string[] = [];
- messages.forEach((msg, i) => {
- if (msg.role === 'user') {
- const nextUserMessageIndex = messages.findIndex(
- (m, j) => j > i && m.role === 'user',
- );
+ const sourceBlocks = msg.responseBlocks.filter(
+ (block): block is Block & { type: 'source' } => block.type === 'source',
+ );
+ const sources = sourceBlocks.flatMap((block) => block.data);
- const aiMessage = messages.find(
- (m, j) =>
- j > i &&
- m.role === 'assistant' &&
- (nextUserMessageIndex === -1 || j < nextUserMessageIndex),
- ) as AssistantMessage | undefined;
+ const widgetBlocks = msg.responseBlocks
+ .filter((b) => b.type === 'widget')
+ .map((b) => b.data) as Widget[];
- const sourceMessage = messages.find(
- (m, j) =>
- j > i &&
- m.role === 'source' &&
- m.sources &&
- (nextUserMessageIndex === -1 || j < nextUserMessageIndex),
- ) as SourceMessage | undefined;
-
- let thinkingEnded = false;
- let processedMessage = aiMessage?.content ?? '';
- let speechMessage = aiMessage?.content ?? '';
- let suggestions: string[] = [];
-
- if (aiMessage) {
+ msg.responseBlocks.forEach((block) => {
+ if (block.type === 'text') {
+ let processedText = block.data;
const citationRegex = /\[([^\]]+)\]/g;
const regex = /\[(\d+)\]/g;
- if (processedMessage.includes('')) {
- const openThinkTag =
- processedMessage.match(//g)?.length || 0;
+ if (processedText.includes('')) {
+ const openThinkTag = processedText.match(//g)?.length || 0;
const closeThinkTag =
- processedMessage.match(/<\/think>/g)?.length || 0;
+ processedText.match(/<\/think>/g)?.length || 0;
if (openThinkTag && !closeThinkTag) {
- processedMessage += ' ';
+ processedText += ' ';
}
}
- if (aiMessage.content.includes(' ')) {
+ if (block.data.includes(' ')) {
thinkingEnded = true;
}
- if (
- sourceMessage &&
- sourceMessage.sources &&
- sourceMessage.sources.length > 0
- ) {
- processedMessage = processedMessage.replace(
+ if (sources.length > 0) {
+ processedText = processedText.replace(
citationRegex,
(_, capturedContent: string) => {
const numbers = capturedContent
@@ -379,7 +364,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
return `[${numStr}]`;
}
- const source = sourceMessage.sources?.[number - 1];
+ const source = sources[number - 1];
const url = source?.metadata?.url;
if (url) {
@@ -393,37 +378,27 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
return linksHtml;
},
);
- speechMessage = aiMessage.content.replace(regex, '');
+ speechMessage += block.data.replace(regex, '');
} else {
- processedMessage = processedMessage.replace(regex, '');
- speechMessage = aiMessage.content.replace(regex, '');
+ processedText = processedText.replace(regex, '');
+ speechMessage += block.data.replace(regex, '');
}
- const suggestionMessage = messages.find(
- (m, j) =>
- j > i &&
- m.role === 'suggestion' &&
- (nextUserMessageIndex === -1 || j < nextUserMessageIndex),
- ) as SuggestionMessage | undefined;
-
- if (suggestionMessage && suggestionMessage.suggestions.length > 0) {
- suggestions = suggestionMessage.suggestions;
- }
+ textBlocks.push(processedText);
+ } else if (block.type === 'suggestion') {
+ suggestions = block.data;
}
+ });
- sections.push({
- userMessage: msg,
- assistantMessage: aiMessage,
- sourceMessage: sourceMessage,
- parsedAssistantMessage: processedMessage,
- speechMessage,
- thinkingEnded,
- suggestions: suggestions,
- });
- }
+ return {
+ message: msg,
+ parsedTextBlocks: textBlocks,
+ speechMessage,
+ thinkingEnded,
+ suggestions,
+ widgets: widgetBlocks,
+ };
});
-
- return sections;
}, [messages]);
useEffect(() => {
@@ -489,24 +464,17 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
const rewrite = (messageId: string) => {
const index = messages.findIndex((msg) => msg.messageId === messageId);
- const chatTurnsIndex = chatTurns.findIndex(
- (msg) => msg.messageId === messageId,
- );
if (index === -1) return;
- const message = chatTurns[chatTurnsIndex - 1];
+ setMessages((prev) => prev.slice(0, index));
- setMessages((prev) => {
- return [
- ...prev.slice(0, messages.length > 2 ? messages.indexOf(message) : 0),
- ];
- });
setChatHistory((prev) => {
- return [...prev.slice(0, chatTurns.length > 2 ? chatTurnsIndex - 1 : 0)];
+ return prev.slice(0, index * 2);
});
- sendMessage(message.content, message.messageId, true);
+ const messageToRewrite = messages[index];
+ sendMessage(messageToRewrite.query, messageToRewrite.messageId, true);
};
useEffect(() => {
@@ -527,88 +495,165 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
) => {
if (loading || !message) return;
setLoading(true);
+ setResearchEnded(false);
setMessageAppeared(false);
if (messages.length <= 1) {
window.history.replaceState(null, '', `/c/${chatId}`);
}
- let recievedMessage = '';
- let added = false;
-
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
+ const backendId = crypto.randomBytes(20).toString('hex');
- setMessages((prevMessages) => [
- ...prevMessages,
- {
- content: message,
- messageId: messageId,
- chatId: chatId!,
- role: 'user',
- createdAt: new Date(),
- },
- ]);
+ const newMessage: Message = {
+ messageId,
+ chatId: chatId!,
+ backendId,
+ query: message,
+ responseBlocks: [],
+ status: 'answering',
+ createdAt: new Date(),
+ };
+
+ setMessages((prevMessages) => [...prevMessages, newMessage]);
+
+ const receivedTextRef = { current: '' };
const messageHandler = async (data: any) => {
if (data.type === 'error') {
toast.error(data.data);
setLoading(false);
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.messageId === messageId
+ ? { ...msg, status: 'error' as const }
+ : msg,
+ ),
+ );
return;
}
+ if (data.type === 'researchComplete') {
+ setResearchEnded(true);
+ if (
+ newMessage.responseBlocks.find(
+ (b) => b.type === 'source' && b.data.length > 0,
+ )
+ ) {
+ setMessageAppeared(true);
+ }
+ }
+
+ if (data.type === 'block') {
+ setMessages((prev) =>
+ prev.map((msg) => {
+ if (msg.messageId === messageId) {
+ return {
+ ...msg,
+ responseBlocks: [...msg.responseBlocks, data.block],
+ };
+ }
+ return msg;
+ }),
+ );
+ }
+
+ if (data.type === 'updateBlock') {
+ setMessages((prev) =>
+ prev.map((msg) => {
+ if (msg.messageId === messageId) {
+ const updatedBlocks = msg.responseBlocks.map((block) => {
+ if (block.id === data.blockId) {
+ const updatedBlock = { ...block };
+ applyPatch(updatedBlock, data.patch);
+ return updatedBlock;
+ }
+ return block;
+ });
+ return { ...msg, responseBlocks: updatedBlocks };
+ }
+ return msg;
+ }),
+ );
+ }
+
if (data.type === 'sources') {
- setMessages((prevMessages) => [
- ...prevMessages,
- {
- messageId: data.messageId,
- chatId: chatId!,
- role: 'source',
- sources: data.data,
- createdAt: new Date(),
- },
- ]);
+ const sourceBlock: Block = {
+ id: crypto.randomBytes(7).toString('hex'),
+ type: 'source',
+ data: data.data,
+ };
+
+ setMessages((prev) =>
+ prev.map((msg) => {
+ if (msg.messageId === messageId) {
+ return {
+ ...msg,
+ responseBlocks: [...msg.responseBlocks, sourceBlock],
+ };
+ }
+ return msg;
+ }),
+ );
if (data.data.length > 0) {
setMessageAppeared(true);
}
}
if (data.type === 'message') {
- if (!added) {
- setMessages((prevMessages) => [
- ...prevMessages,
- {
- content: data.data,
- messageId: data.messageId,
- chatId: chatId!,
- role: 'assistant',
- createdAt: new Date(),
- },
- ]);
- added = true;
- setMessageAppeared(true);
- } else {
- setMessages((prev) =>
- prev.map((message) => {
- if (
- message.messageId === data.messageId &&
- message.role === 'assistant'
- ) {
- return { ...message, content: message.content + data.data };
- }
+ receivedTextRef.current += data.data;
- return message;
- }),
- );
- }
- recievedMessage += data.data;
+ setMessages((prev) =>
+ prev.map((msg) => {
+ if (msg.messageId === messageId) {
+ const existingTextBlockIndex = msg.responseBlocks.findIndex(
+ (b) => b.type === 'text',
+ );
+
+ if (existingTextBlockIndex >= 0) {
+ const updatedBlocks = [...msg.responseBlocks];
+ const existingBlock = updatedBlocks[
+ existingTextBlockIndex
+ ] as Block & { type: 'text' };
+ updatedBlocks[existingTextBlockIndex] = {
+ ...existingBlock,
+ data: existingBlock.data + data.data,
+ };
+ return { ...msg, responseBlocks: updatedBlocks };
+ } else {
+ const textBlock: Block = {
+ id: crypto.randomBytes(7).toString('hex'),
+ type: 'text',
+ data: data.data,
+ };
+ return {
+ ...msg,
+ responseBlocks: [...msg.responseBlocks, textBlock],
+ };
+ }
+ }
+ return msg;
+ }),
+ );
+ setMessageAppeared(true);
}
if (data.type === 'messageEnd') {
- setChatHistory((prevHistory) => [
- ...prevHistory,
+ const newHistory: [string, string][] = [
+ ...chatHistory,
['human', message],
- ['assistant', recievedMessage],
- ]);
+ ['assistant', receivedTextRef.current],
+ ];
+
+ setChatHistory(newHistory);
+
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.messageId === messageId
+ ? { ...msg, status: 'completed' as const }
+ : msg,
+ ),
+ );
setLoading(false);
@@ -626,38 +671,37 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
?.click();
}
- /* Check if there are sources after message id's index and no suggestions */
-
- const userMessageIndex = messagesRef.current.findIndex(
- (msg) => msg.messageId === messageId && msg.role === 'user',
+ // Check if there are sources and no suggestions
+ const currentMsg = messagesRef.current.find(
+ (msg) => msg.messageId === messageId,
);
- const sourceMessage = messagesRef.current.find(
- (msg, i) => i > userMessageIndex && msg.role === 'source',
- ) as SourceMessage | undefined;
-
- const suggestionMessageIndex = messagesRef.current.findIndex(
- (msg, i) => i > userMessageIndex && msg.role === 'suggestion',
+ const hasSourceBlocks = currentMsg?.responseBlocks.some(
+ (block) => block.type === 'source' && block.data.length > 0,
+ );
+ const hasSuggestions = currentMsg?.responseBlocks.some(
+ (block) => block.type === 'suggestion',
);
- if (
- sourceMessage &&
- sourceMessage.sources.length > 0 &&
- suggestionMessageIndex == -1
- ) {
- const suggestions = await getSuggestions(messagesRef.current);
- setMessages((prev) => {
- return [
- ...prev,
- {
- role: 'suggestion',
- suggestions: suggestions,
- chatId: chatId!,
- createdAt: new Date(),
- messageId: crypto.randomBytes(7).toString('hex'),
- },
- ];
- });
+ if (hasSourceBlocks && !hasSuggestions) {
+ const suggestions = await getSuggestions(newHistory);
+ const suggestionBlock: Block = {
+ id: crypto.randomBytes(7).toString('hex'),
+ type: 'suggestion',
+ data: suggestions,
+ };
+
+ setMessages((prev) =>
+ prev.map((msg) => {
+ if (msg.messageId === messageId) {
+ return {
+ ...msg,
+ responseBlocks: [...msg.responseBlocks, suggestionBlock],
+ };
+ }
+ return msg;
+ }),
+ );
}
}
};
@@ -726,7 +770,6 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
{
chatModelProvider,
embeddingModelProvider,
setEmbeddingModelProvider,
+ researchEnded,
+ setResearchEnded,
}}
>
{children}
diff --git a/src/lib/models/base/provider.ts b/src/lib/models/base/provider.ts
index 950525e..cf69d49 100644
--- a/src/lib/models/base/provider.ts
+++ b/src/lib/models/base/provider.ts
@@ -1,5 +1,3 @@
-import { Embeddings } from '@langchain/core/embeddings';
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { ModelList, ProviderMetadata } from '../types';
import { UIConfigField } from '@/lib/config/types';
import BaseLLM from './llm';
diff --git a/src/lib/models/providers/openai/index.ts b/src/lib/models/providers/openai/index.ts
index 8b5eacb..772e762 100644
--- a/src/lib/models/providers/openai/index.ts
+++ b/src/lib/models/providers/openai/index.ts
@@ -1,5 +1,3 @@
-import { BaseChatModel } from '@langchain/core/language_models/chat_models';
-import { Embeddings } from '@langchain/core/embeddings';
import { UIConfigField } from '@/lib/config/types';
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
import { Model, ModelList, ProviderMetadata } from '../../types';
diff --git a/yarn.lock b/yarn.lock
index d202f75..e20ba09 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -24,7 +24,7 @@
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.3.tgz#10491113799fb8d77e1d9273384d5d68deeea8f6"
integrity sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw==
-"@babel/runtime@^7.18.3":
+"@babel/runtime@^7.18.3", "@babel/runtime@^7.26.10":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
@@ -55,6 +55,19 @@
enabled "2.0.x"
kuler "^2.0.0"
+"@deno/shim-deno-test@^0.5.0":
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/@deno/shim-deno-test/-/shim-deno-test-0.5.0.tgz#7d5dd221c736d182e587b8fd9bfca49b4dc0aa79"
+ integrity sha512-4nMhecpGlPi0cSzT67L+Tm+GOJqvuk8gqHBziqcUQOarnuIax1z96/gJHCSIz2Z0zhxE6Rzwb3IZXPtFh51j+w==
+
+"@deno/shim-deno@~0.18.0":
+ version "0.18.2"
+ resolved "https://registry.yarnpkg.com/@deno/shim-deno/-/shim-deno-0.18.2.tgz#9fe2fe7c91062bf2d127204f3110c09806cbef92"
+ integrity sha512-oQ0CVmOio63wlhwQF75zA4ioolPvOwAoK0yuzcS5bDC1JUvH3y1GS8xPh8EOpcoDQRU4FTG8OQfxhpR+c6DrzA==
+ dependencies:
+ "@deno/shim-deno-test" "^0.5.0"
+ which "^4.0.0"
+
"@drizzle-team/brocli@^0.10.2":
version "0.10.2"
resolved "https://registry.yarnpkg.com/@drizzle-team/brocli/-/brocli-0.10.2.tgz#9757c006a43daaa6f45512e6cf5fabed36fb9da7"
@@ -1822,6 +1835,11 @@ commander@^4.0.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
+complex.js@^2.2.5:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.4.3.tgz#72ee9c303a9b89ebcfeca0d39f74927d38721fce"
+ integrity sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ==
+
compute-cosine-similarity@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/compute-cosine-similarity/-/compute-cosine-similarity-1.1.0.tgz#0086a06b0239deb90f231f0da894efdc48884609"
@@ -1947,6 +1965,11 @@ decamelize@1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+decimal.js@^10.4.3:
+ version "10.6.0"
+ resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
+ integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
+
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
@@ -2379,6 +2402,16 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
+escape-latex@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1"
+ integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==
+
+escape-string-regexp@^1.0.2:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@@ -2607,6 +2640,11 @@ expand-template@^2.0.3:
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
+fancy-canvas@2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fancy-canvas/-/fancy-canvas-2.1.0.tgz#44b40e40419ad8ef8304df365e4276767e918552"
+ integrity sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==
+
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -2645,6 +2683,14 @@ fecha@^4.2.0:
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==
+"fetch-mock-cache@npm:fetch-mock-cache@^2.1.3":
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/fetch-mock-cache/-/fetch-mock-cache-2.3.1.tgz#1018f5fc2f91cf2511abcea8a5e3a3b05e2d02bf"
+ integrity sha512-hDk+Nbt0Y8Aq7KTEU6ASQAcpB34UjhkpD3QjzD6yvEKP4xVElAqXrjQ7maL+LYMGafx51Zq6qUfDM57PNu/qMw==
+ dependencies:
+ debug "^4.3.4"
+ filenamify-url "2.1.2"
+
fflate@^0.8.1:
version "0.8.2"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea"
@@ -2662,6 +2708,28 @@ file-uri-to-path@1.0.0:
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+filename-reserved-regex@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229"
+ integrity sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==
+
+filenamify-url@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/filenamify-url/-/filenamify-url-2.1.2.tgz#844607d5e86919617340ba0fad4b458dae247100"
+ integrity sha512-3rMbAr7vDNMOGsj1aMniQFl749QjgM+lMJ/77ZRSPTIgxvolZwoQbn8dXLs7xfd+hAdli+oTnSWZNkJJLWQFEQ==
+ dependencies:
+ filenamify "^4.3.0"
+ humanize-url "^2.1.1"
+
+filenamify@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-4.3.0.tgz#62391cb58f02b09971c9d4f9d63b3cf9aba03106"
+ integrity sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==
+ dependencies:
+ filename-reserved-regex "^2.0.0"
+ strip-outer "^1.0.1"
+ trim-repeated "^1.0.0"
+
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -2764,6 +2832,11 @@ fraction.js@^4.3.7:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
+fraction.js@^5.2.1:
+ version "5.3.4"
+ resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a"
+ integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==
+
framer-motion@^12.23.24:
version "12.23.24"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.23.24.tgz#4895b67e880bd2b1089e61fbaa32ae802fc24b8c"
@@ -3111,6 +3184,13 @@ humanize-ms@^1.2.1:
dependencies:
ms "^2.0.0"
+humanize-url@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/humanize-url/-/humanize-url-2.1.1.tgz#1be3dc2b8a23ee28fdf9db95b22962b3eb5e4683"
+ integrity sha512-V4nxsPGNE7mPjr1qDp471YfW8nhBiTRWrG/4usZlpvFU8I7gsV7Jvrrzv/snbLm5dWO3dr1ennu2YqnhTWFmYA==
+ dependencies:
+ normalize-url "^4.5.1"
+
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -3398,6 +3478,11 @@ jackspeak@^2.3.5, jackspeak@^2.3.6:
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
+javascript-natural-sort@^0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59"
+ integrity sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==
+
jiti@^1.21.0:
version "1.21.0"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
@@ -3440,6 +3525,11 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+json-schema@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
+ integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
+
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
@@ -3578,6 +3668,13 @@ lie@~3.3.0:
dependencies:
immediate "~3.0.5"
+lightweight-charts@^5.0.9:
+ version "5.0.9"
+ resolved "https://registry.yarnpkg.com/lightweight-charts/-/lightweight-charts-5.0.9.tgz#22ccaf5643b4561c0accecee5d84eec78d3d058d"
+ integrity sha512-8oQIis8jfZVfSwz8j9Z5x3O79dIRTkEYI9UY7DKtE4O3ZxlHjMK3L0+4nOVOOFq4FHI/oSIzz1RHeNImCk6/Jg==
+ dependencies:
+ fancy-canvas "2.1.0"
+
lilconfig@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
@@ -3703,6 +3800,21 @@ math-intrinsics@^1.1.0:
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+mathjs@^15.1.0:
+ version "15.1.0"
+ resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-15.1.0.tgz#e910f626c5d66ff1902eb69c3b2e7b7d010fc39e"
+ integrity sha512-HfnAcScQm9drGryodlDqeS3WAl4gUTYGDcOtcqL/8s23MZ28Ib1i8XnYK3ZdjNuaW/L4BAp9lIp8vxAMrcuu1w==
+ dependencies:
+ "@babel/runtime" "^7.26.10"
+ complex.js "^2.2.5"
+ decimal.js "^10.4.3"
+ escape-latex "^1.2.0"
+ fraction.js "^5.2.1"
+ javascript-natural-sort "^0.7.1"
+ seedrandom "^3.0.5"
+ tiny-emitter "^2.1.0"
+ typed-function "^4.2.1"
+
merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@@ -3909,6 +4021,11 @@ normalize-range@^0.1.2:
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
+normalize-url@^4.5.1:
+ version "4.5.1"
+ resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
+ integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
+
object-assign@^4.0.1, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -4354,6 +4471,13 @@ proxy-from-env@^1.1.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+psl@^1.1.33:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.15.0.tgz#bdace31896f1d97cec6a79e8224898ce93d974c6"
+ integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==
+ dependencies:
+ punycode "^2.3.1"
+
pump@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8"
@@ -4362,11 +4486,16 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
-punycode@^2.1.0:
+punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
+querystringify@^2.1.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
+ integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
+
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@@ -4492,6 +4621,11 @@ regexp.prototype.flags@^1.5.2:
es-errors "^1.3.0"
set-function-name "^2.0.1"
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+ integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
+
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -4607,6 +4741,11 @@ scheduler@^0.23.0:
dependencies:
loose-envify "^1.1.0"
+seedrandom@^3.0.5:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
+ integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
+
selderee@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.11.0.tgz#6af0c7983e073ad3e35787ffe20cefd9daf0ec8a"
@@ -4975,6 +5114,13 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
+strip-outer@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631"
+ integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==
+ dependencies:
+ escape-string-regexp "^1.0.2"
+
styled-jsx@5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.6.tgz#83b90c077e6c6a80f7f5e8781d0f311b2fe41499"
@@ -5120,6 +5266,23 @@ thenify-all@^1.0.0:
dependencies:
any-promise "^1.0.0"
+tiny-emitter@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
+ integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
+
+tldts-core@^6.1.86:
+ version "6.1.86"
+ resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.86.tgz#a93e6ed9d505cb54c542ce43feb14c73913265d8"
+ integrity sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==
+
+tldts@^6.1.32:
+ version "6.1.86"
+ resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.86.tgz#087e0555b31b9725ee48ca7e77edc56115cd82f7"
+ integrity sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==
+ dependencies:
+ tldts-core "^6.1.86"
+
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -5127,11 +5290,42 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
+"tough-cookie-file-store@npm:tough-cookie-file-store@^2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie-file-store/-/tough-cookie-file-store-2.0.3.tgz#788f7a6fe5cd8f61a1afb71b2f0b964ebf914b80"
+ integrity sha512-sMpZVcmFf6EYFHFFl+SYH4W1/OnXBYMGDsv2IlbQ2caHyFElW/UR/gpj/KYU1JwmP4dE9xqwv2+vWcmlXHojSw==
+ dependencies:
+ tough-cookie "^4.0.0"
+
+tough-cookie@^4.0.0:
+ version "4.1.4"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36"
+ integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==
+ dependencies:
+ psl "^1.1.33"
+ punycode "^2.1.1"
+ universalify "^0.2.0"
+ url-parse "^1.5.3"
+
+"tough-cookie@npm:tough-cookie@^5.1.1":
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.1.2.tgz#66d774b4a1d9e12dc75089725af3ac75ec31bed7"
+ integrity sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==
+ dependencies:
+ tldts "^6.1.32"
+
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+trim-repeated@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21"
+ integrity sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==
+ dependencies:
+ escape-string-regexp "^1.0.2"
+
triple-beam@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984"
@@ -5235,6 +5429,11 @@ typed-array-length@^1.0.6:
is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0"
+typed-function@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-4.2.1.tgz#19aa51847aa2dea9ef5e7fb7641c060179a74426"
+ integrity sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==
+
typescript@^5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
@@ -5275,6 +5474,11 @@ undici-types@~7.14.0:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.14.0.tgz#4c037b32ca4d7d62fae042174604341588bc0840"
integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==
+universalify@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
+ integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
+
update-browserslist-db@^1.0.13:
version "1.0.13"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4"
@@ -5290,6 +5494,14 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
+url-parse@^1.5.3:
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
+ integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
+ dependencies:
+ querystringify "^2.1.1"
+ requires-port "^1.0.0"
+
use-composed-ref@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
@@ -5490,6 +5702,17 @@ xmlbuilder@^10.0.0:
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz#8cae6688cc9b38d850b7c8d3c0a4161dcaf475b0"
integrity sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==
+yahoo-finance2@^3.10.2:
+ version "3.10.2"
+ resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-3.10.2.tgz#ed1fbcb7cd0e5c37abe84936826aaca451739297"
+ integrity sha512-MH4EdugRurygLTMd1UryPwfYR8aWSOeyh++JSarMrf+bROfvNGmE0lAi/C9TuTc3mH8ORuRdt+O9PEeCCmzTLg==
+ dependencies:
+ "@deno/shim-deno" "~0.18.0"
+ fetch-mock-cache "npm:fetch-mock-cache@^2.1.3"
+ json-schema "^0.4.0"
+ tough-cookie "npm:tough-cookie@^5.1.1"
+ tough-cookie-file-store "npm:tough-cookie-file-store@^2.0.3"
+
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
From 0a62c60da2d736b71cbe3ec060763ac264d67f41 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Mon, 24 Nov 2025 15:35:00 +0530
Subject: [PATCH 053/199] feat(widgets): add LLM context to prevent context
overflow
---
src/lib/agents/search/index.ts | 3 +-
src/lib/agents/search/types.ts | 1 +
.../search/widgets/calculationWidget.ts | 2 +
src/lib/agents/search/widgets/stockWidget.ts | 173 +++++++++---------
.../agents/search/widgets/weatherWidget.ts | 4 +
5 files changed, 99 insertions(+), 84 deletions(-)
diff --git a/src/lib/agents/search/index.ts b/src/lib/agents/search/index.ts
index bacdb88..ab12bfe 100644
--- a/src/lib/agents/search/index.ts
+++ b/src/lib/agents/search/index.ts
@@ -65,14 +65,13 @@ class SearchAgent {
const widgetContext = widgetOutputs
.map((o) => {
- return `${o.type}: ${JSON.stringify(o.data)}`;
+ return `${o.type}: ${o.llmContext}`;
})
.join('\n-------------\n');
const finalContextWithWidgets = `${finalContext} \n${widgetContext} `;
const writerPrompt = getWriterPrompt(finalContextWithWidgets);
-
const answerStream = input.config.llm.streamText({
messages: [
{
diff --git a/src/lib/agents/search/types.ts b/src/lib/agents/search/types.ts
index 421ee7f..82900c5 100644
--- a/src/lib/agents/search/types.ts
+++ b/src/lib/agents/search/types.ts
@@ -43,6 +43,7 @@ export type WidgetConfig = {
export type WidgetOutput = {
type: string;
+ llmContext: string;
data: any;
};
diff --git a/src/lib/agents/search/widgets/calculationWidget.ts b/src/lib/agents/search/widgets/calculationWidget.ts
index c613b40..1c1ba51 100644
--- a/src/lib/agents/search/widgets/calculationWidget.ts
+++ b/src/lib/agents/search/widgets/calculationWidget.ts
@@ -45,6 +45,7 @@ const calculationWidget: Widget = {
return {
type: 'calculation_result',
+ llmContext: `The result of the expression "${params.expression}" is ${result}.`,
data: {
expression: params.expression,
result: result,
@@ -53,6 +54,7 @@ const calculationWidget: Widget = {
} catch (error) {
return {
type: 'calculation_result',
+ llmContext: 'Failed to evaluate mathematical expression.',
data: {
expression: params.expression,
result: `Error evaluating expression: ${error}`,
diff --git a/src/lib/agents/search/widgets/stockWidget.ts b/src/lib/agents/search/widgets/stockWidget.ts
index b4f8b86..c18b734 100644
--- a/src/lib/agents/search/widgets/stockWidget.ts
+++ b/src/lib/agents/search/widgets/stockWidget.ts
@@ -286,120 +286,129 @@ You can set skipSearch to true if the stock widget can fully answer the user's q
chartData: {
'1D': chart1D
? {
- timestamps: chart1D.quotes.map((q: any) => q.date.getTime()),
- prices: chart1D.quotes.map((q: any) => q.close),
- }
+ timestamps: chart1D.quotes.map((q: any) => q.date.getTime()),
+ prices: chart1D.quotes.map((q: any) => q.close),
+ }
: null,
'5D': chart5D
? {
- timestamps: chart5D.quotes.map((q: any) => q.date.getTime()),
- prices: chart5D.quotes.map((q: any) => q.close),
- }
+ timestamps: chart5D.quotes.map((q: any) => q.date.getTime()),
+ prices: chart5D.quotes.map((q: any) => q.close),
+ }
: null,
'1M': chart1M
? {
- timestamps: chart1M.quotes.map((q: any) => q.date.getTime()),
- prices: chart1M.quotes.map((q: any) => q.close),
- }
+ timestamps: chart1M.quotes.map((q: any) => q.date.getTime()),
+ prices: chart1M.quotes.map((q: any) => q.close),
+ }
: null,
'3M': chart3M
? {
- timestamps: chart3M.quotes.map((q: any) => q.date.getTime()),
- prices: chart3M.quotes.map((q: any) => q.close),
- }
+ timestamps: chart3M.quotes.map((q: any) => q.date.getTime()),
+ prices: chart3M.quotes.map((q: any) => q.close),
+ }
: null,
'6M': chart6M
? {
- timestamps: chart6M.quotes.map((q: any) => q.date.getTime()),
- prices: chart6M.quotes.map((q: any) => q.close),
- }
+ timestamps: chart6M.quotes.map((q: any) => q.date.getTime()),
+ prices: chart6M.quotes.map((q: any) => q.close),
+ }
: null,
'1Y': chart1Y
? {
- timestamps: chart1Y.quotes.map((q: any) => q.date.getTime()),
- prices: chart1Y.quotes.map((q: any) => q.close),
- }
+ timestamps: chart1Y.quotes.map((q: any) => q.date.getTime()),
+ prices: chart1Y.quotes.map((q: any) => q.close),
+ }
: null,
MAX: chartMAX
? {
- timestamps: chartMAX.quotes.map((q: any) => q.date.getTime()),
- prices: chartMAX.quotes.map((q: any) => q.close),
- }
+ timestamps: chartMAX.quotes.map((q: any) => q.date.getTime()),
+ prices: chartMAX.quotes.map((q: any) => q.close),
+ }
: null,
},
comparisonData: comparisonData
? comparisonData.map((comp: any) => ({
- ticker: comp.ticker,
- name: comp.name,
- chartData: {
- '1D': comp.charts[0]
- ? {
- timestamps: comp.charts[0].quotes.map((q: any) =>
- q.date.getTime(),
- ),
- prices: comp.charts[0].quotes.map((q: any) => q.close),
- }
- : null,
- '5D': comp.charts[1]
- ? {
- timestamps: comp.charts[1].quotes.map((q: any) =>
- q.date.getTime(),
- ),
- prices: comp.charts[1].quotes.map((q: any) => q.close),
- }
- : null,
- '1M': comp.charts[2]
- ? {
- timestamps: comp.charts[2].quotes.map((q: any) =>
- q.date.getTime(),
- ),
- prices: comp.charts[2].quotes.map((q: any) => q.close),
- }
- : null,
- '3M': comp.charts[3]
- ? {
- timestamps: comp.charts[3].quotes.map((q: any) =>
- q.date.getTime(),
- ),
- prices: comp.charts[3].quotes.map((q: any) => q.close),
- }
- : null,
- '6M': comp.charts[4]
- ? {
- timestamps: comp.charts[4].quotes.map((q: any) =>
- q.date.getTime(),
- ),
- prices: comp.charts[4].quotes.map((q: any) => q.close),
- }
- : null,
- '1Y': comp.charts[5]
- ? {
- timestamps: comp.charts[5].quotes.map((q: any) =>
- q.date.getTime(),
- ),
- prices: comp.charts[5].quotes.map((q: any) => q.close),
- }
- : null,
- MAX: comp.charts[6]
- ? {
- timestamps: comp.charts[6].quotes.map((q: any) =>
- q.date.getTime(),
- ),
- prices: comp.charts[6].quotes.map((q: any) => q.close),
- }
- : null,
- },
- }))
+ ticker: comp.ticker,
+ name: comp.name,
+ chartData: {
+ '1D': comp.charts[0]
+ ? {
+ timestamps: comp.charts[0].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[0].quotes.map((q: any) => q.close),
+ }
+ : null,
+ '5D': comp.charts[1]
+ ? {
+ timestamps: comp.charts[1].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[1].quotes.map((q: any) => q.close),
+ }
+ : null,
+ '1M': comp.charts[2]
+ ? {
+ timestamps: comp.charts[2].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[2].quotes.map((q: any) => q.close),
+ }
+ : null,
+ '3M': comp.charts[3]
+ ? {
+ timestamps: comp.charts[3].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[3].quotes.map((q: any) => q.close),
+ }
+ : null,
+ '6M': comp.charts[4]
+ ? {
+ timestamps: comp.charts[4].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[4].quotes.map((q: any) => q.close),
+ }
+ : null,
+ '1Y': comp.charts[5]
+ ? {
+ timestamps: comp.charts[5].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[5].quotes.map((q: any) => q.close),
+ }
+ : null,
+ MAX: comp.charts[6]
+ ? {
+ timestamps: comp.charts[6].quotes.map((q: any) =>
+ q.date.getTime(),
+ ),
+ prices: comp.charts[6].quotes.map((q: any) => q.close),
+ }
+ : null,
+ },
+ }))
: null,
};
return {
type: 'stock',
+ llmContext: `Current price of ${stockData.shortName} (${stockData.symbol}) is ${stockData.regularMarketPrice} ${stockData.currency}. Other details: ${JSON.stringify({
+ marketState: stockData.marketState,
+ regularMarketChange: stockData.regularMarketChange,
+ regularMarketChangePercent: stockData.regularMarketChangePercent,
+ marketCap: stockData.marketCap,
+ peRatio: stockData.trailingPE,
+ dividendYield: stockData.dividendYield,
+ })}`,
data: stockData,
};
} catch (error: any) {
return {
type: 'stock',
+ llmContext: 'Failed to fetch stock data.',
data: {
error: `Error fetching stock data: ${error.message || error}`,
ticker: params.ticker,
diff --git a/src/lib/agents/search/widgets/weatherWidget.ts b/src/lib/agents/search/widgets/weatherWidget.ts
index 4b2dcf9..3d78df3 100644
--- a/src/lib/agents/search/widgets/weatherWidget.ts
+++ b/src/lib/agents/search/widgets/weatherWidget.ts
@@ -94,6 +94,7 @@ You can set skipSearch to true if the weather widget can fully answer the user's
return {
type: 'weather',
+ llmContext: `Weather in ${params.location} is ${weatherData.current}`,
data: {
location: params.location,
latitude: location.lat,
@@ -138,6 +139,7 @@ You can set skipSearch to true if the weather widget can fully answer the user's
return {
type: 'weather',
+ llmContext: `Weather in ${locationData.display_name} is ${weatherData.current}`,
data: {
location: locationData.display_name,
latitude: params.lat,
@@ -159,11 +161,13 @@ You can set skipSearch to true if the weather widget can fully answer the user's
return {
type: 'weather',
+ llmContext: 'No valid location or coordinates provided.',
data: null,
};
} catch (err) {
return {
type: 'weather',
+ llmContext: 'Failed to fetch weather data.',
data: {
error: `Error fetching weather data: ${err}`,
},
From 7544bbafaf1b7e00d8c85108dce55cfcf83876ea Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Thu, 27 Nov 2025 11:09:37 +0530
Subject: [PATCH 054/199] feat(weather-widget): prevent [object Object] from
being sent by stringifying
---
src/lib/agents/search/widgets/weatherWidget.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/lib/agents/search/widgets/weatherWidget.ts b/src/lib/agents/search/widgets/weatherWidget.ts
index 3d78df3..2c6d7ab 100644
--- a/src/lib/agents/search/widgets/weatherWidget.ts
+++ b/src/lib/agents/search/widgets/weatherWidget.ts
@@ -94,7 +94,7 @@ You can set skipSearch to true if the weather widget can fully answer the user's
return {
type: 'weather',
- llmContext: `Weather in ${params.location} is ${weatherData.current}`,
+ llmContext: `Weather in ${params.location} is ${JSON.stringify(weatherData.current)}`,
data: {
location: params.location,
latitude: location.lat,
@@ -139,7 +139,7 @@ You can set skipSearch to true if the weather widget can fully answer the user's
return {
type: 'weather',
- llmContext: `Weather in ${locationData.display_name} is ${weatherData.current}`,
+ llmContext: `Weather in ${locationData.display_name} is ${JSON.stringify(weatherData.current)}`,
data: {
location: locationData.display_name,
latitude: params.lat,
From f83bd06e89ef1a4cd5516122eba4c688c0ba1115 Mon Sep 17 00:00:00 2001
From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com>
Date: Thu, 27 Nov 2025 11:10:04 +0530
Subject: [PATCH 055/199] feat(ollama-llm): remove explicit think parameter
setting
---
src/lib/models/providers/ollama/ollamaLLM.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/lib/models/providers/ollama/ollamaLLM.ts b/src/lib/models/providers/ollama/ollamaLLM.ts
index 05869ca..1089cb9 100644
--- a/src/lib/models/providers/ollama/ollamaLLM.ts
+++ b/src/lib/models/providers/ollama/ollamaLLM.ts
@@ -98,7 +98,6 @@ class OllamaLLM extends BaseLLM {
model: this.config.model,
messages: input.messages,
format: z.toJSONSchema(input.schema),
- think: false,
options: {
top_p: this.config.options?.topP,
temperature: 0.7,
@@ -126,7 +125,6 @@ class OllamaLLM extends BaseLLM