show thiniing

This commit is contained in:
bracesproul
2025-02-03 16:30:37 -08:00
parent 67d4df0d8a
commit 3c812ddee7
18 changed files with 849 additions and 135 deletions
+1
View File
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"groq",
"langchain",
"langsmith",
"opencanvas",
+2
View File
@@ -39,12 +39,14 @@
"@langchain/community": "^0.3.28",
"@langchain/core": "^0.3.36",
"@langchain/google-genai": "^0.1.7",
"@langchain/groq": "^0.1.3",
"@langchain/langgraph": "^0.2.41",
"@langchain/langgraph-sdk": "^0.0.36",
"@langchain/ollama": "^0.1.4",
"@langchain/openai": "^0.4.2",
"@mendable/firecrawl-js": "1.10.1",
"@nextjournal/lang-clojure": "^1.0.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
+1 -1
View File
@@ -48,7 +48,7 @@ export const reflectNode = async (
// This lets us "debounce" repeated requests to the memory graph
// if the user is actively engaging in a conversation. This saves us $$ and
// can help reduce the occurrence of duplicate memories.
afterSeconds: 15,
afterSeconds: 5 * 60, // 5 minutes
}
);
@@ -1,3 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import {
OpenCanvasGraphAnnotation,
OpenCanvasGraphReturnType,
@@ -8,16 +9,23 @@ import { buildPrompt, createNewArtifactContent, validateState } from "./utils";
import {
createContextDocumentMessages,
getFormattedReflections,
getModelConfig,
getModelFromConfig,
isUsingO1MiniModel,
optionallyGetSystemPromptFromConfig,
} from "@/agent/utils";
import { isArtifactMarkdownContent } from "@/lib/artifact_content_types";
import { AIMessage } from "@langchain/core/messages";
import {
extractThinkingAndResponseTokens,
isThinkingModel,
} from "@/contexts/utils";
export const rewriteArtifact = async (
state: typeof OpenCanvasGraphAnnotation.State,
config: LangGraphRunnableConfig
): Promise<OpenCanvasGraphReturnType> => {
const { modelName } = getModelConfig(config);
const smallModelWithConfig = (await getModelFromConfig(config)).withConfig({
runName: "rewrite_artifact_model_call",
});
@@ -55,12 +63,25 @@ export const rewriteArtifact = async (
recentHumanMessage,
]);
let thinkingMessage: AIMessage | undefined;
let artifactContentText = newArtifactResponse.content as string;
if (isThinkingModel(modelName)) {
const { thinking, response } =
extractThinkingAndResponseTokens(artifactContentText);
thinkingMessage = new AIMessage({
id: `thinking-${uuidv4()}`,
content: thinking,
});
artifactContentText = response;
}
const newArtifactContent = createNewArtifactContent({
artifactType,
state,
currentArtifactContent,
artifactMetaToolCall,
newContent: newArtifactResponse.content as string,
newContent: artifactContentText as string,
});
return {
@@ -69,5 +90,7 @@ export const rewriteArtifact = async (
currentIndex: state.artifact.contents.length + 1,
contents: [...state.artifact.contents, newArtifactContent],
},
messages: [...(thinkingMessage ? [thinkingMessage] : [])],
_messages: [...(thinkingMessage ? [thinkingMessage] : [])],
};
};
@@ -1,10 +1,16 @@
import { v4 as uuidv4 } from "uuid";
import { LangGraphRunnableConfig } from "@langchain/langgraph";
import { getArtifactContent } from "../../../contexts/utils";
import {
extractThinkingAndResponseTokens,
getArtifactContent,
isThinkingModel,
} from "../../../contexts/utils";
import { isArtifactMarkdownContent } from "../../../lib/artifact_content_types";
import { ArtifactV3, Reflections } from "../../../types";
import {
ensureStoreInConfig,
formatReflections,
getModelConfig,
getModelFromConfig,
} from "../../utils";
import {
@@ -15,11 +21,13 @@ import {
CHANGE_ARTIFACT_TO_PIRATE_PROMPT,
} from "../prompts";
import { OpenCanvasGraphAnnotation, OpenCanvasGraphReturnType } from "../state";
import { AIMessage } from "@langchain/core/messages";
export const rewriteArtifactTheme = async (
state: typeof OpenCanvasGraphAnnotation.State,
config: LangGraphRunnableConfig
): Promise<OpenCanvasGraphReturnType> => {
const { modelName } = getModelConfig(config);
const smallModel = await getModelFromConfig(config);
const store = ensureStoreInConfig(config);
@@ -110,6 +118,19 @@ export const rewriteArtifactTheme = async (
{ role: "user", content: formattedPrompt },
]);
let thinkingMessage: AIMessage | undefined;
let artifactContentText = newArtifactValues.content as string;
if (isThinkingModel(modelName)) {
const { thinking, response } =
extractThinkingAndResponseTokens(artifactContentText);
thinkingMessage = new AIMessage({
id: `thinking-${uuidv4()}`,
content: thinking,
});
artifactContentText = response;
}
const newArtifact: ArtifactV3 = {
...state.artifact,
currentIndex: state.artifact.contents.length + 1,
@@ -118,12 +139,14 @@ export const rewriteArtifactTheme = async (
{
...currentArtifactContent,
index: state.artifact.contents.length + 1,
fullMarkdown: newArtifactValues.content as string,
fullMarkdown: artifactContentText,
},
],
};
return {
artifact: newArtifact,
messages: [...(thinkingMessage ? [thinkingMessage] : [])],
_messages: [...(thinkingMessage ? [thinkingMessage] : [])],
};
};
@@ -1,8 +1,13 @@
import { v4 as uuidv4 } from "uuid";
import { LangGraphRunnableConfig } from "@langchain/langgraph";
import { getArtifactContent } from "../../../contexts/utils";
import {
extractThinkingAndResponseTokens,
getArtifactContent,
isThinkingModel,
} from "../../../contexts/utils";
import { isArtifactCodeContent } from "../../../lib/artifact_content_types";
import { ArtifactCodeV3, ArtifactV3 } from "../../../types";
import { getModelFromConfig } from "../../utils";
import { getModelConfig, getModelFromConfig } from "../../utils";
import {
ADD_COMMENTS_TO_CODE_ARTIFACT_PROMPT,
ADD_LOGS_TO_CODE_ARTIFACT_PROMPT,
@@ -10,11 +15,13 @@ import {
PORT_LANGUAGE_CODE_ARTIFACT_PROMPT,
} from "../prompts";
import { OpenCanvasGraphAnnotation, OpenCanvasGraphReturnType } from "../state";
import { AIMessage } from "@langchain/core/messages";
export const rewriteCodeArtifactTheme = async (
state: typeof OpenCanvasGraphAnnotation.State,
config: LangGraphRunnableConfig
): Promise<OpenCanvasGraphReturnType> => {
const { modelName } = getModelConfig(config);
const smallModel = await getModelFromConfig(config);
const currentArtifactContent = state.artifact
@@ -80,13 +87,26 @@ export const rewriteCodeArtifactTheme = async (
{ role: "user", content: formattedPrompt },
]);
let thinkingMessage: AIMessage | undefined;
let artifactContentText = newArtifactValues.content as string;
if (isThinkingModel(modelName)) {
const { thinking, response } =
extractThinkingAndResponseTokens(artifactContentText);
thinkingMessage = new AIMessage({
id: `thinking-${uuidv4()}`,
content: thinking,
});
artifactContentText = response;
}
const newArtifactContent: ArtifactCodeV3 = {
index: state.artifact.contents.length + 1,
type: "code",
title: currentArtifactContent.title,
// Ensure the new artifact's language is updated, if necessary
language: state.portLanguage || currentArtifactContent.language,
code: newArtifactValues.content as string,
code: artifactContentText,
};
const newArtifact: ArtifactV3 = {
@@ -97,5 +117,7 @@ export const rewriteCodeArtifactTheme = async (
return {
artifact: newArtifact,
messages: [...(thinkingMessage ? [thinkingMessage] : [])],
_messages: [...(thinkingMessage ? [thinkingMessage] : [])],
};
};
+35
View File
@@ -215,6 +215,7 @@ export const getModelConfig = (
apiKey: process.env.OPENAI_API_KEY,
};
}
if (customModelName.includes("claude-")) {
return {
...providerConfig,
@@ -222,13 +223,32 @@ export const getModelConfig = (
apiKey: process.env.ANTHROPIC_API_KEY,
};
}
if (customModelName.includes("fireworks/")) {
let actualModelName = providerConfig.modelName;
if (
extra?.isToolCalling &&
actualModelName !== "accounts/fireworks/models/llama-v3p3-70b-instruct"
) {
actualModelName = "accounts/fireworks/models/llama-v3p3-70b-instruct";
}
return {
...providerConfig,
modelName: actualModelName,
modelProvider: "fireworks",
apiKey: process.env.FIREWORKS_API_KEY,
};
}
if (customModelName.startsWith("groq/")) {
const actualModelName = customModelName.replace("groq/", "");
return {
modelName: actualModelName,
modelProvider: "groq",
apiKey: process.env.GROQ_API_KEY,
};
}
if (customModelName.includes("gemini-")) {
let actualModelName = providerConfig.modelName;
if (extra?.isToolCalling && actualModelName.includes("thinking")) {
@@ -242,6 +262,21 @@ export const getModelConfig = (
apiKey: process.env.GOOGLE_API_KEY,
};
}
if (customModelName.includes("gemini-")) {
let actualModelName = providerConfig.modelName;
if (extra?.isToolCalling && actualModelName.includes("thinking")) {
// Gemini thinking does not support tools.
actualModelName = "gemini-2.0-flash-exp";
}
return {
...providerConfig,
modelName: actualModelName,
modelProvider: "google-genai",
apiKey: process.env.GOOGLE_API_KEY,
};
}
if (customModelName.startsWith("ollama-")) {
return {
modelName: customModelName.replace("ollama-", ""),
+51 -1
View File
@@ -1,9 +1,16 @@
"use client";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
ActionBarPrimitive,
getExternalStoreMessage,
MessagePrimitive,
MessageState,
useMessage,
} from "@assistant-ui/react";
import React, { Dispatch, SetStateAction, type FC } from "react";
@@ -23,12 +30,55 @@ interface AssistantMessageProps {
setFeedbackSubmitted: Dispatch<SetStateAction<boolean>>;
}
const ThinkingAssistantMessage = ({
message,
}: {
message: MessageState;
}): React.ReactElement => {
const { id, content } = message;
let contentText = "";
if (typeof content === "string") {
contentText = content;
} else {
const firstItem = content?.[0];
if (firstItem?.type === "text") {
contentText = firstItem.text;
}
}
if (contentText === "") {
return <></>;
}
return (
<Accordion
defaultValue={`accordion-${id}`}
type="single"
collapsible
className="w-full"
>
<AccordionItem value={`accordion-${id}`}>
<AccordionTrigger>Thoughts</AccordionTrigger>
<AccordionContent>{contentText}</AccordionContent>
</AccordionItem>
</Accordion>
);
};
export const AssistantMessage: FC<AssistantMessageProps> = ({
runId,
feedbackSubmitted,
setFeedbackSubmitted,
}) => {
const isLast = useMessage().isLast;
const message = useMessage();
const { isLast } = message;
const isThinkingMessage = message.id.startsWith("thinking-");
if (isThinkingMessage) {
// TODO: This will currently cause replies to be missing.
return <ThinkingAssistantMessage message={message} />;
}
return (
<MessagePrimitive.Root className="relative grid w-full max-w-2xl grid-cols-[auto_auto_1fr] grid-rows-[auto_1fr] py-4">
<Avatar className="col-start-1 row-span-full row-start-1 mr-4">
@@ -12,12 +12,18 @@ import {
ALL_MODELS,
LANGCHAIN_USER_ONLY_MODELS,
} from "@/constants";
import { useCallback, useEffect, useState } from "react";
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from "react";
import { AlertNewModelSelectorFeature } from "./alert-new-model-selector";
import { ModelConfigPanel } from "./model-config-pannel";
import { IsNewBadge } from "./new-badge";
import { cn } from "@/lib/utils";
import { CustomModelConfig } from "@/types";
import { CustomModelConfig, ModelConfigurationParams } from "@/types";
import { CaretSortIcon, GearIcon } from "@radix-ui/react-icons";
import {
Popover,
@@ -40,6 +46,71 @@ interface ModelSelectorProps {
modelConfigs: Record<string, CustomModelConfig>;
}
interface CommandModelItemProps {
model: ModelConfigurationParams;
handleModelChange: (newModel: ALL_MODEL_NAMES) => Promise<void>;
selectedModelName: ALL_MODEL_NAMES;
openConfigModelId: string | undefined;
config: CustomModelConfig;
setOpenConfigModelId: Dispatch<SetStateAction<string | undefined>>;
setModelConfig: (
modelName: ALL_MODEL_NAMES,
config: CustomModelConfig
) => void;
}
function CommandModelItem({
model,
handleModelChange,
selectedModelName,
openConfigModelId,
config,
setOpenConfigModelId,
setModelConfig,
}: CommandModelItemProps) {
return (
<CommandItem
value={model.name}
onSelect={handleModelChange}
className="flex items-center"
>
<Check
className={cn(
"mr-1 size-4",
selectedModelName === model.name ? "opacity-100" : "opacity-0"
)}
/>
<span className="flex flex-row w-full items-center justify-start gap-2">
{model.label}
{model.isNew && <IsNewBadge />}
</span>
{openConfigModelId === model.name ? (
<ModelConfigPanel
model={model}
modelConfig={config}
isOpen={true}
onOpenChange={(open) =>
setOpenConfigModelId(open ? model.name : undefined)
}
onClick={(e) => e.stopPropagation()}
setModelConfig={setModelConfig}
/>
) : (
<button
className="ml-auto flex-shrink-0 flex size-6 items-center justify-center focus:outline-none focus:ring-0"
onClick={(e) => {
e.stopPropagation();
setOpenConfigModelId(model.name);
}}
>
<GearIcon className="size-4" />
</button>
)}
</CommandItem>
);
}
export default function ModelSelector({
chatStarted,
modelName,
@@ -51,8 +122,7 @@ export default function ModelSelector({
const [isLangChainUser, setIsLangChainUser] = useState(false);
const [showAlert, setShowAlert] = useState(false);
const [open, setOpen] = useState(false);
const [openConfigModelId, setOpenConfigModelId] =
useState<ALL_MODEL_NAMES | null>(null);
const [openConfigModelId, setOpenConfigModelId] = useState<ALL_MODEL_NAMES>();
useEffect(() => {
if (!user) return;
@@ -112,6 +182,13 @@ export default function ModelSelector({
return false;
}
if (
model.name.includes("groq/") &&
process.env.NEXT_PUBLIC_GROQ_ENABLED === "false"
) {
return false;
}
// By default, return true if the environment variable is not set or is set to true
return true;
});
@@ -122,6 +199,28 @@ export default function ModelSelector({
(m) => m.name === modelName && m.isNew
);
const azureModelGroup = allAllowedModels.filter(
(m) => m.config.provider === "azure_openai"
);
const openaiModelGroup = allAllowedModels.filter(
(m) => m.config.provider === "openai"
);
const ollamaModelGroup = allAllowedModels.filter(
(m) => m.config.provider === "ollama"
);
const anthropicModelGroup = allAllowedModels.filter(
(m) => m.config.provider === "anthropic"
);
const genAiModelGroup = allAllowedModels.filter(
(m) => m.config.provider === "google-genai"
);
const fireworksModelGroup = allAllowedModels.filter(
(m) => m.config.provider === "fireworks"
);
const groqModelGroup = allAllowedModels.filter(
(m) => m.config.provider === "groq"
);
return (
<div className="relative">
<Popover open={open} onOpenChange={setOpen}>
@@ -147,55 +246,146 @@ export default function ModelSelector({
<PopoverContent className="min-w-[180px] w-[280px] p-0 shadow-md rounded-md">
<Command>
<CommandList>
{allAllowedModels.map((model) => {
const config =
modelConfigs[model.name] ||
modelConfigs[model.name.replace("azure/", "")];
return (
<CommandGroup key={model.name} className="w-full">
<CommandItem
value={model.name}
onSelect={handleModelChange}
className="flex items-center"
>
<Check
className={cn(
"mr-1 size-4",
modelName === model.name ? "opacity-100" : "opacity-0"
)}
{openaiModelGroup.length > 0 && (
<CommandGroup heading="OpenAI" className="w-full">
{openaiModelGroup.map((model) => {
const config = modelConfigs[model.name];
return (
<CommandModelItem
key={model.name}
model={model}
handleModelChange={handleModelChange}
config={config}
selectedModelName={modelName}
openConfigModelId={openConfigModelId}
setOpenConfigModelId={setOpenConfigModelId}
setModelConfig={setModelConfig}
/>
<span className="flex flex-row w-full items-center justify-start gap-2">
{model.label}
{model.isNew && <IsNewBadge />}
</span>
);
})}
</CommandGroup>
)}
{openConfigModelId === model.name ? (
<ModelConfigPanel
model={model}
modelConfig={config}
isOpen={true}
onOpenChange={(open) =>
setOpenConfigModelId(open ? model.name : null)
}
onClick={(e) => e.stopPropagation()}
setModelConfig={setModelConfig}
/>
) : (
<button
className="ml-auto flex-shrink-0 flex size-6 items-center justify-center focus:outline-none focus:ring-0"
onClick={(e) => {
e.stopPropagation();
setOpenConfigModelId(model.name);
}}
>
<GearIcon className="size-4" />
</button>
)}
</CommandItem>
</CommandGroup>
);
})}
{azureModelGroup.length > 0 && (
<CommandGroup heading="Azure OpenAI" className="w-full">
{azureModelGroup.map((model) => {
const config =
modelConfigs[model.name.replace("azure/", "")];
return (
<CommandModelItem
key={model.name}
model={model}
handleModelChange={handleModelChange}
config={config}
selectedModelName={modelName}
openConfigModelId={openConfigModelId}
setOpenConfigModelId={setOpenConfigModelId}
setModelConfig={setModelConfig}
/>
);
})}
</CommandGroup>
)}
{anthropicModelGroup.length > 0 && (
<CommandGroup heading="Anthropic" className="w-full">
{anthropicModelGroup.map((model) => {
const config = modelConfigs[model.name];
return (
<CommandModelItem
key={model.name}
model={model}
handleModelChange={handleModelChange}
config={config}
selectedModelName={modelName}
openConfigModelId={openConfigModelId}
setOpenConfigModelId={setOpenConfigModelId}
setModelConfig={setModelConfig}
/>
);
})}
</CommandGroup>
)}
{genAiModelGroup.length > 0 && (
<CommandGroup heading="Google GenAI" className="w-full">
{genAiModelGroup.map((model) => {
const config = modelConfigs[model.name];
return (
<CommandModelItem
key={model.name}
model={model}
handleModelChange={handleModelChange}
config={config}
selectedModelName={modelName}
openConfigModelId={openConfigModelId}
setOpenConfigModelId={setOpenConfigModelId}
setModelConfig={setModelConfig}
/>
);
})}
</CommandGroup>
)}
{groqModelGroup.length > 0 && (
<CommandGroup heading="Groq" className="w-full">
{groqModelGroup.map((model) => {
const config = modelConfigs[model.name];
return (
<CommandModelItem
key={model.name}
model={model}
handleModelChange={handleModelChange}
config={config}
selectedModelName={modelName}
openConfigModelId={openConfigModelId}
setOpenConfigModelId={setOpenConfigModelId}
setModelConfig={setModelConfig}
/>
);
})}
</CommandGroup>
)}
{fireworksModelGroup.length > 0 && (
<CommandGroup heading="Fireworks" className="w-full">
{fireworksModelGroup.map((model) => {
const config = modelConfigs[model.name];
return (
<CommandModelItem
key={model.name}
model={model}
handleModelChange={handleModelChange}
config={config}
selectedModelName={modelName}
openConfigModelId={openConfigModelId}
setOpenConfigModelId={setOpenConfigModelId}
setModelConfig={setModelConfig}
/>
);
})}
</CommandGroup>
)}
{ollamaModelGroup.length > 0 && (
<CommandGroup heading="Ollama" className="w-full">
{ollamaModelGroup.map((model) => {
const config = modelConfigs[model.name];
return (
<CommandModelItem
key={model.name}
model={model}
handleModelChange={handleModelChange}
config={config}
selectedModelName={modelName}
openConfigModelId={openConfigModelId}
setOpenConfigModelId={setOpenConfigModelId}
setModelConfig={setModelConfig}
/>
);
})}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
@@ -238,7 +238,7 @@ export function ThreadHistoryComponent(props: ThreadHistoryProps) {
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<TooltipIconButton
tooltip="New chat"
tooltip="History"
variant="ghost"
className="w-fit h-fit p-2"
>
+4 -1
View File
@@ -68,6 +68,7 @@ export const Thread: FC<ThreadProps> = (props: ThreadProps) => {
} = useThreadContext();
const { user } = useUserContext();
// Render the LangSmith trace link
useLangSmithLinkToolUI();
const handleCreateThread = async () => {
@@ -98,7 +99,9 @@ export const Thread: FC<ThreadProps> = (props: ThreadProps) => {
setModelConfig(modelName, modelConfig);
clearState();
setChatStarted(false);
const thread = await searchOrCreateThread();
// Set `true` for `isNewThread` because we want to create a new thread
// if the existing one has values.
const thread = await searchOrCreateThread(true);
if (!thread) {
toast({
title: "Failed to create a new thread",
+56
View File
@@ -0,0 +1,56 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { cn } from "@/lib/utils";
import { ChevronDownIcon } from "@radix-ui/react-icons";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
+52 -4
View File
@@ -246,9 +246,29 @@ export const ANTHROPIC_MODELS = [
];
export const FIREWORKS_MODELS: ModelConfigurationParams[] = [
{
name: "accounts/fireworks/models/llama-v3p3-70b-instruct",
label: "Llama 3.3 70B",
config: {
provider: "fireworks",
temperatureRange: {
min: 0,
max: 1,
default: 0.5,
current: 0.5,
},
maxTokens: {
min: 1,
max: 16384,
default: 4096,
current: 4096,
},
},
isNew: true,
},
{
name: "accounts/fireworks/models/llama-v3p1-70b-instruct",
label: "Llama 70B (Fireworks)",
label: "Llama 70B (old)",
config: {
provider: "fireworks",
temperatureRange: {
@@ -268,7 +288,7 @@ export const FIREWORKS_MODELS: ModelConfigurationParams[] = [
},
{
name: "accounts/fireworks/models/deepseek-v3",
label: "DeepSeek V3 (Fireworks)",
label: "DeepSeek V3",
config: {
provider: "fireworks",
temperatureRange: {
@@ -288,7 +308,7 @@ export const FIREWORKS_MODELS: ModelConfigurationParams[] = [
},
{
name: "accounts/fireworks/models/deepseek-r1",
label: "DeepSeek R1 (Fireworks)",
label: "DeepSeek R1",
config: {
provider: "fireworks",
temperatureRange: {
@@ -308,6 +328,29 @@ export const FIREWORKS_MODELS: ModelConfigurationParams[] = [
},
];
export const GROQ_MODELS: ModelConfigurationParams[] = [
{
name: "groq/deepseek-r1-distill-llama-70b",
label: "DeepSeek R1 Llama 70b Distill",
config: {
provider: "groq",
temperatureRange: {
min: 0,
max: 1,
default: 0.5,
current: 0.5,
},
maxTokens: {
min: 1,
max: 8000,
default: 4096,
current: 4096,
},
},
isNew: true,
},
];
export const GEMINI_MODELS: ModelConfigurationParams[] = [
{
name: "gemini-1.5-flash",
@@ -392,6 +435,8 @@ export const NON_STREAMING_TEXT_MODELS = [
"o1",
"gemini-2.0-flash-thinking-exp-01-21",
];
// Models which preform CoT before generating a final response.
export const THINKING_MODELS = ["accounts/fireworks/models/deepseek-r1"];
export const ALL_MODELS: ModelConfigurationParams[] = [
...OPENAI_MODELS,
@@ -400,6 +445,7 @@ export const ALL_MODELS: ModelConfigurationParams[] = [
...GEMINI_MODELS,
...AZURE_MODELS,
...OLLAMA_MODELS,
...GROQ_MODELS,
];
export type OPENAI_MODEL_NAMES = (typeof OPENAI_MODELS)[number]["name"];
@@ -408,13 +454,15 @@ export type FIREWORKS_MODEL_NAMES = (typeof FIREWORKS_MODELS)[number]["name"];
export type GEMINI_MODEL_NAMES = (typeof GEMINI_MODELS)[number]["name"];
export type AZURE_MODEL_NAMES = (typeof AZURE_MODELS)[number]["name"];
export type OLLAMA_MODEL_NAMES = (typeof OLLAMA_MODELS)[number]["name"];
export type GROQ_MODEL_NAMES = (typeof GROQ_MODELS)[number]["name"];
export type ALL_MODEL_NAMES =
| OPENAI_MODEL_NAMES
| ANTHROPIC_MODEL_NAMES
| FIREWORKS_MODEL_NAMES
| GEMINI_MODEL_NAMES
| AZURE_MODEL_NAMES
| OLLAMA_MODEL_NAMES;
| OLLAMA_MODEL_NAMES
| GROQ_MODEL_NAMES;
export const DEFAULT_MODEL_NAME: ALL_MODEL_NAMES = OPENAI_MODELS[0].name;
export const DEFAULT_MODEL_CONFIG: CustomModelConfig = {
+44 -8
View File
@@ -1,3 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { useUserContext } from "@/contexts/UserContext";
import {
isArtifactCodeContent,
@@ -46,6 +47,8 @@ import {
import {
convertToArtifactV3,
handleGenerateArtifactToolCallChunk,
handleRewriteArtifactThinkingModel,
isThinkingModel,
removeCodeBlockFormatting,
replaceOrInsertMessageChunk,
updateHighlightedCode,
@@ -374,7 +377,8 @@ export function GraphProvider({ children }: { children: ReactNode }) {
// The root level run ID of this stream
let runId = "";
let followupMessageId = "";
// let lastMessage: AIMessage | undefined = undefined;
// The ID of the message containing the thinking content.
let thinkingMessageId = "";
try {
const stream = client.runs.stream(
@@ -418,12 +422,17 @@ export function GraphProvider({ children }: { children: ReactNode }) {
// Whether or not the first update has been made when updating highlighted code.
let isFirstUpdate = true;
// The new text of an artifact that is being rewritten
// The full text content of an artifact that is being rewritten.
// This may include thinking tokens if the model generates them.
let fullNewArtifactContent = "";
// The response text ONLY of the artifact that is being rewritten.
let newArtifactContent = "";
// The updated full markdown text when using the highlight update tool
let highlightedText: TextHighlight | undefined = undefined;
// The message ID of the message containing the thinking content.
for await (const chunk of stream) {
if (chunk.event === "error") {
const errorMessage =
@@ -669,9 +678,22 @@ export function GraphProvider({ children }: { children: ReactNode }) {
return;
}
newArtifactContent +=
fullNewArtifactContent +=
extractStreamDataChunk(chunk.data.data.chunk)?.content || "";
if (isThinkingModel(threadData.modelName)) {
if (!thinkingMessageId) {
thinkingMessageId = `thinking-${uuidv4()}`;
}
newArtifactContent = handleRewriteArtifactThinkingModel({
newArtifactContent: fullNewArtifactContent,
setMessages,
thinkingMessageId,
});
} else {
newArtifactContent = fullNewArtifactContent;
}
// Ensure we have the language to update the artifact with
let artifactLanguage = params.portLanguage || undefined;
if (
@@ -749,9 +771,23 @@ export function GraphProvider({ children }: { children: ReactNode }) {
return;
}
newArtifactContent +=
fullNewArtifactContent +=
extractStreamDataChunk(chunk.data.data.chunk)?.content || "";
if (isThinkingModel(threadData.modelName)) {
if (!thinkingMessageId) {
thinkingMessageId = `thinking-${uuidv4()}`;
}
console.log("Extracting thinking message");
newArtifactContent = handleRewriteArtifactThinkingModel({
newArtifactContent: fullNewArtifactContent,
setMessages,
thinkingMessageId,
});
} else {
newArtifactContent = fullNewArtifactContent;
}
// Ensure we have the language to update the artifact with
const artifactLanguage =
params.portLanguage ||
@@ -820,7 +856,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
const message = extractStreamDataOutput(chunk.data.data.output);
newArtifactContent += message.content || "";
fullNewArtifactContent += message.content || "";
// Ensure we have the language to update the artifact with
let artifactLanguage = params.portLanguage || undefined;
@@ -846,7 +882,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
throw new Error("No artifact found when updating markdown");
}
let content = newArtifactContent;
let content = fullNewArtifactContent;
if (!rewriteArtifactMeta) {
console.error(
"No rewrite artifact meta found when updating artifact"
@@ -1074,7 +1110,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
return;
}
const message = extractStreamDataOutput(chunk.data.data.output);
newArtifactContent += message?.content || "";
fullNewArtifactContent += message?.content || "";
// Ensure we have the language to update the artifact with
const artifactLanguage =
@@ -1099,7 +1135,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
throw new Error("No artifact found when updating markdown");
}
let content = newArtifactContent;
let content = fullNewArtifactContent;
if (artifactType === "code") {
content = removeCodeBlockFormatting(content);
}
+12 -5
View File
@@ -25,7 +25,7 @@ type ThreadContentType = {
modelConfigs: Record<ALL_MODEL_NAMES, CustomModelConfig>;
createThreadLoading: boolean;
clearThreadsWithNoValues: () => Promise<void>;
searchOrCreateThread: () => Promise<Thread | undefined>;
searchOrCreateThread: (isNewThread?: boolean) => Promise<Thread | undefined>;
getUserThreads: () => Promise<void>;
deleteThread: (id: string, clearMessages: () => void) => Promise<void>;
setThreadId: (id: string) => void;
@@ -87,7 +87,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
});
const modelConfig = useMemo(() => {
// Try exact match first, then try without azure/ prefix
// Try exact match first, then try without "azure/" or "groq/" prefixes
return (
modelConfigs[modelName] || modelConfigs[modelName.replace("azure/", "")]
);
@@ -215,7 +215,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
}
};
const searchOrCreateThread = async () => {
const searchOrCreateThread = async (isNewThread?: boolean) => {
const storedThreadId =
searchParams.get(THREAD_ID_QUERY_PARAM) ||
getCookie(THREAD_ID_COOKIE_NAME);
@@ -228,8 +228,15 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
// Thread ID is in cookies.
const thread = await getThreadById(storedThreadId);
if (thread) {
setThreadId(storedThreadId);
return thread;
const isEmptyThread =
!thread.values || Object.keys(thread.values).length === 0;
const shouldUseExistingThread =
!isNewThread || (isNewThread && isEmptyThread);
if (shouldUseExistingThread) {
setThreadId(storedThreadId);
return thread;
}
}
// Current thread has activity. Create a new thread.
+132
View File
@@ -1,3 +1,4 @@
import { THINKING_MODELS } from "@/constants";
import { cleanContent } from "@/lib/normalize_string";
import {
Artifact,
@@ -14,6 +15,7 @@ import {
BaseMessageChunk,
} from "@langchain/core/messages";
import { parsePartialJson } from "@langchain/core/output_parsers";
import { Dispatch, SetStateAction } from "react";
export function removeCodeBlockFormatting(text: string): string {
if (!text) return text;
@@ -408,3 +410,133 @@ export function handleGenerateArtifactToolCallChunk(toolCallChunkArgs: string) {
};
}
}
type ThinkingAndResponseTokens = {
thinking: string;
response: string;
};
/**
* Extracts thinking and response content from a text string containing XML-style think tags.
* Designed to handle streaming text where tags might be incomplete.
*
* @param text - The input text that may contain <think> tags
* @returns An object containing:
* - thinking: Content between <think> tags, or all content after <think> if no closing tag
* - response: All text outside of think tags
*
* @example
* // Complete tags
* extractThinkingAndResponseTokens('Hello <think>processing...</think>world')
* // Returns: { thinking: 'processing...', response: 'Hello world' }
*
* // Streaming/incomplete tags
* extractThinkingAndResponseTokens('Hello <think>processing...')
* // Returns: { thinking: 'processing...', response: 'Hello ' }
*
* // No tags
* extractThinkingAndResponseTokens('Hello world')
* // Returns: { thinking: '', response: 'Hello world' }
*/
export function extractThinkingAndResponseTokens(
text: string
): ThinkingAndResponseTokens {
const thinkStartTag = "<think>";
const thinkEndTag = "</think>";
const startIndex = text.indexOf(thinkStartTag);
// No thinking tag found
if (startIndex === -1) {
return {
thinking: "",
response: text.trim(),
};
}
const afterStartTag = text.substring(startIndex + thinkStartTag.length);
const endIndex = afterStartTag.indexOf(thinkEndTag);
// If no closing tag, all remaining text is thinking
if (endIndex === -1) {
return {
thinking: afterStartTag.trim(),
response: text.substring(0, startIndex).trim(),
};
}
// We have both opening and closing tags
const thinking = afterStartTag.substring(0, endIndex).trim();
const response = (
text.substring(0, startIndex) +
afterStartTag.substring(endIndex + thinkEndTag.length)
).trim();
return {
thinking,
response,
};
}
type HandleRewriteArtifactThinkingModelParams = {
newArtifactContent: string;
setMessages: Dispatch<SetStateAction<BaseMessage[]>>;
thinkingMessageId: string;
};
/**
* Handles the rewriting of artifact content by processing thinking tokens and updating messages state.
* This function extracts thinking and response tokens from the new artifact content, updates the message
* state with thinking tokens if present, and returns the response content.
*
* @param {Object} params - The parameters for handling artifact rewriting
* @param {string} params.newArtifactContent - The new content to process for the artifact
* @param {Dispatch<SetStateAction<BaseMessage[]>>} params.setMessages - State setter function for updating messages
* @param {string} params.thinkingMessageId - Unique identifier for the thinking message to update or create
* @returns {string} The extracted response content from the artifact
*/
export function handleRewriteArtifactThinkingModel({
newArtifactContent,
setMessages,
thinkingMessageId,
}: HandleRewriteArtifactThinkingModelParams): string {
const { thinking, response } =
extractThinkingAndResponseTokens(newArtifactContent);
if (thinking.length > 0) {
setMessages((prevMessages) => {
if (!thinkingMessageId) {
console.error("Thinking message not found");
return prevMessages;
}
const prevHasThinkingMsg = prevMessages.some(
(msg) => msg.id === thinkingMessageId
);
const thinkingMessage = new AIMessage({
id: thinkingMessageId,
content: thinking,
});
if (prevHasThinkingMsg) {
// The message exists, so replace it
const newMsgs = prevMessages.map((msg) => {
if (msg.id !== thinkingMessageId) {
return msg;
}
return thinkingMessage;
});
return newMsgs;
}
// The message does not yet exist, so create it:
return [...prevMessages, thinkingMessage];
});
}
return response;
}
export function isThinkingModel(model: string): boolean {
return THINKING_MODELS.some((m) => m === model);
}
+75 -57
View File
@@ -9,34 +9,52 @@ const config: Config = {
],
theme: {
extend: {
keyframes: {
'gradient-xy-enhanced': {
'0%, 100%': {
'background-size': '400% 400%',
'background-position': 'left center',
'transform': 'rotate(-3deg)'
},
'50%': {
'background-size': '200% 200%',
'background-position': 'right center',
'transform': 'rotate(3deg)'
}
},
'gradient-x': {
'0%, 100%': {
'background-size': '200% 200%',
'background-position': 'left center',
},
'50%': {
'background-size': '200% 200%',
'background-position': 'right center',
},
},
},
animation: {
'gradient-xy-enhanced': 'gradient-xy-enhanced 15s ease infinite',
'gradient-x': 'gradient-x 3s ease-in-out infinite',
},
keyframes: {
'gradient-xy-enhanced': {
'0%, 100%': {
'background-size': '400% 400%',
'background-position': 'left center',
transform: 'rotate(-3deg)'
},
'50%': {
'background-size': '200% 200%',
'background-position': 'right center',
transform: 'rotate(3deg)'
}
},
'gradient-x': {
'0%, 100%': {
'background-size': '200% 200%',
'background-position': 'left center'
},
'50%': {
'background-size': '200% 200%',
'background-position': 'right center'
}
},
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
}
},
animation: {
'gradient-xy-enhanced': 'gradient-xy-enhanced 15s ease infinite',
'gradient-x': 'gradient-x 3s ease-in-out infinite',
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))'
@@ -88,35 +106,35 @@ const config: Config = {
'5': 'hsl(var(--chart-5))'
}
},
fontFamily: {
mono: [
`"Fira Code"`,
`ui-monospace`,
`SFMono-Regular`,
`Menlo`,
`Monaco`,
`Consolas`,
`"Liberation Mono"`,
`"Courier New"`,
`monospace`,
],
sans: [
"Inter",
"-apple-system",
"BlinkMacSystemFont",
'Segoe UI',
"Roboto",
"Helvetica",
"Arial",
"sans-serif",
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol',
],
},
letterSpacing: {
tighter: '-0.04em',
},
fontFamily: {
mono: [
'`"Fira Code"`',
'`ui-monospace`',
'`SFMono-Regular`',
'`Menlo`',
'`Monaco`',
'`Consolas`',
'`"Liberation Mono"`',
'`"Courier New"`',
'`monospace`'
],
sans: [
'Inter',
'-apple-system',
'BlinkMacSystemFont',
'Segoe UI',
'Roboto',
'Helvetica',
'Arial',
'sans-serif',
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol'
]
},
letterSpacing: {
tighter: '-0.04em'
}
}
},
plugins: [
+68
View File
@@ -881,6 +881,16 @@
"@google/generative-ai" "^0.21.0"
zod-to-json-schema "^3.22.4"
"@langchain/groq@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@langchain/groq/-/groq-0.1.3.tgz#02a554c4cb779b4b5087d8e9b497f634012f7092"
integrity sha512-dMzvBVaLf/0IQoHdAOAN8W/PbOcwgbvgUMCn02CqvCC90mxZ45LI0Tipzqnoaam0hiKALR5hLc3dNj1oCYV92w==
dependencies:
"@langchain/openai" "~0.3.0"
groq-sdk "^0.5.0"
zod "^3.22.4"
zod-to-json-schema "^3.22.5"
"@langchain/langgraph-checkpoint@~0.0.13":
version "0.0.13"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.13.tgz#b349f1518184e1b154227d1fead2bec1e27707fd"
@@ -936,6 +946,16 @@
zod "^3.22.4"
zod-to-json-schema "^3.22.3"
"@langchain/openai@~0.3.0":
version "0.3.17"
resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-0.3.17.tgz#5b8613ac8d849da90f3ecd8368ae7389fca4ee13"
integrity sha512-uw4po32OKptVjq+CYHrumgbfh4NuD7LqyE+ZgqY9I/LrLc6bHLMc+sisHmI17vgek0K/yqtarI0alPJbzrwyag==
dependencies:
js-tiktoken "^1.0.12"
openai "^4.77.0"
zod "^3.22.4"
zod-to-json-schema "^3.22.3"
"@langchain/textsplitters@>=0.0.0 <0.2.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@langchain/textsplitters/-/textsplitters-0.1.0.tgz#f37620992192df09ecda3dfbd545b36a6bcbae46"
@@ -1215,6 +1235,21 @@
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3"
integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==
"@radix-ui/react-accordion@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz#96ac3de896189553219e342d5e773589eb119dce"
integrity sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-collapsible" "1.1.2"
"@radix-ui/react-collection" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-arrow@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a"
@@ -1253,6 +1288,20 @@
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/react-collapsible@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz#42477c428bb0d2eec35b9b47601c5ff0a6210165"
integrity sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-collection@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed"
@@ -4573,6 +4622,20 @@ groq-sdk@^0.13.0:
formdata-node "^4.3.2"
node-fetch "^2.6.7"
groq-sdk@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/groq-sdk/-/groq-sdk-0.5.0.tgz#8eefea81c3709e815c96dffa941200e85a50cf19"
integrity sha512-RVmhW7qZ+XZoy5fIuSdx/LGQJONpL8MHgZEW7dFwTdgkzStub2XQx6OKv28CHogijdwH41J+Npj/z2jBPu3vmw==
dependencies:
"@types/node" "^18.11.18"
"@types/node-fetch" "^2.6.4"
abort-controller "^3.0.0"
agentkeepalive "^4.2.1"
form-data-encoder "1.7.2"
formdata-node "^4.3.2"
node-fetch "^2.6.7"
web-streams-polyfill "^3.2.1"
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
@@ -8998,6 +9061,11 @@ web-streams-polyfill@4.0.0-beta.3:
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38"
integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==
web-streams-polyfill@^3.2.1:
version "3.3.3"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b"
integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"