feat: Next iteration of ChatUI (#847)
Co-authored-by: Charles Packer <packercharles@gmail.com>
This commit is contained in:
12
chatui/package-lock.json
generated
12
chatui/package-lock.json
generated
@@ -54,6 +54,7 @@
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tslib": "^2.3.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.4.6"
|
||||
},
|
||||
@@ -16296,6 +16297,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-debounce": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.0.tgz",
|
||||
"integrity": "sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tslib": "^2.3.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.4.6"
|
||||
},
|
||||
|
||||
15
chatui/src/app/auth.tsx
Normal file
15
chatui/src/app/auth.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { useAuthStoreActions, useAuthStoreState } from './libs/auth/auth.store';
|
||||
import { useAuthQuery } from './libs/auth/use-auth.query';
|
||||
|
||||
const Auth = (props: PropsWithChildren) => {
|
||||
const result = useAuthQuery();
|
||||
const { uuid } = useAuthStoreState();
|
||||
const { setAsAuthenticated } = useAuthStoreActions();
|
||||
if (result.isSuccess && uuid !== result.data.uuid) {
|
||||
setAsAuthenticated(result.data.uuid);
|
||||
}
|
||||
return props.children;
|
||||
};
|
||||
|
||||
export default Auth;
|
||||
@@ -2,15 +2,16 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { API_BASE_URL } from '../constants';
|
||||
import { AgentMemoryUpdate } from './agent-memory-update';
|
||||
|
||||
export const useAgentMemoryUpdateMutation = () => {
|
||||
export const useAgentMemoryUpdateMutation = (userId: string | null | undefined) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (params: AgentMemoryUpdate) =>
|
||||
await fetch(API_BASE_URL + `/agents/memory`, {
|
||||
await fetch(API_BASE_URL + `/agents/memory?${userId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': ' application/json' },
|
||||
body: JSON.stringify(params),
|
||||
}).then((res) => res.json()),
|
||||
onSuccess: (res, { agent_id }) => queryClient.invalidateQueries({ queryKey: ['agents', agent_id, 'memory'] }),
|
||||
onSuccess: (res, { agent_id }) =>
|
||||
queryClient.invalidateQueries({ queryKey: [userId, 'agents', 'entry', agent_id, 'memory'] }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,12 +2,12 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { API_BASE_URL } from '../constants';
|
||||
import { AgentMemory } from './agent-memory';
|
||||
|
||||
export const useAgentMemoryQuery = (agent_id: string | null | undefined) =>
|
||||
export const useAgentMemoryQuery = (userId: string | null | undefined, agentId: string | null | undefined) =>
|
||||
useQuery({
|
||||
queryKey: ['agents', agent_id, 'memory'],
|
||||
queryKey: [userId, 'agents', 'entry', agentId, 'memory'],
|
||||
queryFn: async () =>
|
||||
(await fetch(API_BASE_URL + `/agents/memory?agent_id=${agent_id}&user_id=null`).then((res) =>
|
||||
(await fetch(API_BASE_URL + `/agents/memory?agent_id=${agentId}&user_id=${userId}`).then((res) =>
|
||||
res.json()
|
||||
)) as Promise<AgentMemory>,
|
||||
enabled: !!agent_id,
|
||||
enabled: !!userId && !!agentId,
|
||||
});
|
||||
|
||||
@@ -2,18 +2,15 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { API_BASE_URL } from '../constants';
|
||||
import { Agent } from './agent';
|
||||
|
||||
export const useAgentsCreateMutation = () => {
|
||||
export const useAgentsCreateMutation = (userId: string | null | undefined) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (params: {
|
||||
user_id: string;
|
||||
config: { name: string; human: string; persona: string; model: string };
|
||||
}) =>
|
||||
mutationFn: async (params: { name: string; human: string; persona: string; model: string }) =>
|
||||
(await fetch(API_BASE_URL + '/agents', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': ' application/json' },
|
||||
body: JSON.stringify(params),
|
||||
body: JSON.stringify({ config: params, user_id: userId }),
|
||||
}).then((res) => res.json())) as Promise<Agent>,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['agents'] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: [userId, 'agents', 'list'] }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,11 +2,12 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { API_BASE_URL } from '../constants';
|
||||
import { Agent } from './agent';
|
||||
|
||||
export const useAgentsQuery = () =>
|
||||
export const useAgentsQuery = (userId: string | null | undefined) =>
|
||||
useQuery({
|
||||
queryKey: ['agents'],
|
||||
queryKey: [userId, 'agents', 'list'],
|
||||
enabled: !!userId,
|
||||
queryFn: async () =>
|
||||
(await fetch(API_BASE_URL + '/agents?user_id=null').then((res) => res.json())) as Promise<{
|
||||
(await fetch(API_BASE_URL + `/agents?user_id=${userId}`).then((res) => res.json())) as Promise<{
|
||||
num_agents: number;
|
||||
agents: Agent[];
|
||||
}>,
|
||||
|
||||
32
chatui/src/app/libs/auth/auth.store.ts
Normal file
32
chatui/src/app/libs/auth/auth.store.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||
|
||||
export type AuthState = {
|
||||
uuid: string | null;
|
||||
};
|
||||
export type AuthActions = { setAsAuthenticated: (uuid: string) => void };
|
||||
|
||||
const useAuthStore = create(
|
||||
persist<{ auth: AuthState; actions: AuthActions }>(
|
||||
(set, get) => ({
|
||||
auth: { uuid: null },
|
||||
actions: {
|
||||
setAsAuthenticated: (uuid: string) =>
|
||||
set((prev) => ({
|
||||
...prev,
|
||||
auth: {
|
||||
uuid,
|
||||
},
|
||||
})),
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: ({ actions, ...rest }: any) => rest,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const useAuthStoreState = () => useAuthStore().auth;
|
||||
export const useAuthStoreActions = () => useAuthStore().actions;
|
||||
9
chatui/src/app/libs/auth/use-auth.query.ts
Normal file
9
chatui/src/app/libs/auth/use-auth.query.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { API_BASE_URL } from '../constants';
|
||||
|
||||
export type AuthResponse = { uuid: string };
|
||||
export const useAuthQuery = () =>
|
||||
useQuery({
|
||||
queryKey: ['auth'],
|
||||
queryFn: async () => (await fetch(API_BASE_URL + `/auth`).then((res) => res.json())) as Promise<AuthResponse>,
|
||||
});
|
||||
@@ -25,7 +25,17 @@ const useMessageStreamStore = create(
|
||||
},
|
||||
(set, get) => ({
|
||||
actions: {
|
||||
sendMessage: ({ agentId, message, role }: { agentId: string; message: string; role?: 'user' | 'system' }) => {
|
||||
sendMessage: ({
|
||||
userId,
|
||||
agentId,
|
||||
message,
|
||||
role,
|
||||
}: {
|
||||
userId: string;
|
||||
agentId: string;
|
||||
message: string;
|
||||
role?: 'user' | 'system';
|
||||
}) => {
|
||||
const abortController = new AbortController();
|
||||
set((state) => ({ ...state, abortController, readyState: ReadyState.LOADING }));
|
||||
const onMessageCallback = get().onMessageCallback;
|
||||
@@ -41,7 +51,7 @@ const useMessageStreamStore = create(
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
||||
body: JSON.stringify({
|
||||
user_id: 'null',
|
||||
user_id: userId,
|
||||
agent_id: agentId,
|
||||
message,
|
||||
role: role ?? 'user',
|
||||
|
||||
19
chatui/src/app/libs/messages/use-messages.query.ts
Normal file
19
chatui/src/app/libs/messages/use-messages.query.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { API_BASE_URL } from '../constants';
|
||||
|
||||
export const useMessagesQuery = (
|
||||
userId: string | null | undefined,
|
||||
agentId: string | null | undefined,
|
||||
start = 0,
|
||||
count = 10
|
||||
) =>
|
||||
useQuery({
|
||||
queryKey: [userId, 'agents', 'item', agentId, 'messages', 'list', start, count],
|
||||
queryFn: async () =>
|
||||
(await fetch(
|
||||
API_BASE_URL + `/agents/message?agent_id=${agentId}&user_id=${userId}&start=${start}&count=${count}`
|
||||
).then((res) => res.json())) as Promise<{
|
||||
messages: { role: string; name: string; content: string }[];
|
||||
}>,
|
||||
enabled: !!userId && !!agentId,
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useAgentActions, useCurrentAgent, useLastAgentInitMessage } from '../../libs/agents/agent.store';
|
||||
import { useAuthStoreState } from '../../libs/auth/auth.store';
|
||||
import { useMessageHistoryActions, useMessagesForKey } from '../../libs/messages/message-history.store';
|
||||
import {
|
||||
ReadyState,
|
||||
@@ -11,7 +12,7 @@ import UserInput from './user-input';
|
||||
|
||||
const Chat = () => {
|
||||
const initialized = useRef(false);
|
||||
|
||||
const auth = useAuthStoreState();
|
||||
const currentAgent = useCurrentAgent();
|
||||
const lastAgentInitMessage = useLastAgentInitMessage();
|
||||
const messages = useMessagesForKey(currentAgent?.id ?? '');
|
||||
@@ -21,12 +22,11 @@ const Chat = () => {
|
||||
const { addMessage } = useMessageHistoryActions();
|
||||
const { setLastAgentInitMessage } = useAgentActions();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const sendMessageAndAddToHistory = useCallback(
|
||||
(message: string, role: 'user' | 'system' = 'user') => {
|
||||
if (!currentAgent) return;
|
||||
if (!currentAgent || !auth.uuid) return;
|
||||
const date = new Date();
|
||||
sendMessage({ agentId: currentAgent.id, message, role });
|
||||
sendMessage({ userId: auth.uuid, agentId: currentAgent.id, message, role });
|
||||
addMessage(currentAgent.id, {
|
||||
type: role === 'user' ? 'user_message' : 'system_message',
|
||||
message_type: 'user_message',
|
||||
@@ -34,7 +34,7 @@ const Chat = () => {
|
||||
date,
|
||||
});
|
||||
},
|
||||
[currentAgent, sendMessage, addMessage]
|
||||
[currentAgent, auth.uuid, sendMessage, addMessage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -64,7 +64,7 @@ const Chat = () => {
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-screen-xl p-4">
|
||||
<MessageContainer agentSet={!!currentAgent} readyState={readyState} messages={messages} />
|
||||
<MessageContainer currentAgent={currentAgent} readyState={readyState} previousMessages={[]} messages={messages} />
|
||||
<UserInput enabled={readyState !== ReadyState.LOADING} onSend={sendMessageAndAddToHistory} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,23 +10,26 @@ import {
|
||||
FormMessage,
|
||||
} from '@memgpt/components/form';
|
||||
import { Textarea } from '@memgpt/components/textarea';
|
||||
import { cnMuted } from '@memgpt/components/typography';
|
||||
import { cn } from '@memgpt/utils';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { Loader2, LucideCheckCheck } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { AgentMemory } from '../../../libs/agents/agent-memory';
|
||||
import { AgentMemoryUpdateSchema } from '../../../libs/agents/agent-memory-update';
|
||||
import { useAgentMemoryUpdateMutation } from '../../../libs/agents/use-agent-memory.mutation';
|
||||
import { useAuthStoreState } from '../../../libs/auth/auth.store';
|
||||
|
||||
export function MemoryForm({ className, data, agentId }: { className?: string; data: AgentMemory; agentId: string }) {
|
||||
const mutation = useAgentMemoryUpdateMutation();
|
||||
const auth = useAuthStoreState();
|
||||
const mutation = useAgentMemoryUpdateMutation(auth.uuid);
|
||||
|
||||
const form = useForm<z.infer<typeof AgentMemoryUpdateSchema>>({
|
||||
resolver: zodResolver(AgentMemoryUpdateSchema),
|
||||
defaultValues: {
|
||||
persona: data?.core_memory?.persona,
|
||||
human: data?.core_memory?.human,
|
||||
user_id: 'null',
|
||||
user_id: auth.uuid ?? undefined,
|
||||
agent_id: agentId,
|
||||
},
|
||||
});
|
||||
@@ -68,10 +71,23 @@ export function MemoryForm({ className, data, agentId }: { className?: string; d
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button className="mt-4" type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : undefined}
|
||||
{mutation.isPending ? 'Saving Changes' : 'Save Changes'}
|
||||
</Button>
|
||||
<div className="mt-4 flex items-center justify-end">
|
||||
{mutation.isPending && (
|
||||
<span className={cnMuted('mr-6 flex items-center animate-in slide-in-from-bottom')}>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving Memory...
|
||||
</span>
|
||||
)}
|
||||
{mutation.isSuccess && (
|
||||
<span className={cnMuted('mr-6 flex items-center text-emerald-600 animate-in slide-in-from-bottom')}>
|
||||
<LucideCheckCheck className="mr-2 h-4 w-4" />
|
||||
New Memory Saved
|
||||
</span>
|
||||
)}
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
Save Memory
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@memgpt/components/dialog';
|
||||
import { useCurrentAgent } from '../../../libs/agents/agent.store';
|
||||
import { useAgentMemoryQuery } from '../../../libs/agents/use-agent-memory.query';
|
||||
import { useAuthStoreState } from '../../../libs/auth/auth.store';
|
||||
import { MemoryForm } from './memory-form';
|
||||
|
||||
const MemoryView = ({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) => {
|
||||
const auth = useAuthStoreState();
|
||||
const currentAgent = useCurrentAgent();
|
||||
const { data, isLoading } = useAgentMemoryQuery(currentAgent?.id);
|
||||
const { data, isLoading } = useAgentMemoryQuery(auth.uuid, currentAgent?.id);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Badge } from '@memgpt/components/badge';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Agent } from '../../../libs/agents/agent';
|
||||
import { Message } from '../../../libs/messages/message';
|
||||
import { ReadyState } from '../../../libs/messages/message-stream.store';
|
||||
import MessageContainerLayout from './message-container-layout';
|
||||
@@ -7,18 +9,20 @@ import SelectAgentForm from './select-agent-form';
|
||||
import ThinkingIndicator from './thinking-indicator';
|
||||
|
||||
const MessageContainer = ({
|
||||
agentSet,
|
||||
currentAgent,
|
||||
messages,
|
||||
readyState,
|
||||
previousMessages,
|
||||
}: {
|
||||
agentSet: boolean;
|
||||
currentAgent: Agent | null;
|
||||
messages: Message[];
|
||||
readyState: ReadyState;
|
||||
previousMessages: { role: string; content: string; name: string; function_call: { arguments: string } }[];
|
||||
}) => {
|
||||
const messageBox = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => messageBox.current?.scrollIntoView(false), [messages]);
|
||||
|
||||
if (!agentSet) {
|
||||
if (!currentAgent) {
|
||||
return (
|
||||
<MessageContainerLayout>
|
||||
<SelectAgentForm />
|
||||
@@ -28,7 +32,18 @@ const MessageContainer = ({
|
||||
|
||||
return (
|
||||
<MessageContainerLayout>
|
||||
<Badge
|
||||
className="sticky left-1/2 top-2 z-10 mx-auto origin-center -translate-x-1/2 bg-background py-1 px-4"
|
||||
variant="outline"
|
||||
>
|
||||
<span>{currentAgent.name}</span>
|
||||
</Badge>
|
||||
<div className="flex flex-1 flex-col space-y-4 px-4 py-6" ref={messageBox}>
|
||||
{previousMessages.map((m) => (
|
||||
<p>
|
||||
{m.name} | {m.role} | {m.content} | {m.function_call?.arguments}
|
||||
</p>
|
||||
))}
|
||||
{messages.map((message, i) => pickMessageElement(message, i))}
|
||||
{readyState === ReadyState.LOADING ? <ThinkingIndicator className="flex items-center py-3 px-3" /> : undefined}
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,11 @@ import { useState } from 'react';
|
||||
import { Agent } from '../../../libs/agents/agent';
|
||||
import { useAgentActions } from '../../../libs/agents/agent.store';
|
||||
import { useAgentsQuery } from '../../../libs/agents/use-agents.query';
|
||||
import { useAuthStoreState } from '../../../libs/auth/auth.store';
|
||||
|
||||
const SelectAgentForm = () => {
|
||||
const { data } = useAgentsQuery();
|
||||
const { uuid } = useAuthStoreState();
|
||||
const { data } = useAgentsQuery(uuid);
|
||||
const [newAgent, setNewAgent] = useState<Agent | null>(null);
|
||||
const { setAgent } = useAgentActions();
|
||||
return (
|
||||
|
||||
@@ -11,26 +11,26 @@ import { Input } from '@memgpt/components/input';
|
||||
import { Label } from '@memgpt/components/label';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useAgentsCreateMutation } from '../../libs/agents/use-agents.mutation';
|
||||
import { useAuthStoreState } from '../../libs/auth/auth.store';
|
||||
|
||||
const CreateAgentDialog = (props: { open: boolean; onOpenChange: (open: boolean) => void }) => {
|
||||
const createAgent = useAgentsCreateMutation();
|
||||
const auth = useAuthStoreState();
|
||||
const createAgent = useAgentsCreateMutation(auth.uuid);
|
||||
return (
|
||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (!auth.uuid) return;
|
||||
const formData = new FormData(event.currentTarget);
|
||||
// ✅ mutation is invoked when the form is submitted
|
||||
createAgent.mutate(
|
||||
{
|
||||
user_id: 'placeholder',
|
||||
config: {
|
||||
name: `${formData.get('name')}`,
|
||||
human: `${formData.get('human')}`,
|
||||
persona: `${formData.get('persona')}`,
|
||||
model: `${formData.get('model')}`,
|
||||
},
|
||||
name: `${formData.get('name')}`,
|
||||
human: `${formData.get('human')}`,
|
||||
persona: `${formData.get('persona')}`,
|
||||
model: `${formData.get('model')}`,
|
||||
},
|
||||
{
|
||||
onSuccess: () => props.onOpenChange(false),
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import { Button } from '@memgpt/components/button';
|
||||
import { Input } from '@memgpt/components/input';
|
||||
import { Skeleton } from '@memgpt/components/skeleton';
|
||||
import { cnH1, cnLead } from '@memgpt/components/typography';
|
||||
import { LucidePlus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { cnH1, cnH3, cnLead, cnMuted } from '@memgpt/components/typography';
|
||||
import { LucidePlus, LucideSearch } from 'lucide-react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { useAgentActions, useCurrentAgent } from '../../libs/agents/agent.store';
|
||||
import { useAgentsQuery } from '../../libs/agents/use-agents.query';
|
||||
import { useAuthStoreState } from '../../libs/auth/auth.store';
|
||||
import AgentCard from './agent-card';
|
||||
import CreateAgentDialog from './create-agent-dialog';
|
||||
|
||||
const Home = () => {
|
||||
const { data, isLoading } = useAgentsQuery();
|
||||
const { uuid } = useAuthStoreState();
|
||||
const { data, isSuccess, isLoading } = useAgentsQuery(uuid);
|
||||
const { setAgent } = useAgentActions();
|
||||
const currentAgent = useCurrentAgent();
|
||||
const [showingAgentCreation, setShowingAgentCreation] = useState(false);
|
||||
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [debouncedInput] = useDebounce(searchInput, 300);
|
||||
const filteredAgents = (data?.agents ?? []).filter((a) => a.name.includes(debouncedInput));
|
||||
const agentsOrSkeletons = isLoading
|
||||
? [1, 2, 3, 4, 5, 6, 7, 8].map((_, i) => <Skeleton key={i} className="h-52 w-full flex-none opacity-30 sm:w-96" />)
|
||||
: (data?.agents ?? []).map((a) => (
|
||||
: filteredAgents.map((a) => (
|
||||
<AgentCard
|
||||
className="flex h-52 w-full flex-none flex-col justify-between shadow-md sm:w-96"
|
||||
key={a.name}
|
||||
@@ -29,6 +35,7 @@ const Home = () => {
|
||||
/>
|
||||
));
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full flex-col items-center overflow-y-scroll">
|
||||
@@ -36,16 +43,35 @@ const Home = () => {
|
||||
<h1 className={cnH1()}>Welcome to MemGPT</h1>
|
||||
<p className={cnLead('mt-2 mb-4')}>Select or create an agent to start your conversation...</p>
|
||||
</div>
|
||||
<div className="mx-auto flex w-full max-w-screen-2xl flex-wrap gap-12 px-8 pb-20">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowingAgentCreation(true)}
|
||||
className="flex h-52 w-full flex-none flex-col items-center justify-center sm:w-96"
|
||||
>
|
||||
<LucidePlus className="h-8 w-8" />
|
||||
<span className="mt-2">Add New</span>
|
||||
<div className="mx-auto mb-12 flex w-full max-w-screen-lg justify-between">
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
ref={inputRef}
|
||||
placeholder="Search for Agent"
|
||||
className="w-full pl-12 sm:w-80"
|
||||
/>
|
||||
<LucideSearch
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
className="absolute top-1/2 left-3 z-0 h-5 w-5 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={() => setShowingAgentCreation(true)}>
|
||||
<LucidePlus className="h-5 w-5" />
|
||||
<span className="ml-2">Add New</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mx-auto flex w-full max-w-screen-2xl flex-wrap gap-12 px-8 pb-20">
|
||||
{agentsOrSkeletons}
|
||||
{isSuccess && data?.num_agents === 0 ? (
|
||||
<div className="flex w-full flex-col items-center justify-center p-20">
|
||||
<h3 className={cnH3()}>No Agents exist</h3>
|
||||
<p className={cnMuted('mt-4')}>
|
||||
Create your first agent and start chatting by clicking the Add New button.
|
||||
</p>
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
</div>
|
||||
<CreateAgentDialog open={showingAgentCreation} onOpenChange={(open) => setShowingAgentCreation(open)} />
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { useAgentActions, useCurrentAgent } from '../../../libs/agents/agent.store';
|
||||
import { useAgentsQuery } from '../../../libs/agents/use-agents.query';
|
||||
import { useAuthStoreState } from '../../../libs/auth/auth.store';
|
||||
import { SettingsLayout } from '../layout';
|
||||
|
||||
const agentsFormSchema = z.object({
|
||||
@@ -33,7 +34,8 @@ const getDefaultValues: (initialName: string | null | undefined) => Partial<Agen
|
||||
});
|
||||
|
||||
export function AgentsForm() {
|
||||
const { data, isLoading } = useAgentsQuery();
|
||||
const auth = useAuthStoreState();
|
||||
const { data, isLoading } = useAgentsQuery(auth.uuid);
|
||||
const currentAgent = useCurrentAgent();
|
||||
const { setAgent } = useAgentActions();
|
||||
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { createBrowserRouter, Outlet } from 'react-router-dom';
|
||||
import Auth from './auth';
|
||||
import { chatRoute } from './modules/chat/chat.routes';
|
||||
import Home from './modules/home/home';
|
||||
import { settingsRoute } from './modules/settings/settings.routes';
|
||||
import Footer from './shared/layout/footer';
|
||||
import Header from './shared/layout/header';
|
||||
|
||||
const rootRoute = () => (
|
||||
<>
|
||||
<Header />
|
||||
<div className="h-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
|
||||
const RootRoute = () => {
|
||||
return (
|
||||
<Auth>
|
||||
<Header />
|
||||
<div className="h-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
<Footer />
|
||||
</Auth>
|
||||
);
|
||||
};
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: rootRoute(),
|
||||
element: RootRoute(),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
||||
@@ -253,6 +253,19 @@ class ServerChoice(Enum):
|
||||
ws_api = "websocket"
|
||||
|
||||
|
||||
def create_default_user_or_exit(config: MemGPTConfig, ms: MetadataStore):
|
||||
user_id = uuid.UUID(config.anon_clientid)
|
||||
user = ms.get_user(user_id=user_id)
|
||||
if user is None:
|
||||
ms.create_user(User(id=user_id))
|
||||
user = ms.get_user(user_id=user_id)
|
||||
if user is None:
|
||||
typer.secho(f"Failed to create default user in database.", fg=typer.colors.RED)
|
||||
sys.exit(1)
|
||||
else:
|
||||
return user
|
||||
|
||||
|
||||
def server(
|
||||
type: ServerChoice = typer.Option("rest", help="Server to run"),
|
||||
port: int = typer.Option(None, help="Port to run the server on"),
|
||||
@@ -278,13 +291,21 @@ def server(
|
||||
import uvicorn
|
||||
from memgpt.server.rest_api.server import app
|
||||
|
||||
if MemGPTConfig.exists():
|
||||
config = MemGPTConfig.load()
|
||||
ms = MetadataStore(config)
|
||||
create_default_user_or_exit(config, ms)
|
||||
else:
|
||||
typer.secho(f"No configuration exists. Run memgpt configure before starting the server.", fg=typer.colors.RED)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
# Start the subprocess in a new session
|
||||
uvicorn.run(app, host=host or "localhost", port=port or REST_DEFAULT_PORT)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
# Handle CTRL-C
|
||||
print("Terminating the server...")
|
||||
typer.secho("Terminating the server...")
|
||||
sys.exit(0)
|
||||
|
||||
elif type == ServerChoice.ws_api:
|
||||
@@ -299,7 +320,7 @@ def server(
|
||||
command = f"python server.py {port}"
|
||||
|
||||
# Run the command
|
||||
print(f"Running WS (websockets) server: {command} (inside {server_directory})")
|
||||
typer.secho(f"Running WS (websockets) server: {command} (inside {server_directory})")
|
||||
|
||||
try:
|
||||
# Start the subprocess in a new session
|
||||
@@ -307,13 +328,13 @@ def server(
|
||||
process.wait()
|
||||
except KeyboardInterrupt:
|
||||
# Handle CTRL-C
|
||||
print("Terminating the server...")
|
||||
typer.secho("Terminating the server...")
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
print("Server terminated with kill()")
|
||||
typer.secho("Server terminated with kill()")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@@ -435,17 +456,7 @@ def run(
|
||||
|
||||
# read user id from config
|
||||
ms = MetadataStore(config)
|
||||
user_id = uuid.UUID(config.anon_clientid)
|
||||
user = ms.get_user(user_id=user_id)
|
||||
if user is None:
|
||||
print("Creating user", user_id)
|
||||
ms.create_user(User(id=user_id))
|
||||
user = ms.get_user(user_id=user_id)
|
||||
if user is None:
|
||||
typer.secho(f"Failed to create default user in database.", fg=typer.colors.RED)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("existing user", user, user_id)
|
||||
user = create_default_user_or_exit(config, ms)
|
||||
|
||||
# override with command line arguments
|
||||
if debug:
|
||||
|
||||
@@ -3,12 +3,13 @@ from asyncio import AbstractEventLoop
|
||||
from enum import Enum
|
||||
import json
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, Body, HTTPException, Query
|
||||
from pydantic import BaseModel, Field, constr, validator
|
||||
from fastapi import APIRouter, Body, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from memgpt.constants import JSON_ENSURE_ASCII
|
||||
from memgpt.server.rest_api.interface import QueuingInterface
|
||||
from memgpt.server.server import SyncServer
|
||||
|
||||
@@ -112,7 +113,7 @@ def setup_agents_message_router(server: SyncServer, interface: QueuingInterface)
|
||||
|
||||
async def formatted_message_generator():
|
||||
async for message in interface.message_generator():
|
||||
formatted_message = f"data: {json.dumps(message)}\n\n"
|
||||
formatted_message = f"data: {json.dumps(message, ensure_ascii=JSON_ENSURE_ASCII)}\n\n"
|
||||
yield formatted_message
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
0
memgpt/server/rest_api/auth/__init__.py
Normal file
0
memgpt/server/rest_api/auth/__init__.py
Normal file
33
memgpt/server/rest_api/auth/index.py
Normal file
33
memgpt/server/rest_api/auth/index.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from memgpt.server.rest_api.interface import QueuingInterface
|
||||
from memgpt.server.server import SyncServer
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class AuthResponse(BaseModel):
|
||||
uuid: UUID = Field(..., description="UUID of the user")
|
||||
|
||||
|
||||
def setup_auth_router(server: SyncServer, interface: QueuingInterface):
|
||||
@router.get("/auth", tags=["auth"], response_model=AuthResponse)
|
||||
def authenticate_user():
|
||||
"""
|
||||
Authenticates the user and sends response with User related data.
|
||||
|
||||
Currently, this is a placeholder that simply returns a UUID placeholder
|
||||
"""
|
||||
interface.clear()
|
||||
try:
|
||||
response = server.authenticate_user()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"{e}")
|
||||
return AuthResponse(uuid=response)
|
||||
|
||||
return router
|
||||
@@ -11,6 +11,7 @@ from memgpt.server.rest_api.agents.command import setup_agents_command_router
|
||||
from memgpt.server.rest_api.agents.config import setup_agents_config_router
|
||||
from memgpt.server.rest_api.agents.memory import setup_agents_memory_router
|
||||
from memgpt.server.rest_api.agents.message import setup_agents_message_router
|
||||
from memgpt.server.rest_api.auth.index import setup_auth_router
|
||||
from memgpt.server.rest_api.config.index import setup_config_index_router
|
||||
from memgpt.server.server import SyncServer
|
||||
from memgpt.server.rest_api.interface import QueuingInterface
|
||||
@@ -48,6 +49,8 @@ app.add_middleware(
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
# /api/auth endpoints
|
||||
app.include_router(setup_auth_router(server, interface), prefix=API_PREFIX)
|
||||
# /api/agents endpoints
|
||||
app.include_router(setup_agents_command_router(server, interface), prefix=API_PREFIX)
|
||||
app.include_router(setup_agents_config_router(server, interface), prefix=API_PREFIX)
|
||||
|
||||
@@ -876,3 +876,7 @@ class SyncServer(LockingServer):
|
||||
"new_core_memory": new_core_memory,
|
||||
"modified": modified,
|
||||
}
|
||||
|
||||
def authenticate_user(self) -> uuid.UUID:
|
||||
# TODO: Implement actual authentication to enable multi user setup
|
||||
return uuid.UUID(int=uuid.getnode())
|
||||
|
||||
129
memgpt/server/static_files/assets/index-273ebfe0.js
Normal file
129
memgpt/server/static_files/assets/index-273ebfe0.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
memgpt/server/static_files/assets/index-9ace7bf7.css
Normal file
1
memgpt/server/static_files/assets/index-9ace7bf7.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>MemGPT Chat UI</title>
|
||||
<title>MemgptFrontend</title>
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@@ -29,8 +29,8 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-c3a527b1.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-27e4af26.css">
|
||||
<script type="module" crossorigin src="/assets/index-273ebfe0.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-9ace7bf7.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="h-full w-full" id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user