Skip to content

Commit

Permalink
Merge pull request #2 from Mirascope/add-project-ui
Browse files Browse the repository at this point in the history
added project ui
  • Loading branch information
Brendan Kao authored Sep 6, 2024
2 parents 4f3ea1b + 8a044c8 commit 8ea8092
Show file tree
Hide file tree
Showing 21 changed files with 497 additions and 33 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,5 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

.db
3 changes: 2 additions & 1 deletion lilypad/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from contextlib import suppress

from . import app as app
from .prompts import prompt
from .tools import tools
from .trace import trace
Expand All @@ -10,4 +11,4 @@
from . import openai as openai


__all__ = ["openai", "prompt", "tools", "trace"]
__all__ = ["app", "openai", "prompt", "tools", "trace"]
1 change: 1 addition & 0 deletions lilypad/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""The lilypad app module."""
Binary file added lilypad/app/database.db
Binary file not shown.
5 changes: 5 additions & 0 deletions lilypad/app/db/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""This module is responsible for handling the database connection."""

from .session import engine, get_session

__all__ = ["engine", "get_session"]
20 changes: 20 additions & 0 deletions lilypad/app/db/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Database session management"""

from collections.abc import Generator

from sqlmodel import Session, create_engine

engine = create_engine("sqlite:///database.db")


def get_session() -> Generator[Session, None, None]:
"""Get a SQLModel session"""
with Session(engine) as session:
yield session
try:
session.flush()
except Exception:
session.rollback()
raise
else:
session.commit()
8 changes: 8 additions & 0 deletions lilypad/app/db/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Create the database tables."""

from sqlmodel import SQLModel

from lilypad.app.db.session import engine
from lilypad.app.models import * # noqa: F403

SQLModel.metadata.create_all(engine)
155 changes: 146 additions & 9 deletions lilypad/app/main.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,159 @@
"""Main FastAPI application module for Lilypad."""

from fastapi import FastAPI, Form, Request
from fastapi.responses import HTMLResponse
from typing import Annotated

from fastapi import Depends, FastAPI, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlmodel import Session, select

from lilypad.app.db.session import get_session
from lilypad.app.models import ProjectTable, PromptVersionTable

app = FastAPI()
templates = Jinja2Templates(directory="templates")


@app.get("/", response_class=HTMLResponse)
async def root(request: Request) -> HTMLResponse:
"""Render the index.html template."""
return templates.TemplateResponse("index.html", {"request": request})
async def projects(request: Request) -> HTMLResponse:
"""Render the projects.html template."""
return templates.TemplateResponse("projects.html", {"request": request})


@app.get("/projects", response_class=HTMLResponse)
async def get_projects(
request: Request,
session: Annotated[Session, Depends(get_session)],
) -> HTMLResponse:
"""Get all projects from the database."""
projects = session.exec(select(ProjectTable)).all()
return templates.TemplateResponse(
"partials/project_list.html", {"request": request, "projects": projects}
)


@app.post("/projects")
async def add_project(
request: Request,
session: Annotated[Session, Depends(get_session)],
project_name: str = Form(...),
) -> RedirectResponse:
"""Add a project to the database."""
project = ProjectTable(name=project_name)
session.add(project)
session.commit()
session.flush()
return RedirectResponse(url=f"/projects/{project.name}", status_code=303)


@app.get("/projects/{project_name}", response_class=HTMLResponse)
async def create_project(
request: Request,
session: Annotated[Session, Depends(get_session)],
project_name: str,
) -> HTMLResponse:
"""Render the create_project.html template."""
project = session.exec(
select(ProjectTable).where(ProjectTable.name == project_name)
).first()
return templates.TemplateResponse(
"create_project.html", {"request": request, "project": project}
)


@app.get("/projects/{project_name}", response_class=HTMLResponse)
async def show_project(
request: Request,
session: Annotated[Session, Depends(get_session)],
project_name: str,
) -> HTMLResponse:
"""Render the create_project.html template."""
project = session.exec(
select(ProjectTable).where(ProjectTable.name == project_name)
).first()
return templates.TemplateResponse(
"create_project.html", {"request": request, "project": project}
)


@app.get("/projects/{project_name}/versions", response_class=HTMLResponse)
async def get_prompt_versions(
request: Request,
session: Annotated[Session, Depends(get_session)],
project_name: str,
) -> HTMLResponse:
"""Render the version_list.html template."""
project = session.exec(
select(ProjectTable).where(ProjectTable.name == project_name)
).first()

if not project:
raise HTTPException(status_code=404)

return templates.TemplateResponse(
"partials/version_list.html",
{
"request": request,
"project": project,
"prompt_versions": project.prompt_versions,
},
)


@app.post("/projects/{project_name}/versions")
async def create_version(
request: Request,
session: Annotated[Session, Depends(get_session)],
project_name: str,
prompt_template: str = Form(...),
) -> RedirectResponse:
"""Render the create_version.html template."""
project = session.exec(
select(ProjectTable).where(ProjectTable.name == project_name)
).first()

if not project:
raise HTTPException(status_code=404)

latest_prompt_version = session.exec(
select(PromptVersionTable).order_by("id")
).first()

prompt_version = PromptVersionTable(
project_id=project.id,
prompt_template=prompt_template,
previous_version_id=latest_prompt_version.id if latest_prompt_version else None,
)
session.add(prompt_version)
session.commit()
session.flush()
return RedirectResponse(
url=f"/projects/{project_name}/versions/{prompt_version.id}", status_code=303
)


@app.get(
"/projects/{project_name}/versions/{prompt_version_id}", response_class=HTMLResponse
)
async def view_version(
request: Request,
session: Annotated[Session, Depends(get_session)],
project_name: str,
prompt_version_id: str,
) -> HTMLResponse:
"""Render the create_version.html template."""
prompt_version = session.exec(
select(PromptVersionTable).where(PromptVersionTable.id == prompt_version_id)
).first()

if not prompt_version:
raise HTTPException(status_code=404)

@app.post("/submit", response_class=HTMLResponse)
async def submit(request: Request, input_value: str = Form(...)) -> HTMLResponse:
"""Render the result.html template."""
return templates.TemplateResponse(
"partials/result.html", {"request": request, "input_value": input_value}
"create_project.html",
{
"request": request,
"prompt_template": prompt_version.prompt_template,
"project": prompt_version.project,
},
)
7 changes: 7 additions & 0 deletions lilypad/app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""This module initializes the models package."""

from .base_sql_model import BaseSQLModel
from .projects import ProjectTable
from .prompt_versions import PromptVersionTable

__all__ = ["BaseSQLModel", "ProjectTable", "PromptVersionTable"]
23 changes: 23 additions & 0 deletions lilypad/app/models/base_sql_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Base SQLModel class for all SQLModel classes to inherit from"""

from pydantic import ConfigDict
from sqlmodel import SQLModel


class BaseSQLModel(SQLModel):
"""Base SQLModel class"""

model_config = ConfigDict( # pyright:ignore[reportAssignmentType]
populate_by_name=True,
from_attributes=True,
validate_assignment=True,
)


BaseSQLModel.metadata.naming_convention = {
"ix": "%(column_0_label)s_idx",
"uq": "%(table_name)s_%(column_0_name)s_key",
"ck": "%(table_name)s_%(constraint_name)s_check",
"fk": "%(table_name)s_%(column_0_name)s_%(referred_table_name)s_fkey",
"pk": "%(table_name)s_pkey",
}
28 changes: 28 additions & 0 deletions lilypad/app/models/projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Project model"""

import datetime
from typing import TYPE_CHECKING

from sqlmodel import Field, Relationship

from lilypad.app.models import BaseSQLModel

from .table_names import PROJECT_TABLE_NAME

if TYPE_CHECKING:
from lilypad.app.models import PromptVersionTable


class ProjectTable(BaseSQLModel, table=True):
"""Project model"""

__tablename__ = PROJECT_TABLE_NAME # type: ignore

id: int | None = Field(default=None, primary_key=True)
name: str = Field(nullable=False, unique=True)
created_at: datetime.datetime = Field(
default=datetime.datetime.now(datetime.UTC), nullable=False
)
prompt_versions: list["PromptVersionTable"] = Relationship(
back_populates="project", cascade_delete=True
)
31 changes: 31 additions & 0 deletions lilypad/app/models/prompt_versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Project model"""

import datetime
from typing import TYPE_CHECKING

from sqlmodel import Field, Relationship

from lilypad.app.models import BaseSQLModel

from .table_names import PROJECT_TABLE_NAME, PROMPT_VERSION_TABLE_NAME

if TYPE_CHECKING:
from lilypad.app.models import ProjectTable


class PromptVersionTable(BaseSQLModel, table=True):
"""Prompt version model"""

__tablename__ = PROMPT_VERSION_TABLE_NAME # type: ignore

id: int | None = Field(default=None, primary_key=True)
project_id: int | None = Field(default=None, foreign_key=f"{PROJECT_TABLE_NAME}.id")
prompt_template: str = Field(nullable=False)
created_at: datetime.datetime = Field(
default=datetime.datetime.now(datetime.UTC), nullable=False
)
previous_version_id: int | None = Field(
default=None, foreign_key=f"{PROMPT_VERSION_TABLE_NAME}.id"
)

project: "ProjectTable" = Relationship(back_populates="prompt_versions")
4 changes: 4 additions & 0 deletions lilypad/app/models/table_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Table names for the database models."""

PROJECT_TABLE_NAME = "projects"
PROMPT_VERSION_TABLE_NAME = "prompt_versions"
45 changes: 45 additions & 0 deletions lilypad/app/templates/create_project.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HTMX Form</title>
<script src="https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js"></script>
<script
src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"
defer
></script>
</head>
<body
x-data="{
prompt_template: '{{ prompt_template }}',
projectName: '{{ project.name }}',
versionId: '{{ version_id }}'
}"
>
<button hx-get="/" hx-target="body" hx-push-url="true">Back</button>
<h1>{{ project.name }}</h1>
<form
hx-post="/projects/{{ project.name }}/versions"
hx-target="body"
hx-push-url="true"
>
<textarea
name="prompt_template"
rows="4"
cols="50"
x-model="prompt_template"
></textarea>
<br />
<button type="submit">Submit</button>
</form>
<div id="prompt-versions-list-container">
<h2>Prompt Versions:</h2>
<ul
id="prompt-versions-list"
hx-get="/projects/{{ project.name }}/versions"
hx-trigger="load"
></ul>
</div>
</body>
</html>
17 changes: 0 additions & 17 deletions lilypad/app/templates/index.html

This file was deleted.

5 changes: 5 additions & 0 deletions lilypad/app/templates/partials/project_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% for project in projects %}
<li>
<a href="projects/{{project.name}}" hx-push-url="true">{{ project.name }}</a>
</li>
{% endfor %}
1 change: 0 additions & 1 deletion lilypad/app/templates/partials/result.html

This file was deleted.

Loading

0 comments on commit 8ea8092

Please sign in to comment.