Skip to content

Commit d0d30ac

Browse files
committed
Add feature to create a custom agent directly from the side panel with currently configured settings
- Also, when in not subscribed state, fallback to the default model when chatting with an agent - With conversion, create a brand new agent from inside the chat view that can be managed separately
1 parent 5d6eca4 commit d0d30ac

File tree

7 files changed

+279
-12
lines changed

7 files changed

+279
-12
lines changed

src/interface/web/app/agents/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { AppSidebar } from "../components/appSidebar/appSidebar";
3535
import { Separator } from "@/components/ui/separator";
3636
import { KhojLogoType } from "../components/logo/khojLogo";
3737
import { DialogTitle } from "@radix-ui/react-dialog";
38+
import Link from "next/link";
3839

3940
const agentsFetcher = () =>
4041
window
@@ -343,6 +344,14 @@ export default function Agents() {
343344
/>
344345
<span className="font-bold">How it works</span> Use any of these
345346
specialized personas to tune your conversation to your needs.
347+
{
348+
!isSubscribed && (
349+
<span>
350+
{" "}
351+
<Link href="/settings" className="font-bold">Upgrade your plan</Link> to leverage custom models. You will fallback to the default model when chatting.
352+
</span>
353+
)
354+
}
346355
</AlertDescription>
347356
</Alert>
348357
<div className="pt-6 md:pt-8">

src/interface/web/app/components/agentCard/agentCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ export function AgentCard(props: AgentCardProps) {
453453
/>
454454
</DialogContent>
455455
) : (
456-
<DialogContent className="whitespace-pre-line max-h-[80vh] max-w-[90vw] rounded-lg">
456+
<DialogContent className="whitespace-pre-line max-h-[80vh] max-w-[90vw] md:max-w-[50vw] rounded-lg">
457457
<DialogHeader>
458458
<div className="flex items-center">
459459
{getIconFromIconName(props.data.icon, props.data.color)}

src/interface/web/app/components/chatSidebar/chatSidebar.tsx

Lines changed: 258 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22

3-
import { ArrowsDownUp, CaretCircleDown, CircleNotch, Sparkle } from "@phosphor-icons/react";
3+
import { ArrowsDownUp, CaretCircleDown, CheckCircle, Circle, CircleNotch, PersonSimpleTaiChi, Sparkle } from "@phosphor-icons/react";
44

55
import { Button } from "@/components/ui/button";
66

@@ -14,13 +14,20 @@ import { mutate } from "swr";
1414
import { Sheet, SheetContent } from "@/components/ui/sheet";
1515
import { AgentData } from "../agentCard/agentCard";
1616
import { useEffect, useState } from "react";
17-
import { getIconForSlashCommand, getIconFromIconName } from "@/app/common/iconUtils";
17+
import { getAvailableIcons, getIconForSlashCommand, getIconFromIconName } from "@/app/common/iconUtils";
1818
import { Label } from "@/components/ui/label";
1919
import { Checkbox } from "@/components/ui/checkbox";
2020
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
2121
import { TooltipContent } from "@radix-ui/react-tooltip";
2222
import { useAuthenticatedData } from "@/app/common/auth";
2323
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
24+
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
25+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
26+
import { convertColorToTextClass, tailwindColors } from "@/app/common/colorUtils";
27+
import { Input } from "@/components/ui/input";
28+
import Link from "next/link";
29+
import { motion } from "framer-motion";
30+
2431

2532
interface ChatSideBarProps {
2633
conversationId: string;
@@ -54,11 +61,245 @@ export function ChatSidebar({ ...props }: ChatSideBarProps) {
5461
);
5562
}
5663

64+
interface IAgentCreationProps {
65+
customPrompt: string;
66+
selectedModel: string;
67+
inputTools: string[];
68+
outputModes: string[];
69+
}
70+
71+
interface AgentError {
72+
detail: string;
73+
}
74+
75+
function AgentCreationForm(props: IAgentCreationProps) {
76+
const iconOptions = getAvailableIcons();
77+
const colorOptions = tailwindColors;
78+
79+
const [isCreating, setIsCreating] = useState<boolean>(false);
80+
const [customAgentName, setCustomAgentName] = useState<string | undefined>();
81+
const [customAgentIcon, setCustomAgentIcon] = useState<string | undefined>();
82+
const [customAgentColor, setCustomAgentColor] = useState<string | undefined>();
83+
84+
const [doneCreating, setDoneCreating] = useState<boolean>(false);
85+
const [createdSlug, setCreatedSlug] = useState<string | undefined>();
86+
const [isValid, setIsValid] = useState<boolean>(false);
87+
const [error, setError] = useState<string | undefined>();
88+
89+
function createAgent() {
90+
if (isCreating) {
91+
return;
92+
}
93+
94+
setIsCreating(true);
95+
96+
const data = {
97+
name: customAgentName,
98+
icon: customAgentIcon,
99+
color: customAgentColor,
100+
persona: props.customPrompt,
101+
chat_model: props.selectedModel,
102+
input_tools: props.inputTools,
103+
output_modes: props.outputModes,
104+
privacy_level: "private",
105+
};
106+
107+
const createAgentUrl = `/api/agents`;
108+
109+
fetch(createAgentUrl, {
110+
method: "POST",
111+
headers: {
112+
"Content-Type": "application/json"
113+
},
114+
body: JSON.stringify(data)
115+
})
116+
.then((res) => res.json())
117+
.then((data: AgentData | AgentError) => {
118+
console.log("Success:", data);
119+
if ('detail' in data) {
120+
setError(`Error creating agent: ${data.detail}`);
121+
setIsCreating(false);
122+
return;
123+
}
124+
setDoneCreating(true);
125+
setCreatedSlug(data.slug);
126+
setIsCreating(false);
127+
})
128+
.catch((error) => {
129+
console.error("Error:", error);
130+
setError(`Error creating agent: ${error}`);
131+
setIsCreating(false);
132+
});
133+
}
134+
135+
useEffect(() => {
136+
if (customAgentName && customAgentIcon && customAgentColor) {
137+
setIsValid(true);
138+
} else {
139+
setIsValid(false);
140+
}
141+
}, [customAgentName, customAgentIcon, customAgentColor]);
142+
143+
return (
144+
145+
<Dialog>
146+
<DialogTrigger asChild>
147+
<Button
148+
className="p-1"
149+
variant="ghost"
150+
>
151+
Create Agent
152+
</Button>
153+
</DialogTrigger>
154+
<DialogContent>
155+
<DialogHeader>
156+
{
157+
doneCreating && createdSlug ? (
158+
<DialogTitle>
159+
Created {customAgentName}
160+
</DialogTitle>
161+
) : (
162+
<DialogTitle>
163+
Create a New Agent
164+
</DialogTitle>
165+
)
166+
}
167+
<DialogClose />
168+
</DialogHeader>
169+
<div className="py-4">
170+
{
171+
doneCreating && createdSlug ? (
172+
<div className="flex flex-col items-center justify-center gap-4 py-8">
173+
<motion.div
174+
initial={{ scale: 0 }}
175+
animate={{ scale: 1 }}
176+
transition={{
177+
type: "spring",
178+
stiffness: 260,
179+
damping: 20
180+
}}
181+
>
182+
<CheckCircle
183+
className="w-16 h-16 text-green-500"
184+
weight="fill"
185+
/>
186+
</motion.div>
187+
<motion.p
188+
initial={{ opacity: 0, y: 10 }}
189+
animate={{ opacity: 1, y: 0 }}
190+
transition={{ delay: 0.2 }}
191+
className="text-center text-lg font-medium text-accent-foreground"
192+
>
193+
Created successfully!
194+
</motion.p>
195+
<motion.div
196+
initial={{ opacity: 0, y: 10 }}
197+
animate={{ opacity: 1, y: 0 }}
198+
transition={{ delay: 0.4 }}
199+
>
200+
<Link href={`/agents?agent=${createdSlug}`}>
201+
<Button variant="secondary" className="mt-2">
202+
Manage Agent
203+
</Button>
204+
</Link>
205+
</motion.div>
206+
</div>
207+
) :
208+
<div className="flex flex-col gap-4">
209+
<div>
210+
<Label htmlFor="agent_name">Name</Label>
211+
<Input
212+
id="agent_name"
213+
className="w-full p-2 border mt-4 border-slate-500 rounded-lg"
214+
disabled={isCreating}
215+
value={customAgentName}
216+
onChange={(e) => setCustomAgentName(e.target.value)}
217+
/>
218+
</div>
219+
<div className="flex gap-4">
220+
<div className="flex-1">
221+
<Select onValueChange={setCustomAgentColor} defaultValue={customAgentColor}>
222+
<SelectTrigger className="w-full dark:bg-muted" disabled={isCreating}>
223+
<SelectValue placeholder="Color" />
224+
</SelectTrigger>
225+
<SelectContent className="items-center space-y-1 inline-flex flex-col">
226+
{colorOptions.map((colorOption) => (
227+
<SelectItem key={colorOption} value={colorOption}>
228+
<div className="flex items-center space-x-2">
229+
<Circle
230+
className={`w-6 h-6 mr-2 ${convertColorToTextClass(colorOption)}`}
231+
weight="fill"
232+
/>
233+
{colorOption}
234+
</div>
235+
</SelectItem>
236+
))}
237+
</SelectContent>
238+
</Select>
239+
</div>
240+
<div className="flex-1">
241+
<Select onValueChange={setCustomAgentIcon} defaultValue={customAgentIcon}>
242+
<SelectTrigger className="w-full dark:bg-muted" disabled={isCreating}>
243+
<SelectValue placeholder="Icon" />
244+
</SelectTrigger>
245+
<SelectContent className="items-center space-y-1 inline-flex flex-col">
246+
{iconOptions.map((iconOption) => (
247+
<SelectItem key={iconOption} value={iconOption}>
248+
<div className="flex items-center space-x-2">
249+
{getIconFromIconName(
250+
iconOption,
251+
customAgentColor ?? "gray",
252+
"w-6",
253+
"h-6",
254+
)}
255+
{iconOption}
256+
</div>
257+
</SelectItem>
258+
))}
259+
</SelectContent>
260+
</Select>
261+
</div>
262+
</div>
263+
</div>
264+
}
265+
</div>
266+
<DialogFooter>
267+
{
268+
error && (
269+
<div className="text-red-500 text-sm">
270+
{error}
271+
</div>
272+
)
273+
}
274+
{
275+
!doneCreating && (
276+
<Button
277+
type="submit"
278+
onClick={() => createAgent()}
279+
disabled={isCreating || !isValid}
280+
>
281+
{
282+
isCreating ?
283+
<CircleNotch className="animate-spin" />
284+
:
285+
<PersonSimpleTaiChi />
286+
}
287+
Create
288+
</Button>
289+
)
290+
}
291+
<DialogClose />
292+
</DialogFooter>
293+
</DialogContent>
294+
</Dialog >
295+
296+
)
297+
}
57298

58299
function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
59300
const [isEditable, setIsEditable] = useState<boolean>(false);
60301
const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } =
61-
useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher);
302+
useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher);
62303

63304
const { data: agentData, isLoading: agentDataLoading, error: agentDataError } = useSWR<AgentData>(`/api/agents/conversation?conversation_id=${props.conversationId}`, fetcher);
64305
const {
@@ -211,9 +452,20 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
211452
</a>
212453
</div>
213454
) : (
214-
<div className="flex items-center relative text-sm">
215-
{getIconFromIconName("lightbulb", "orange")}
216-
Chat Options
455+
<div className="flex items-center relative text-sm justify-between">
456+
<p>
457+
Chat Options
458+
</p>
459+
{
460+
isEditable && customPrompt && !isDefaultAgent && selectedModel && (
461+
<AgentCreationForm
462+
customPrompt={customPrompt}
463+
selectedModel={selectedModel}
464+
inputTools={inputTools ?? []}
465+
outputModes={outputModes ?? []}
466+
/>
467+
)
468+
}
217469
</div>
218470
)
219471
}

src/khoj/database/adapters/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,8 +1356,10 @@ async def aget_conversation_starters(user: KhojUser, max_results=3):
13561356
return random.sample(all_questions, max_results)
13571357

13581358
@staticmethod
1359-
def get_valid_chat_model(user: KhojUser, conversation: Conversation):
1360-
agent: Agent = conversation.agent if AgentAdapters.get_default_agent() != conversation.agent else None
1359+
def get_valid_chat_model(user: KhojUser, conversation: Conversation, is_subscribed: bool):
1360+
agent: Agent = (
1361+
conversation.agent if is_subscribed and AgentAdapters.get_default_agent() != conversation.agent else None
1362+
)
13611363
if agent and agent.chat_model:
13621364
chat_model = conversation.agent.chat_model
13631365
else:

src/khoj/routers/api_agents.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ async def get_agent_by_conversation(
110110
conversation_id: str,
111111
) -> Response:
112112
user: KhojUser = request.user.object if request.user.is_authenticated else None
113+
is_subscribed = has_required_scope(request, ["premium"])
113114
conversation = await ConversationAdapters.aget_conversation_by_user(user=user, conversation_id=conversation_id)
114115

115116
if not conversation:
@@ -132,7 +133,7 @@ async def get_agent_by_conversation(
132133
"color": agent.style_color,
133134
"icon": agent.style_icon,
134135
"privacy_level": agent.privacy_level,
135-
"chat_model": agent.chat_model.name,
136+
"chat_model": agent.chat_model.name if is_subscribed else None,
136137
"has_files": has_files,
137138
"input_tools": agent.input_tools,
138139
"output_modes": agent.output_modes,

src/khoj/routers/api_chat.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from asgiref.sync import sync_to_async
1313
from fastapi import APIRouter, Depends, HTTPException, Request
1414
from fastapi.responses import Response, StreamingResponse
15-
from starlette.authentication import requires
15+
from starlette.authentication import has_required_scope, requires
1616

1717
from khoj.app.settings import ALLOWED_HOSTS
1818
from khoj.database.adapters import (
@@ -637,6 +637,7 @@ async def event_generator(q: str, images: list[str]):
637637
chat_metadata: dict = {}
638638
connection_alive = True
639639
user: KhojUser = request.user.object
640+
is_subscribed = has_required_scope(request, ["premium"])
640641
event_delimiter = "␃🔚␗"
641642
q = unquote(q)
642643
train_of_thought = []
@@ -1251,6 +1252,7 @@ def collect_telemetry():
12511252
generated_mermaidjs_diagram,
12521253
program_execution_context,
12531254
generated_asset_results,
1255+
is_subscribed,
12541256
tracer,
12551257
)
12561258

0 commit comments

Comments
 (0)