├── .gitignore
├── LICENSE
├── README.md
├── app
├── __init__.py
├── api.py
├── app.py
├── exception_handlers.py
├── htmy.py
├── i18n.py
└── main.py
├── lc
├── __init__.py
├── authenticator.py
├── chat
│ ├── __init__.py
│ ├── api.py
│ ├── bot
│ │ ├── __init__.py
│ │ ├── beer_corpus.py
│ │ ├── bot.py
│ │ ├── coffee_corpus.py
│ │ └── utils.py
│ └── model.py
├── fastapi.py
├── pages
│ ├── __init__.py
│ ├── chat_page.py
│ ├── index_page.md
│ ├── index_page.py
│ ├── page.py
│ └── profile_page.py
├── ui
│ ├── __init__.py
│ ├── base_page.html
│ ├── base_page.py
│ ├── centered.py
│ ├── chat_bubble.py
│ ├── chat_container.py
│ ├── chat_input.py
│ ├── copy_button.html
│ ├── copy_button.py
│ ├── dialog.html
│ ├── dialog.py
│ ├── login_dialog.py
│ ├── md.py
│ ├── navbar.html
│ ├── navbar.py
│ ├── not_found.py
│ ├── t_function.py
│ └── utils.py
└── user
│ ├── auth_api.py
│ └── model.py
├── locale
└── en.json
├── pyproject.toml
├── requirements.txt
├── uv.lock
└── vercel.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Vercel
10 | .vercel
11 |
12 | # Virtual environments
13 | .venv
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Peter Volf
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lipsum chat
2 |
3 | This project is a technology demonstration for server-side rendering with Python, FastAPI, and htmx; in the disguise of an AI chatbot that responds to questions about beer and coffee. More specifically the project showcases the following libraries:
4 |
5 | - [FastAPI](https://fastapi.tiangolo.com/): A modern, async, Python web framework.
6 | - [FastHX](https://volfpeter.github.io/fasthx/): Server-side rendering utility with HTMX support for FastAPI.
7 | - [htmy](https://volfpeter.github.io/htmy/): A powerful, async, pure-Python server-side rendering engine.
8 | - [htmx](https://htmx.org/): A JavaScript library for making AJAX requests and DOM updates using HTML attributes.
9 |
10 | Importantly, the project **does not** use Jinja or any other traditional templating engine. Instead, it uses `htmy` – in some cases with plain `html` and `markdown` snippets with no custom templating syntax –, so you can enjoy all the benefits of modern IDEs, linters, static code analysis tools, and coding assistants.
11 |
12 | Styling is done with [TailwindCSS v4](https://tailwindcss.com/) and [DaisyUI v5](https://daisyui.com/), but the project is not a TailwindCSS or DaisyUI demo, the focus is entirely on server-side rendering.
13 |
14 | ## Support
15 |
16 | Need help kickstarting a new project with this or a similar toolchain? [Get in touch!](https://www.volfp.com/contact)
17 |
18 | ## Getting started
19 |
20 | For a seamless experience, you need `uv` to be installed. If not available, you can find the required dependencies in `pyproject.toml`.
21 |
22 | With `uv` in place, all you need to do is run `uv run uvicorn app.main:app --reload` and open http://127.0.0.1:8000/ in your browser.
23 |
24 | `uv run` tools:
25 |
26 | - Linting: `ruff check .` or `ruff check . --fix`
27 | - Formatting: `ruff format .` or `ruff format . --check` for format check.
28 | - Static type analysis: `uv run mypy .`
29 |
30 | ## Deployment
31 |
32 | `vercel.json` contains the configuration for deployment on Vercel.
33 |
34 | The Vercel Python runtime only supports `requirements.txt`. The requirements file can be generated with `uv export --no-dev > requirements.txt`. It must be done manually before every push to the `main` branch.
35 |
36 | The Vercel Python runtime is experimental at this time (March 2025), so this config may become outdated relatively soon.
37 |
38 | ## Similar projects
39 |
40 | - [fastapi-htmx-tailwind-example](https://github.com/volfpeter/fastapi-htmx-tailwind-example): A similar, but slightly outdated technology demonstration using Jinja and MongDB, with more focus on HTMX.
41 |
42 | ## License - MIT
43 |
44 | The package is open-sourced under the conditions of the [MIT license](https://choosealicense.com/licenses/mit/).
45 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/volfpeter/lipsum-chat/6b228e1d18087f25cb52ff046553e41a573c7f7c/app/__init__.py
--------------------------------------------------------------------------------
/app/api.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI, HTTPException
2 |
3 | from lc.authenticator import Authenticator
4 | from lc.pages.chat_page import chat_page
5 | from lc.pages.index_page import index_page
6 | from lc.pages.profile_page import profile_page
7 | from lc.ui.navbar import ChatPageKey, chat_page_keys
8 |
9 | from .htmy import htmy
10 |
11 |
12 | def add_routes(app: FastAPI, *, auth: Authenticator) -> None:
13 | from lc.chat.api import make_api as make_chat_api
14 | from lc.user.auth_api import make_api as make_auth_api
15 |
16 | app.include_router(make_auth_api(auth, htmy))
17 | app.include_router(make_chat_api(htmy))
18 |
19 | @app.get("/ack", response_model=None)
20 | async def ack() -> None:
21 | """Just to have something to HTMX can call to trigger actions."""
22 | ...
23 |
24 | @app.get("/profile")
25 | @htmy.page(profile_page)
26 | async def me() -> None:
27 | """Profile page."""
28 | ...
29 |
30 | @app.get("/")
31 | @htmy.page(index_page)
32 | async def index() -> None:
33 | """Index page."""
34 | ...
35 |
36 | @app.get("/{page}")
37 | @htmy.page(chat_page)
38 | async def pages(page: ChatPageKey | str = "home") -> ChatPageKey:
39 | if page in chat_page_keys:
40 | return page # type: ignore[return-value]
41 |
42 | raise HTTPException(status_code=404)
43 |
--------------------------------------------------------------------------------
/app/app.py:
--------------------------------------------------------------------------------
1 | """
2 | App factory and configuration.
3 | """
4 |
5 | from fastapi import FastAPI
6 |
7 | from lc.authenticator import Authenticator
8 | from lc.ui.t_function import TFunction
9 |
10 | from .api import add_routes
11 | from .exception_handlers import add_exception_handlers
12 | from .htmy import htmy
13 | from .i18n import i18n
14 |
15 |
16 | def make_app() -> FastAPI:
17 | # Create the app and the necessary utilities.
18 | app = FastAPI()
19 | auth = Authenticator()
20 |
21 | # Add the auth middleware.
22 | app.middleware("http")(auth.middleware)
23 |
24 | # Register the htmy context processors.
25 | htmy.request_processors.extend(
26 | (
27 | # Authenticator
28 | auth.htmy_context_processor,
29 | # Translation function
30 | lambda r: TFunction(i18n, r.headers.get("Language-Selection-Not-Supported", "en")).to_context(),
31 | )
32 | )
33 |
34 | # Register exception handlers.
35 | add_exception_handlers(app)
36 |
37 | # Register all routes.
38 | add_routes(app, auth=auth)
39 |
40 | return app
41 |
--------------------------------------------------------------------------------
/app/exception_handlers.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI, HTTPException, Request
2 | from fastapi.responses import HTMLResponse
3 |
4 | from lc.ui.base_page import base_page
5 | from lc.ui.not_found import not_found
6 |
7 | from .htmy import htmy
8 |
9 |
10 | def add_exception_handlers(app: FastAPI) -> None:
11 | @app.exception_handler(401)
12 | @app.exception_handler(403)
13 | @app.exception_handler(404)
14 | async def handle_not_found(request: Request, _: HTTPException) -> HTMLResponse:
15 | return HTMLResponse(await htmy.render_component(base_page(not_found()), request))
16 |
--------------------------------------------------------------------------------
/app/htmy.py:
--------------------------------------------------------------------------------
1 | from fasthx.htmy import HTMY
2 |
3 | htmy = HTMY(no_data=True)
4 |
--------------------------------------------------------------------------------
/app/i18n.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from htmy.i18n import I18n
4 |
5 | _locale_dir = Path(__file__).parent.parent / "locale"
6 |
7 | i18n = I18n(_locale_dir)
8 |
--------------------------------------------------------------------------------
/app/main.py:
--------------------------------------------------------------------------------
1 | """
2 | The main entry point (application instance).
3 |
4 | Primarily for deployment.
5 | """
6 |
7 | from .app import make_app
8 |
9 | app = make_app()
10 |
--------------------------------------------------------------------------------
/lc/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/volfpeter/lipsum-chat/6b228e1d18087f25cb52ff046553e41a573c7f7c/lc/__init__.py
--------------------------------------------------------------------------------
/lc/authenticator.py:
--------------------------------------------------------------------------------
1 | from fastapi import HTTPException, Request, Response
2 | from htmy import Context
3 | from pydantic import ValidationError
4 |
5 | from .fastapi import FastAPIMiddlewareNextFunction
6 | from .user.model import User
7 |
8 |
9 | class Authenticator:
10 | """
11 | Authenticator with cookie-based login.
12 |
13 | It requires its middleware to be added to the FastAPI app.
14 |
15 | The authenticator also provides an `htmy` request processor that (if used)
16 | automatically adds the current user (may be `None`) to `htmy` rendering
17 | contexts under the `user` key.
18 | """
19 |
20 | __slots__ = ()
21 |
22 | def login(self, user: User, response: Response) -> Response:
23 | """Logs in the given user."""
24 | response.set_cookie("user", user.model_dump_json())
25 | return response
26 |
27 | def logout(self, response: Response) -> Response:
28 | """Logs out the current user."""
29 | response.delete_cookie("user")
30 | return response
31 |
32 | def current_user(self, request: Request) -> User | None:
33 | """FastAPI dependency that returns the current user."""
34 | return getattr(request.state, "user", None)
35 |
36 | async def middleware(self, request: Request, call_next: FastAPIMiddlewareNextFunction) -> Response:
37 | """
38 | FastAPI middleware that sets the `request.state.user` attribute to the current user.
39 | """
40 | cookie = request.cookies.get("user")
41 | if cookie:
42 | try:
43 | user: User | None = User.model_validate_json(cookie)
44 | request.state.user = user
45 | except ValidationError as e:
46 | raise HTTPException(status_code=401, detail="Invalid authentication token.") from e
47 |
48 | return await call_next(request)
49 |
50 | def htmy_context_processor(self, request: Request) -> Context:
51 | """
52 | `htmy` context processor that adds the current user to the rendering context.
53 | """
54 | return {"user": self.current_user(request)}
55 |
56 | def requires_anonymus(self, request: Request) -> None:
57 | """
58 | FastAPI dependency that raises an `HTTPException` if there is a logged in user.
59 | """
60 | user = self.current_user(request)
61 | if user is not None:
62 | HTTPException(status_code=403, detail="Forbidden")
63 |
64 | def requires_user(self, request: Request) -> User:
65 | """
66 | FastAPI dependency that returns the current user or raises an
67 | `HTTPException` if there is no logged in user.
68 | """
69 | user = self.current_user(request)
70 | if user is None:
71 | HTTPException(status_code=401, detail="Unauthorized")
72 |
73 | return user # type: ignore[return-value]
74 |
--------------------------------------------------------------------------------
/lc/chat/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/volfpeter/lipsum-chat/6b228e1d18087f25cb52ff046553e41a573c7f7c/lc/chat/__init__.py
--------------------------------------------------------------------------------
/lc/chat/api.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 |
3 | from fastapi import APIRouter, Form
4 | from fasthx.htmy import HTMY
5 | from htmy import Component, Text
6 |
7 | from lc.ui.chat_bubble import chat_bubble, chat_bubble_with_clipboard
8 | from lc.ui.chat_container import chat_container_ref
9 | from lc.ui.chat_input import chat_input
10 | from lc.ui.md import markdown
11 | from lc.ui.navbar import ChatPageKey
12 |
13 | from .bot import get_response
14 | from .model import Message
15 |
16 |
17 | def make_api(htmy: HTMY) -> APIRouter:
18 | api = APIRouter(prefix="/chat")
19 |
20 | @api.post("/{key}", response_model=None)
21 | @htmy.page(lambda result: result)
22 | async def chat(message: Annotated[Message, Form()], key: ChatPageKey) -> Component:
23 | return (
24 | chat_bubble(markdown(Text(message.message)), speaker="user"),
25 | chat_bubble_with_clipboard(*get_response(message.message, corpus=key)),
26 | chat_input(
27 | {
28 | "hx-swap-oob": "true",
29 | "hx-post": f"/chat/{key}",
30 | "hx-target": chat_container_ref,
31 | "hx-swap": "beforeend",
32 | }
33 | ),
34 | )
35 |
36 | return api
37 |
--------------------------------------------------------------------------------
/lc/chat/bot/__init__.py:
--------------------------------------------------------------------------------
1 | from .bot import get_response as get_response
2 |
--------------------------------------------------------------------------------
/lc/chat/bot/beer_corpus.py:
--------------------------------------------------------------------------------
1 | from .utils import make_word_sets
2 |
3 | sentences: tuple[str, ...] = (
4 | "Beer is one of the oldest and most widely consumed alcoholic drinks in the world.",
5 | "The brewing process involves fermentation of starches, typically from cereal grains.",
6 | "Beer can be made from barley, wheat, corn, or rice.",
7 | "The alcohol content of beer typically ranges from 4% to 6% by volume.",
8 | "Craft beer is a popular trend in the beer industry, emphasizing unique flavors and ingredients.",
9 | "Lager and ale are the two primary categories of beer.",
10 | "The hops used in beer provide bitterness, flavor, and aroma.",
11 | "Ales tend to be brewed at warmer temperatures than lagers.",
12 | "The word 'beer' comes from the Latin word 'bibere', meaning 'to drink'.",
13 | "Beer was a staple in ancient civilizations, including Egypt and Mesopotamia.",
14 | "The oldest known recipe for beer was discovered in ancient Sumerian tablets.",
15 | "The process of brewing beer involves mashing, boiling, fermenting, conditioning, and packaging.",
16 | "Beer is often served in a variety of glasses, including pint glasses, steins, and tulip glasses.",
17 | "The carbonation in beer is typically created by natural fermentation or by adding CO2 during packaging.",
18 | "IPAs (India Pale Ales) are known for their strong hop flavor and higher alcohol content.",
19 | "Pilsner is a type of pale lager that originated in the Czech Republic.",
20 | "Stouts are dark, rich beers with roasted malt flavors and a smooth finish.",
21 | "Porters are similar to stouts but typically have a slightly lighter body and flavor.",
22 | "Wheat beers are often cloudy and have a refreshing, light taste.",
23 | "Sours are beers intentionally brewed to have a tart or acidic flavor.",
24 | "Beer can be paired with food to enhance the flavors of both.",
25 | "Many people enjoy drinking beer while watching sports, especially during tailgates.",
26 | "Some breweries experiment with fruit and spices to create seasonal or unique flavors.",
27 | "Beers with higher alcohol content are often labeled as 'strong ales' or 'imperial' versions.",
28 | "The color of beer can range from pale yellow to deep brown, depending on the ingredients used.",
29 | "Pale ales are generally amber to copper in color, with a balanced hop flavor.",
30 | "A beer's head refers to the foam that forms on top when it is poured.",
31 | "A good beer should have a thick, lasting head that enhances the drinking experience.",
32 | "Beer is often served at different temperatures depending on the style, with lagers served colder than ales.",
33 | "The 'best before' date on a beer label refers to the beer's peak freshness, not an expiration date.",
34 | "Some beers are brewed with added spices, such as coriander or orange peel.",
35 | "Beer enthusiasts often participate in beer tastings to explore different styles and flavors.",
36 | "The alcohol by volume (ABV) of a beer indicates its strength, with higher ABV beers being stronger.",
37 | "Many cultures have traditional beer styles that reflect their unique brewing history.",
38 | "Beer is often served in bars, pubs, and restaurants, making it a popular social drink.",
39 | "Beer can be enjoyed straight from the bottle, in a glass, or in a can.",
40 | "Lagers are generally lighter in flavor and less hoppy than ales.",
41 | "Some beers, such as Belgian Tripels, are known for their fruity and spicy aromas.",
42 | "The perfect pour involves tilting the glass at a 45-degree angle to minimize excess foam.",
43 | "Beer can be made with a wide variety of additional ingredients, including fruit, spices, and herbs.",
44 | "Some breweries produce limited-edition beers that are available for a short time.",
45 | "The term 'session beer' refers to beers with a lower ABV, making them easy to drink over extended periods.",
46 | "Beer culture varies from country to country, with different traditions, beer styles, and drinking habits.",
47 | "The concept of a 'beer garden' originated in Germany and is a popular social gathering spot.",
48 | "Beer is often served at festivals, including Oktoberfest in Germany and the Great American Beer Festival.",
49 | "The brewing industry has seen a rise in microbreweries and homebrewers experimenting with different styles.",
50 | "Some beers undergo barrel aging, which imparts unique flavors from the wood.",
51 | "Beer can be made with a variety of yeasts, which contribute to the flavor and aroma profile.",
52 | "The first beer cans were introduced in the 1930s, changing the way beer was distributed and consumed.",
53 | "The process of aging beer can enhance its flavors, especially in styles like stouts and barleywines.",
54 | "Beer is often enjoyed as a refreshing beverage on hot summer days.",
55 | "Lager yeast ferments at cooler temperatures than ale yeast, leading to a cleaner flavor profile.",
56 | "Beer pairs well with a variety of foods, including pizza, burgers, and cheese.",
57 | "Some beers are brewed with exotic ingredients, such as coffee, chocolate, and vanilla.",
58 | "Belgian beers are known for their complex flavors, often with hints of fruit, spice, and earthiness.",
59 | "A brewery’s taproom is where customers can sample and purchase fresh, often exclusive beers.",
60 | "Beer is an essential part of many cultural and religious traditions worldwide.",
61 | "Some beers are fermented with wild yeast strains, producing funky or sour flavors.",
62 | "The concept of 'beer flight' refers to sampling small portions of several beers to compare flavors.",
63 | "Brewpubs combine breweries and restaurants, offering fresh beer and food in one location.",
64 | "Beer can be made with various malts, which contribute sweetness, color, and flavor.",
65 | "Beers can be categorized by their flavor profile, such as hoppy, malty, or fruity.",
66 | "Some people enjoy creating custom beer blends by mixing different beers together.",
67 | "Beer can be brewed to be low-carb, gluten-free, or alcohol-free for those with dietary preferences.",
68 | "Some beer styles, like barleywine, can be aged for several years to improve their flavors.",
69 | "Beer is commonly served in pints, which hold 16 fluid ounces in the U.S. and 20 fluid ounces in the UK.",
70 | "The term 'draft beer' refers to beer served directly from a keg rather than from a bottle or can.",
71 | "Beer drinkers often discuss the 'mouthfeel' of a beer, referring to its texture and body.",
72 | "A beer’s 'finish' refers to the lingering taste after swallowing.",
73 | "Beer is made with four main ingredients: water, malt, hops, and yeast.",
74 | "Some beers feature high hop content, which can impart a piney, floral, or citrusy taste.",
75 | "The world’s largest beer company is Anheuser-Busch InBev, known for brands like Budweiser and Corona.",
76 | "Many countries have their own beer laws, which regulate the strength, sale, and distribution of beer.",
77 | "Beer can be carbonated naturally during fermentation or artificially injected with CO2.",
78 | "A beer’s bitterness is measured in International Bitterness Units (IBUs), with higher numbers indicating a more bitter taste.",
79 | "Some beers are brewed with a mix of different malts, resulting in complex flavor profiles.",
80 | "Beer festivals celebrate the diverse range of beer styles and the craftsmanship of brewers.",
81 | "The IPA style of beer originated in England and was originally brewed to withstand long voyages to India.",
82 | "Beer can be paired with dessert, such as chocolate stouts with cake or fruit beers with sorbet.",
83 | "Many modern breweries use traditional brewing methods combined with innovative techniques.",
84 | "Beer is known to be a natural source of certain B vitamins, including folic acid.",
85 | "The most common beer glass shapes are pint glasses, tulip glasses, and snifters.",
86 | "The term 'lager' comes from the German word 'lagern', meaning 'to store', as lagers are aged at cool temperatures.",
87 | "Beer is one of the most versatile beverages, available in an almost endless variety of styles.",
88 | "Some beer aficionados take part in beer rating apps to document and compare their beer experiences.",
89 | "Many local craft breweries are focused on sustainability and eco-friendly brewing practices.",
90 | "Some people enjoy brewing their own beer at home, a hobby known as homebrewing.",
91 | "The craft beer movement in the U.S. has exploded in recent years, with thousands of breweries now in operation.",
92 | "Some breweries use special filtration techniques to remove yeast and other particles, resulting in clearer beer.",
93 | "Beer bottles come in different sizes, with the most common being 12 oz, 16 oz, and 22 oz.",
94 | "In some countries, beer is served alongside traditional pub foods, such as fish and chips or pretzels.",
95 | "The 'bitter' in bitter beers comes from the hops, which provide balance to the malt sweetness.",
96 | "Beers that are brewed with fruit are often referred to as fruit beers, which can be sweet or sour.",
97 | "Seasonal beers are often brewed for specific holidays or times of the year, such as pumpkin ales for fall.",
98 | "Some beers are brewed with a higher carbonation level, creating a fizzy, effervescent mouthfeel.",
99 | "Beers with a high malt content tend to have a sweet, bready flavor, while those with higher hop content are more bitter.",
100 | "The tradition of toasting with beer is thought to come from ancient customs of offering drinks to gods.",
101 | "The famous 'beer belly' is a result of consuming excess calories from alcohol and carbohydrates.",
102 | "Beer is sometimes used as an ingredient in cooking, with some dishes like beer-battered fish relying on beer for flavor and texture.",
103 | "Many beer enthusiasts collect beer cans or bottles as a hobby, often focusing on rare or vintage items.",
104 | "The popularity of beer continues to rise worldwide, with increasing numbers of people discovering new beer styles and flavors.",
105 | )
106 |
107 |
108 | word_sets = make_word_sets(sentences)
109 |
--------------------------------------------------------------------------------
/lc/chat/bot/bot.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | from htmy import ComponentSequence
4 |
5 | from lc.ui.navbar import ChatPageKey
6 |
7 | from .beer_corpus import sentences as beer
8 | from .beer_corpus import word_sets as beer_word_sets
9 | from .coffee_corpus import sentences as coffee
10 | from .coffee_corpus import word_sets as coffee_word_sets
11 | from .utils import weights_for_message
12 |
13 |
14 | def get_response(message: str, *, corpus: ChatPageKey) -> ComponentSequence:
15 | if corpus == "beer":
16 | sentences, word_sets = beer, beer_word_sets
17 | elif corpus == "coffee":
18 | sentences, word_sets = coffee, coffee_word_sets
19 | else:
20 | raise ValueError(f"Unknown corpus: {corpus}")
21 |
22 | return random.choices( # noqa: S311
23 | sentences,
24 | weights_for_message(message, word_sets=word_sets),
25 | k=random.randint(2, 7), # noqa: S311
26 | )
27 |
--------------------------------------------------------------------------------
/lc/chat/bot/coffee_corpus.py:
--------------------------------------------------------------------------------
1 | from .utils import make_word_sets
2 |
3 | sentences: tuple[str, ...] = (
4 | "Coffee is the perfect way to start your day.",
5 | "A cup of coffee can instantly boost your mood.",
6 | "Espresso is the base for many popular coffee drinks.",
7 | "There are countless ways to brew coffee, from French press to pour-over.",
8 | "Coffee beans are grown in tropical regions around the world.",
9 | "Dark roast coffee has a stronger, bolder flavor than light roast.",
10 | "Caffeine is the primary active ingredient in coffee.",
11 | "Some people prefer their coffee black, while others add milk or sugar.",
12 | "Drinking coffee can help improve concentration and alertness.",
13 | "Coffee is a popular beverage for social gatherings.",
14 | "A latte is a coffee drink made with espresso and steamed milk.",
15 | "Cappuccinos are topped with a layer of frothed milk.",
16 | "Coffee is a great way to fuel your productivity during work.",
17 | "The aroma of freshly brewed coffee can be very comforting.",
18 | "Cold brew coffee is a smooth and less acidic alternative to hot coffee.",
19 | "Many people enjoy a cup of coffee after a meal to aid digestion.",
20 | "Coffee has been enjoyed for centuries, dating back to the 15th century.",
21 | "The most popular coffee drink in the U.S. is drip coffee.",
22 | "A macchiato is an espresso drink with a small amount of milk.",
23 | "Coffee beans are actually the seeds of the coffee fruit.",
24 | "The word 'coffee' comes from the Arabic word 'qahwa'.",
25 | "Some people add flavored syrups to their coffee for extra sweetness.",
26 | "Coffee shops are a popular place to hang out, study, or work.",
27 | "Coffee has been linked to a lower risk of certain diseases, including Parkinson's.",
28 | "A good cup of coffee can be a real treat.",
29 | "There are many different types of coffee beans, including Arabica and Robusta.",
30 | "In Italy, espresso is an important part of daily life.",
31 | "Many people drink coffee to stay awake during long drives or study sessions.",
32 | "Decaffeinated coffee still has a small amount of caffeine.",
33 | "Coffee is best enjoyed fresh, right after it's brewed.",
34 | "A perfect espresso shot is often described as having a layer of crema on top.",
35 | "Some people drink coffee with a sprinkle of cinnamon or cocoa powder for extra flavor.",
36 | "Coffee has a rich, complex flavor profile that can vary based on its origin.",
37 | "The caffeine in coffee can help improve physical performance during exercise.",
38 | "In the morning, coffee often acts as a wake-up call.",
39 | "Coffee beans are roasted to develop their unique flavor characteristics.",
40 | "A flat white is a coffee drink similar to a latte, but with less foam.",
41 | "Coffee is often enjoyed with a sweet pastry or dessert.",
42 | "Coffee has become a global cultural phenomenon.",
43 | "The coffee industry is worth billions of dollars worldwide.",
44 | "Coffee can help you stay alert during a long workday.",
45 | "Some people love the ritual of brewing their own coffee every morning.",
46 | "A French press allows for a full-bodied and rich cup of coffee.",
47 | "Coffee is often a key ingredient in desserts like tiramisu and coffee cake.",
48 | "Some coffee drinkers swear by the health benefits of black coffee.",
49 | "Iced coffee is a refreshing way to enjoy coffee during the summer months.",
50 | "Caffeine is known to help with focus and mental clarity.",
51 | "Coffee can be a great conversation starter when meeting new people.",
52 | "A good cup of coffee can turn an ordinary moment into something special.",
53 | "Coffee is enjoyed by people of all ages around the world.",
54 | "Some coffee shops offer unique and creative coffee drinks.",
55 | "Drinking coffee in moderation can be part of a healthy lifestyle.",
56 | "The best coffee beans are often grown at high altitudes.",
57 | "Coffee is a key component of many traditional breakfast routines.",
58 | "Coffee culture has evolved over the years, with many new trends emerging.",
59 | "Some coffee drinkers prefer their coffee strong and bold, while others prefer it mild.",
60 | "Coffee is often used to make various coffee-based cocktails.",
61 | "The caffeine in coffee can help improve reaction time and coordination.",
62 | "Many people enjoy a cup of coffee to relax and unwind after a long day.",
63 | "Coffee can be a great companion while reading a book or working on a project.",
64 | "Many people find that coffee helps them feel more awake and alert in the morning.",
65 | "Coffee is often seen as a symbol of energy and productivity.",
66 | "Coffee beans undergo a complex process of roasting to bring out their flavors.",
67 | "A coffee grinder is an essential tool for fresh coffee enthusiasts.",
68 | "Coffee is sometimes paired with chocolate for a delicious combination.",
69 | "Espresso has a rich, concentrated flavor that many coffee lovers enjoy.",
70 | "The coffee bean's journey from farm to cup involves many steps.",
71 | "Iced coffee can be made at home by brewing coffee and chilling it.",
72 | "Many people enjoy drinking coffee while people-watching at cafés.",
73 | "Coffee is a source of antioxidants, which can be beneficial to your health.",
74 | "Drinking coffee can improve mental performance, especially in the morning.",
75 | "Coffee can be customized in many ways with various types of milk and sweeteners.",
76 | "A pour-over coffee maker provides control over the brewing process.",
77 | "Coffee consumption is a daily ritual for millions of people worldwide.",
78 | "Coffee's popularity has inspired countless cafés and specialty coffee shops.",
79 | "A coffee mug can be a comforting object that brings warmth and happiness.",
80 | "Coffee is often enjoyed as a midday pick-me-up.",
81 | "The coffee industry continues to innovate with new brewing methods and technologies.",
82 | "Coffee can be enjoyed in both hot and cold forms, depending on your preference.",
83 | "Many people love the ritual of sipping coffee while starting their day.",
84 | "Coffee has a complex flavor that varies depending on its roast and origin.",
85 | "Some coffee aficionados are always searching for the perfect cup of coffee.",
86 | "A well-made cup of coffee can be a work of art in itself.",
87 | "Drinking coffee can help you power through tough tasks or long hours of work.",
88 | "Coffee is a social drink, often shared with friends or colleagues.",
89 | "Coffee beans are sometimes blended to create unique flavor profiles.",
90 | "Some people prefer to drink their coffee with a splash of cream or milk.",
91 | "Coffee lovers enjoy experimenting with different brewing techniques and methods.",
92 | "The caffeine in coffee can provide a temporary energy boost.",
93 | "In many cultures, coffee is enjoyed with a sense of ceremony and tradition.",
94 | "Some people drink coffee to help with focus during study sessions or work meetings.",
95 | "The rich, bold flavor of coffee can provide a comforting sense of warmth.",
96 | "Many people find that drinking coffee helps them feel more productive and energetic.",
97 | "Coffee is often served with a variety of pastries or snacks at cafés.",
98 | "Some people prefer to drink their coffee black, while others enjoy adding flavors like vanilla or caramel.",
99 | "Coffee has a way of bringing people together for conversation and bonding.",
100 | "Caffeine sensitivity varies from person to person, with some people able to handle more than others.",
101 | "Coffee is sometimes used in desserts like ice cream or cakes for its rich flavor.",
102 | "A good coffee grinder is essential for ensuring that your coffee is ground to the right consistency.",
103 | "The coffee plant requires a specific climate and altitude to grow well.",
104 | "Coffee beans can have a range of flavors, from fruity to nutty to chocolaty.",
105 | "In many parts of the world, coffee is an integral part of the daily routine.",
106 | "Coffee is a drink that people often enjoy while relaxing or working on projects.",
107 | "Many coffee lovers appreciate the ritual of brewing their coffee just the way they like it.",
108 | "Coffee is known for its ability to provide a boost of energy and alertness.",
109 | "Espresso is often used as the base for other popular coffee drinks like lattes and cappuccinos.",
110 | "A well-made cup of coffee can be a satisfying and enjoyable experience.",
111 | )
112 |
113 |
114 | word_sets = make_word_sets(sentences)
115 |
--------------------------------------------------------------------------------
/lc/chat/bot/utils.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | _splitter = re.compile(r"[^a-z0-9]+")
4 |
5 |
6 | def make_word_sets(sentences: tuple[str, ...]) -> tuple[set[str], ...]:
7 | """
8 | Returns a tuple of sets of words from the given sentences.
9 |
10 | The order of the sets matches the order of the sentences.
11 | """
12 | return tuple(set(re.split(_splitter, sentence.lower())) for sentence in sentences)
13 |
14 |
15 | def weights_for_message(sentence: str, *, word_sets: tuple[set[str], ...]) -> tuple[float, ...]:
16 | """
17 | Returns a tuple of weights for the given sentence.
18 | """
19 | words = set(re.split(_splitter, sentence.lower()))
20 | return tuple(1 + len(words & word_set) * 29 for word_set in word_sets)
21 |
--------------------------------------------------------------------------------
/lc/chat/model.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 |
4 | class Message(BaseModel):
5 | message: str
6 |
--------------------------------------------------------------------------------
/lc/fastapi.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Callable
2 | from typing import Awaitable, TypeAlias
3 |
4 | from fastapi import Request, Response
5 |
6 | FastAPIMiddlewareNextFunction: TypeAlias = Callable[[Request], Awaitable[Response]]
7 | FastAPIMiddlewareFunction: TypeAlias = Callable[
8 | [Request, FastAPIMiddlewareNextFunction], Awaitable[Response]
9 | ]
10 |
--------------------------------------------------------------------------------
/lc/pages/__init__.py:
--------------------------------------------------------------------------------
1 | """Application and model-specific UI components."""
2 |
--------------------------------------------------------------------------------
/lc/pages/chat_page.py:
--------------------------------------------------------------------------------
1 | from htmy import Component, Context, component, html
2 |
3 | from lc.ui.chat_bubble import chat_bubble
4 | from lc.ui.chat_container import chat_container, chat_container_ref
5 | from lc.ui.chat_input import chat_input
6 | from lc.ui.navbar import ChatPageKey
7 | from lc.ui.t_function import TFunction
8 |
9 | from .page import page
10 |
11 |
12 | @component
13 | async def chat_page(key: ChatPageKey, ctx: Context) -> Component:
14 | t = TFunction.from_context(ctx)
15 | return page(
16 | html.div(
17 | chat_container(
18 | chat_bubble(await t(f"chat_page.{key}.hi")),
19 | chat_bubble(await t(f"chat_page.{key}.message")),
20 | chat_bubble(await t(f"chat_page.{key}.question")),
21 | # hx-swap=beforeend show:bottom should work on the chat input, but
22 | # it doesn't, so we trigger scroll to bottom manually.
23 | **{"hx-on:htmx:after-settle": "this.scroll(undefined, this.scrollHeight)"},
24 | ),
25 | chat_input(
26 | {
27 | "hx-post": f"/chat/{key}",
28 | "hx-target": chat_container_ref,
29 | "hx-swap": "beforeend",
30 | }
31 | ),
32 | class_="flex flex-col px-6 gap-2 h-full w-full overflow-hidden",
33 | ),
34 | )
35 |
--------------------------------------------------------------------------------
/lc/pages/index_page.md:
--------------------------------------------------------------------------------
1 | # What is this?
2 |
3 | This project is a technology demonstration for server-side rendering with Python, FastAPI, and htmx; in the disguise of an AI chatbot that responds to questions about beer and coffee. More specifically the project showcases the following libraries:
4 |
5 | - [FastAPI](https://fastapi.tiangolo.com/): A modern, async, Python web framework.
6 | - [FastHX](https://volfpeter.github.io/fasthx/): Server-side rendering utility with HTMX support for FastAPI.
7 | - [htmy](https://volfpeter.github.io/htmy/): A powerful, async, pure-Python server-side rendering engine.
8 | - [htmx](https://htmx.org/): A JavaScript library for making AJAX requests and DOM updates using HTML attributes.
9 |
10 | Importantly, the project **does not** use Jinja or any other traditional templating engine. Instead, it uses `htmy` – in some cases with plain `html` and `markdown` snippets with no custom templating syntax –, so you can enjoy all the benefits of modern IDEs, linters, static code analysis tools, and coding assistants.
11 |
12 | Styling is done with [TailwindCSS v4](https://tailwindcss.com/) and [DaisyUI v5](https://daisyui.com/), but the project is not a TailwindCSS or DaisyUI demo, the focus is entirely on server-side rendering.
13 |
14 | # Is it open source?
15 |
16 | This project is fully open source, you can find the code on [GitHub](https://github.com/volfpeter/lipsum-chat).
17 |
18 | # Is my data secure?
19 |
20 | The backend **does not** store or process any data, so yes, your data is fully secure. 😌
21 |
--------------------------------------------------------------------------------
/lc/pages/index_page.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from htmy import Component
4 |
5 | from lc.ui.md import markdown
6 | from lc.ui.utils import replace_py_extension
7 |
8 | from .page import page
9 |
10 |
11 | def index_page(_: Any) -> Component:
12 | """
13 | Component factory for the index page.
14 | """
15 | return page(markdown(replace_py_extension(__file__, "md")))
16 |
--------------------------------------------------------------------------------
/lc/pages/page.py:
--------------------------------------------------------------------------------
1 | from htmy import ComponentType, Context, component, html
2 |
3 | from lc.ui.base_page import base_page
4 | from lc.ui.navbar import navbar
5 | from lc.ui.t_function import TFunction
6 | from lc.user.model import User
7 |
8 |
9 | def login_button(title: str) -> html.button:
10 | return html.button(
11 | title,
12 | hx_get="/auth/login-dialog",
13 | hx_target="#dialogs",
14 | class_="btn btn-primary",
15 | )
16 |
17 |
18 | @component
19 | async def page(content: ComponentType, ctx: Context) -> ComponentType:
20 | """
21 | Page with navbar.
22 | """
23 | user: User | None = ctx.get("user")
24 | t = TFunction.from_context(ctx)
25 |
26 | user_menu: ComponentType
27 | if user is None:
28 | user_menu = login_button(await t("page.button.login"))
29 | else:
30 | user_menu = user.user_menu()
31 |
32 | return base_page(
33 | html.div(
34 | navbar(user_menu),
35 | html.div(content, class_="flex flex-col h-full overflow-auto"),
36 | class_="flex flex-col h-full w-full p-4 gap-8 lg:w-5xl", # 5xl is the lg breakpoint width
37 | )
38 | )
39 |
--------------------------------------------------------------------------------
/lc/pages/profile_page.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from htmy import Component, Context, component, html
4 |
5 | from lc.ui.centered import centered
6 | from lc.ui.t_function import TFunction
7 | from lc.user.model import User
8 |
9 | from .page import page
10 |
11 |
12 | @component
13 | async def profile_page(_: Any, ctx: Context) -> Component:
14 | user: User | None = ctx.get("user")
15 | t = TFunction.from_context(ctx)
16 |
17 | return page(
18 | centered(
19 | html.div(
20 | html.label(await t("profile_page.username"), class_="font-semibold"),
21 | html.label(user.username if user else await t("profile_page.anonymus")),
22 | html.label(await t("profile_page.logged_in_at"), class_="font-semibold"),
23 | html.label(
24 | user.logged_in_at if user else await t("profile_page.not_logged_in"),
25 | class_="text-warning" if user is None else None,
26 | ),
27 | class_="grid grid-cols-[max-content_1fr] gap-4",
28 | )
29 | )
30 | )
31 |
--------------------------------------------------------------------------------
/lc/ui/__init__.py:
--------------------------------------------------------------------------------
1 | """Generic UI components that don't rely on the model or the application."""
2 |
--------------------------------------------------------------------------------
/lc/ui/base_page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |