Skip to content

Commit

Permalink
Remove Separate API Service for Web Interface (#174)
Browse files Browse the repository at this point in the history
* Adds 50 turn free message allotment.

* Fixes

* Cleanup.

* Better message.

* Use server/admin actions to prevent js injection.

* Cleanup

* Updates auth check to support trial membership.

* chore: Upgrade Honcho Version (#170)

* fix(stripe) Handle de-duplication between trial and active subscriptions

* Rendering Fully without stripe

* chore: consolidate lib and utils

* fix(www) Chat, reactions, and get thought working

* fix(conversations): Automatically make new convo if none exist on load

* Fix unstable stream via vercel sdk

* README Updates

---------

Co-authored-by: Ben Lopata <[email protected]>
  • Loading branch information
VVoruganti and bLopata authored Dec 2, 2024
1 parent b1c4351 commit bd12699
Show file tree
Hide file tree
Showing 28 changed files with 1,231 additions and 412 deletions.
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ the backend logic for different clients.
- `agent/` - this contains the core logic and prompting architecture
- `bot/` - this contains the discord bot implementation
- `api/` - this contains a FastAPI API interface that exposes the `agent/` logic
- `www/` - this contains a `NextJS` web front end that can connect to the API interface
- `www/` - this contains a FullStack `NextJS` version of Tutor-GPT
- `supabase/` - contains SQL scripts necessary for setting up local supabase

Most of the project is developed using python with the exception of the NextJS
Expand Down Expand Up @@ -113,8 +113,6 @@ Below are more detailed explanations of environment variables

### FastAPI

**NextJS & fastAPI**

- `URL` — The URL endpoint for the frontend Next.js application
- `HONCHO_URL` — The base URL for the instance of Honcho you are using
- `HONCHO_APP_NAME` — The name of the honcho application to use for Tutor-GPT
Expand Down Expand Up @@ -147,7 +145,6 @@ run the bot.

```bash
docker run -p 8000:8000 --env-file .env tutor-gpt-core python -m uvicorn api.main:app --host 0.0.0.0 --port 8000 # FastAPI Backend
docker run tutor-gpt-web
```

> NOTE: the default run command in the docker file for the core runs the FastAPI backend so you could just run docker run --env-file .env tutor-gpt-core
Expand Down
8 changes: 6 additions & 2 deletions www/.env.template
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# Core
NEXT_PUBLIC_SITE_URL=
NEXT_PUBLIC_API_URL=
# Supabase
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY
SUPABASE_SERVICE_ROLE_KEY=
# Sentry
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_ENVIRONMENT=
Expand All @@ -17,3 +16,8 @@ NEXT_PUBLIC_STRIPE_ENABLED=false
STRIPE_SECRET_KEY=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
STRIPE_WEBHOOK_SECRET=
# Agent
OPENAI_API_KEY=
MODEL=
HONCHO_URL=
HONCHO_APP_NAME=
10 changes: 8 additions & 2 deletions www/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Tutor-GPT Web UI

This directory contains the code for the Web-UI of Tutor-GPT. It is developed
This directory contains the code for the Full Stack Web-UI of Tutor-GPT. It is developed
using [Next.js](https://nextjs.org/) and [pnpm](https://pnpm.io).

The project uses [Supabase](https://supabase.com/) for authentication and managing users subscriptions
Expand Down Expand Up @@ -33,7 +33,13 @@ Tutor-GPT webui. A `.env.template` file is provided to get started quickly.

- `NEXT_PUBLIC_SITE_URL` — The URL that the Next.js application will run from. For
local development it will be `http://localhost:3000` by default.
- `NEXT_PUBLIC_API_URL` — The URL that the FastAPI backend is running from.

**Agent**

- `MODEL` — The Openrouter model to use for generating responses.
- `OPENAI_API_KEY` — The Openrouter API key to use
- `HONCHO_URL` — The URL for the Honcho instance to use
- `HONCHO_APP_NAME` — The name of the app in Honcho to use

**Supabase**

Expand Down
112 changes: 112 additions & 0 deletions www/app/actions/conversations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use server';

import { createClient } from '@/utils/supabase/server';
import { honcho, getHonchoApp } from '@/utils/honcho';

// TODO add proper authorization check

type Conversation = {
conversationId: string;
name: string;
};

export async function getConversations() {
const supabase = createClient();

const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
throw new Error('Unauthorized');
}

const honchoApp = await getHonchoApp();

const honchoUser = await honcho.apps.users.getOrCreate(honchoApp.id, user.id);

const acc = [];
for await (const convo of honcho.apps.users.sessions.list(
honchoApp.id,
honchoUser.id,
{ is_active: true, reverse: true }
)) {
const name = (convo.metadata?.name as string) ?? 'Untitled';
const instance: Conversation = {
conversationId: convo.id,
name,
};
acc.push(instance);
}
return acc;
}

export async function createConversation() {
const supabase = createClient();

const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
throw new Error('Unauthorized');
}

const honchoApp = await getHonchoApp();
const honchoUser = await honcho.apps.users.getOrCreate(honchoApp.id, user.id);

const session = await honcho.apps.users.sessions.create(
honchoApp.id,
honchoUser.id,
{}
);

return { conversationId: session.id, name: 'Untitled' };
}

export async function deleteConversation(conversationId: string) {
const supabase = createClient();

const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
throw new Error('Unauthorized');
}

const honchoApp = await getHonchoApp();
const honchoUser = await honcho.apps.users.getOrCreate(honchoApp.id, user.id);

await honcho.apps.users.sessions.delete(
honchoApp.id,
honchoUser.id,
conversationId
);

return true;
}

export async function updateConversation(conversationId: string, name: string) {
const supabase = createClient();

const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
throw new Error('Unauthorized');
}

const honchoApp = await getHonchoApp();
const honchoUser = await honcho.apps.users.getOrCreate(honchoApp.id, user.id);

await honcho.apps.users.sessions.update(
honchoApp.id,
honchoUser.id,
conversationId,
{ metadata: { name } }
);

return true;
}
132 changes: 132 additions & 0 deletions www/app/actions/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
'use server';
import { createClient } from '@/utils/supabase/server';
import { honcho, getHonchoApp } from '@/utils/honcho';
import { Message } from '@/utils/types';

const defaultMessage: Message = {
content: `I'm your Aristotelian learning companion — here to help you follow your curiosity in whatever direction you like. My engineering makes me extremely receptive to your needs and interests. You can reply normally, and I’ll always respond!\n\nIf I&apos;m off track, just say so!\n\nNeed to leave or just done chatting? Let me know! I’m conversational by design so I’ll say goodbye 😊.`,
isUser: false,
id: '',
};

export async function getMessages(conversationId: string) {
const supabase = createClient();

const honchoApp = await getHonchoApp();

const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
throw new Error('Unauthorized');
}
const honchoUser = await honcho.apps.users.getOrCreate(honchoApp.id, user.id);
const session = await honcho.apps.users.sessions.get(
honchoApp.id,
honchoUser.id,
conversationId
);
const messages = [];
// TODO check if empty params is necessary
for await (const message of honcho.apps.users.sessions.messages.list(
honchoApp.id,
honchoUser.id,
session.id,
{}
)) {
messages.push({
id: message.id,
content: message.content,
isUser: message.is_user,
metadata: message.metadata,
});
}

return [defaultMessage, ...messages];
}

export async function getThought(conversationId: string, messageId: string) {
const supabase = createClient();

const honchoApp = await getHonchoApp();

const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
throw new Error('Unauthorized');
}

const honchoUser = await honcho.apps.users.getOrCreate(honchoApp.id, user.id);

try {
const thoughts = await honcho.apps.users.sessions.metamessages.list(
honchoApp.id,
honchoUser.id,
conversationId,
{
message_id: messageId,
metamessage_type: 'thought',
filter: { type: 'assistant' },
}
);

return thoughts.items[0]?.content || null;
} catch (error) {
console.error('Error in getThought:', error);
throw new Error('Internal server error');
}
}

export async function addOrRemoveReaction(
conversationId: string,
messageId: string,
reaction: 'thumbs_up' | 'thumbs_down' | null
) {
const supabase = createClient();

const honchoApp = await getHonchoApp();

const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
throw new Error('Unauthorized');
}

if (reaction && !['thumbs_up', 'thumbs_down'].includes(reaction)) {
throw new Error('Invalid reaction type');
}

const honchoUser = await honcho.apps.users.getOrCreate(honchoApp.id, user.id);

const message = await honcho.apps.users.sessions.messages.get(
honchoApp.id,
honchoUser.id,
conversationId,
messageId
);

if (!message) {
throw new Error('Message not found');
}

const metadata = message.metadata || {};

if (reaction === null) {
delete metadata.reaction;
} else {
metadata.reaction = reaction;
}

await honcho.apps.users.sessions.messages.update(
honchoApp.id,
honchoUser.id,
conversationId,
messageId,
{ metadata }
);
}
Loading

0 comments on commit bd12699

Please sign in to comment.