Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a simple profile page #33

Closed
ydennisy opened this issue May 27, 2024 · 2 comments
Closed

Add a simple profile page #33

ydennisy opened this issue May 27, 2024 · 2 comments
Labels

Comments

@ydennisy
Copy link
Owner

ydennisy commented May 27, 2024

Details

Feature

We already have a basic profiles.vue in our app frontend, which calls a /me endpoint which is not yet implemented.

We should add this endpoint in the backend and also create some simple markup to display some basic user data in the frontend.

Our users table in the database has:

  • id
  • email
  • app_email_alias

we can display these values to the user.

Make sure you:

  • use the DB client from db.py and add a new method there
  • add some basic styling using tailwind to the profile page display

Branch

No response

@ydennisy ydennisy added the sweep label May 27, 2024
Copy link
Contributor

sweep-ai bot commented May 27, 2024

🚀 Here's the PR! #35

💎 Sweep Pro: You have unlimited Sweep issues

Actions

  • ↻ Restart Sweep

Step 1: 🔎 Searching

Here are the code search results. I'm now analyzing these search results to write the PR.

Relevant files (click to expand). Mentioned files will always appear here.

kg1/backend/app/db.py

Lines 1 to 128 in 8a84809

from __future__ import annotations
import os
from typing import TYPE_CHECKING
from supabase import create_client
from app.utils import get_logger
if TYPE_CHECKING:
from app.domain import TextNode
from app.domain import URL
log = get_logger(__name__)
class DB:
def __init__(self) -> None:
self._client = create_client(
os.getenv("SUPABASE_URL"), os.getenv("SUPABASE_KEY")
)
def increment_usage_counter(self) -> int:
result = self._client.rpc("update_usage_counter").execute()
return result.data
def search_pages(
self, emb: list[float], user_id: str, threshold: float = 0.5, top_n: int = 10
):
result = self._client.rpc(
"search_pages",
{
"query_embedding": emb,
"user_id_filter": user_id,
"threshold": threshold,
"top_n": top_n,
},
).execute()
return result.data
def search_chunks(
self, emb: list[float], user_id: str, threshold: float = 0.5, top_n: int = 10
):
result = self._client.rpc(
"search_chunks",
{
"query_embedding": emb,
"user_id_filter": user_id,
"threshold": threshold,
"top_n": top_n,
},
).execute()
return result.data
def get_text_node(self, id: str):
result = (
self._client.table("text_nodes")
.select("id, title, text, summary, url, embedding")
.eq("id", id)
.execute()
)
return result.data[0]
def get_similar_text_nodes(self, id: str, top_n: int = 10):
result = self._client.rpc(
"get_similar_text_nodes", {"id": id, "top_n": top_n}
).execute()
return result.data
def create_urls(self, urls: list[URL], user_id: str):
urls_to_persist = []
for url in urls:
url_to_persist = url.to_persistence()
url_to_persist["user_id"] = user_id
urls_to_persist.append(url_to_persist)
try:
return self._client.table("urls_feed").insert(urls_to_persist).execute()
except Exception as ex:
log.exception(ex)
def update_urls(self, urls: list[URL]):
for url in urls:
try:
(
self._client.table("urls_feed")
.update({"status": url.status})
.eq("id", url.id)
.execute()
)
except Exception as ex:
log.exception(f"Failed to update URL with id {url.id}: {ex}")
def create_text_nodes(self, nodes: list[TextNode], user_id: str):
text_nodes_to_persist = []
text_node_chunks_to_persist = []
for node in nodes:
text_node, text_node_chunks = node.to_persistence()
text_node["user_id"] = user_id
for chunk in text_node_chunks:
chunk["user_id"] = user_id
text_nodes_to_persist.append(text_node)
text_node_chunks_to_persist.extend(text_node_chunks)
self._client.table("text_nodes").insert(text_nodes_to_persist).execute()
self._client.table("text_node_chunks").insert(
text_node_chunks_to_persist
).execute()
def get_urls_feed(self, user_id: str):
result = (
self._client.table("urls_feed")
.select("id, created_at, url, status, source")
.eq("user_id", user_id)
.order("created_at", desc=True)
.limit(10)
.execute()
)
return result.data
def get_user_id_by_email_alias(self, app_email_alias: str):
result = (
self._client.table("users")
.select("id")
.eq("app_email_alias", app_email_alias)
.execute()
)
if len(result.data) != 1:
return None

kg1/backend/app/main.py

Lines 1 to 153 in 8a84809

import json
from typing import List
from fastapi import FastAPI, HTTPException, Depends, status, Request, BackgroundTasks
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
from pydantic import BaseModel, Field, ConfigDict, EmailStr
from app.db import DB
from app.llm import answer_with_context
from app.utils import NodeEmbedder
from app.utils import get_current_user
from app.utils import parse_urls_from_text
from app.domain import URL, URLSource
from app.services import IndexingService
load_dotenv()
app = FastAPI()
db = DB()
indexing_service = IndexingService()
allowed_origins = [
"https://kg1.io",
"http://localhost",
"http://localhost:3000",
]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/health")
async def get_health():
return {"STATUS": "OK"}
@app.get("/api/search")
def get_search_route(q: str, user=Depends(get_current_user)):
user_id = user.id
query_emb = NodeEmbedder.embed(q)
pages = db.search_pages(query_emb, user_id=user_id, threshold=0.4)
return pages
@app.get("/api/ask")
def get_ask_route(q: str, id: str = None, user=Depends(get_current_user)):
user_id = user.id
usage_count = db.increment_usage_counter()
if usage_count > 1000:
# TODO: handle this client side!
raise HTTPException(429)
query_emb = NodeEmbedder.embed(q)
if id:
# TODO: check this node belongs to the user requesting it!
node = db.get_text_node(id)
del node["embedding"]
chunks = [node]
else:
chunks = db.search_chunks(query_emb, user_id=user_id)
# NOTE: this is best moved into the DB query when we find the correct value.
chunks = [c for c in chunks if c["score"] >= 0.4]
if len(chunks) == 0:
raise HTTPException(404)
def generate_streaming_response():
yield json.dumps({"context": chunks}) + "<END_OF_CONTEXT>"
for part in answer_with_context(chunks=chunks, question=q):
yield part
return StreamingResponse(generate_streaming_response(), media_type="text/plain")
@app.get("/api/node")
async def get_node_route(id: str, user=Depends(get_current_user)):
user_id = user.id
usage_count = db.increment_usage_counter()
if usage_count > 1000:
# TODO: handle this client side!
raise HTTPException(429)
node = db.get_text_node(id)
related_nodes = db.search_pages(
node["embedding"], user_id=user_id, threshold=0.4, top_n=5
)
related_nodes = [n for n in related_nodes if n["id"] != id]
node["related"] = related_nodes
del node["text"]
del node["embedding"]
return node
@app.get("/api/index-feed")
async def get_index_feed_route(user=Depends(get_current_user)):
user_id = user.id
urls = db.get_urls_feed(user_id)
return urls
class IndexPayload(BaseModel):
urls: List[str]
@app.post("/api/index", status_code=status.HTTP_202_ACCEPTED)
async def post_index_route(
payload: IndexPayload,
background_tasks: BackgroundTasks,
user=Depends(get_current_user),
):
try:
user_id = user.id
urls = [URL(url=url, source=URLSource.WEB) for url in payload.urls]
# await indexing_service.index(urls, user_id)
background_tasks.add_task(indexing_service.index, urls, user_id)
except Exception as ex:
raise HTTPException(500) from ex
class IndexEmailPayload(BaseModel):
model_config = ConfigDict(populate_by_name=True, extra="ignore")
to: EmailStr
from_: EmailStr = Field(..., alias="from")
subject: str = None
text: str = None
@app.post("/api/email", status_code=status.HTTP_201_CREATED)
async def post_index_route(request: Request):
try:
form = await request.form()
parsed_form = jsonable_encoder(form)
payload = IndexEmailPayload(**parsed_form)
app_email_alias = payload.to.split("@")[0]
user_id = db.get_user_id_by_email_alias(app_email_alias)
raw_urls = parse_urls_from_text(payload.text)
urls = [URL(url=url, source=URLSource.EMAIL) for url in raw_urls]
await indexing_service.index(urls, user_id)
except Exception as ex:

<script setup lang="ts">
interface Profile {
name: string;
}
const profile = ref<Profile>({
name: '',
});
const config = useRuntimeConfig();
const apiBase = config.public.apiBase;
const fetchProfile = async () => {
const token = useSupabaseSession().value?.access_token;
// TODO: handle re-auth
if (!token) return;
const { data } = await useFetch<Profile>(`${apiBase}/api/me`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
});
if (data.value) {
profile.value = data.value;
}
};
onMounted(async () => {
await fetchProfile();
});
</script>
<template>
<p>{{ profile.name }}</p>

Step 2: ⌨️ Coding

backend/app/db.py

Add a new method to retrieve user profile data by ID.
--- 
+++ 
@@ -7,4 +7,15 @@
     )
     if len(result.data) != 1:
         return None
-    return result.data[0]["id"]
+    return result.data[0]["id"]
+
+def get_user_profile_by_id(self, user_id: str):
+    result = (
+        self._client.table("users")
+        .select("id, email, app_email_alias")
+        .eq("id", user_id)
+        .execute()
+    )
+    if len(result.data) != 1:
+        return None
+    return result.data[0]

backend/app/main.py

Add a new endpoint to retrieve the current user's profile data.
--- 
+++ 
@@ -2,4 +2,12 @@
 async def get_index_feed_route(user=Depends(get_current_user)):
     user_id = user.id
     urls = db.get_urls_feed(user_id)
-    return urls
+    return urls
+
+@app.get("/api/me")
+async def get_me_route(user=Depends(get_current_user)):
+    user_id = user.id
+    profile = db.get_user_profile_by_id(user_id)
+    if not profile:
+        raise HTTPException(404)
+    return profile

frontend/pages/profile.vue

Update the Profile interface to include email and app_email_alias fields.
--- 
+++ 
@@ -1,3 +1,11 @@
+interface Profile {
+  id: string;
+  email: string;
+  app_email_alias: string;
+}
+
 const profile = ref<Profile>({
-  name: '',
+  id: '',
+  email: '',
+  app_email_alias: '',
 });

frontend/pages/profile.vue

Update the template to display the user's email and app_email_alias.
--- 
+++ 
@@ -1,10 +1,14 @@
 <script setup lang="ts">
 interface Profile {
-  name: string;
+  id: string;
+  email: string;
+  app_email_alias: string;
 }
 
 const profile = ref<Profile>({
-  name: '',
+  id: '',
+  email: '',
+  app_email_alias: '',
 });
 
 const config = useRuntimeConfig();
@@ -31,5 +35,9 @@
 </script>
 
 <template>
-  <p>{{ profile.name }}</p>
+  <div class="p-4">
+    <h1 class="text-2xl font-bold mb-4">Profile</h1>
+    <p class="text-lg mb-2"><strong>Email:</strong> {{ profile.email }}</p>
+    <p class="text-lg"><strong>App Email Alias:</strong> {{ profile.app_email_alias }}</p>
+  </div>
 </template>

Step 3: 🔄️ Validating

Your changes have been successfully made to the branch sweep/add_a_simple_profile_page. I have validated these changes using a syntax checker and a linter.


Tip

To recreate the pull request, edit the issue title or description.

This is an automated message generated by Sweep AI.

@ydennisy
Copy link
Owner Author

Make sure you:

  • use the DB client from db.py and add a new method there
  • add some basic styling using tailwind to the profile page display

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant