diff --git a/.coveragerc b/.coveragerc index 3dbfbb4..c712d25 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,2 @@ [run] -omit = tests/* \ No newline at end of file +omit = tests/* diff --git a/.gitignore b/.gitignore index 16c5cfa..dbad549 100644 --- a/.gitignore +++ b/.gitignore @@ -180,4 +180,4 @@ ollama *.kate-swp # sphinx rst files docs/source/_modules - +poetry.lock diff --git a/Dockerfile b/Dockerfile index 2dd750c..ba1d3d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,6 @@ RUN apt update && apt install curl bash jq RUN pip install poetry RUN poetry install -v -FROM base as dev - # Expose development port EXPOSE 5000 diff --git a/docker-compose.yml b/docker-compose.yml index 15fa1cd..dd64cda 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,6 @@ services: senju: build: context: . - target: dev ports: - "127.0.0.1:5000:5000" volumes: diff --git a/docs/auto_docu.sh b/docs/auto_docu.sh old mode 100644 new mode 100755 diff --git a/docs/source/conf.py b/docs/source/conf.py index 3a1a6eb..f7adaa5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,10 +3,13 @@ # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +from __future__ import annotations + # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information import os import sys + sys.path.insert(0, os.path.abspath("../../senju")) project = 'senju' diff --git a/docs/source/index.rst b/docs/source/index.rst index aa7c231..d4ee233 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,4 +16,4 @@ documentation for details. :caption: Contents: usage - _modules/modules \ No newline at end of file + _modules/modules diff --git a/entrypoint.sh b/entrypoint.sh index a0564d5..63c9f1f 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e # First create a readable multiline string SYSTEM_PROMPT=$(cat <=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "waitress" +version = "3.0.2" +description = "Waitress WSGI server" +optional = false +python-versions = ">=3.9.0" +groups = ["main"] +files = [ + {file = "waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e"}, + {file = "waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f"}, +] + +[package.extras] +docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"] +testing = ["coverage (>=7.6.0)", "pytest", "pytest-cov"] + [[package]] name = "werkzeug" version = "3.1.3" @@ -1732,4 +1748,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.13" -content-hash = "b7a4de48ebf806e2217d5e7a8d2bc3b39babdaacbd09705b93c86c66845111a6" +content-hash = "5492deb1adff40a0e4571b4520e4c19449090cffa82a9b25308b6d7575ca4933" diff --git a/pyproject.toml b/pyproject.toml index a10ccd8..49b85d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "pillow (>=11.1.0,<12.0.0)", "torch (>=2.6.0,<3.0.0)", "transformers (>=4.50.0,<5.0.0)", + "waitress (>=3.0.2,<4.0.0)", ] diff --git a/senju/haiku.py b/senju/haiku.py index 4854319..e9b322a 100644 --- a/senju/haiku.py +++ b/senju/haiku.py @@ -1,3 +1,66 @@ +""" +Haiku Generation Module +======================= + +A client interface for AI-powered haiku poem generation. + +This module provides the core functionality for communicating +with an Ollama-based +AI service to generate three-line haiku poems. It handles the +entire generation +process, from sending properly formatted requests to processing +and validating +the returned poems. + +Classes +------- +Haiku + A dataclass representation of a haiku poem, providing structure +for storage, + manipulation and serialization of poem data. + + **Methods**: + + * ``to_json()``: Converts a haiku instance to JSON format for API + responses + * ``generate_haiku(seed_text)``: Creates a new haiku using + the AI service + +Constants +--------- +AI_SERVICE_URL + The endpoint URL for the Ollama API service. + +AI_MODEL_NAME + The specific AI model used for haiku generation. + +REQUEST_TIMEOUT + The maximum time (in seconds) to wait for AI service responses. + +Dependencies +------------ +* requests: HTTP client library for API communication +* dataclasses: Support for the Haiku data structure +* logging: Error and diagnostic information capture +* json: Processing of API responses + +Implementation Details +---------------------- +The module implements a robust communication pattern with the +AI service, including: + +1. Proper request formatting with seed text integration +2. Multiple retry attempts for handling temporary service issues +3. Response validation to ensure the returned text follows haiku structure +4. Fallback mechanisms when the AI service returns unsuitable content +5. JSON serialization for consistent data exchange + +When communicating with the AI service, the module maintains appropriate +error handling and logging to help diagnose any generation issues. It aims +to provide a reliable haiku generation experience even when dealing with the +inherent unpredictability of AI-generated content. +""" + from __future__ import annotations import json @@ -12,16 +75,43 @@ AI_GEN_ENDPOINT: str = "/generate" @dataclass class Haiku: + """ + A class representing a haiku poem with three lines. + + :ivar lines: A list containing the three lines of the haiku. + :type lines: list[str] + """ lines: list[str] def get_json(self): + """ + Converts the haiku lines to a JSON string. + + :return: A JSON string representation of the haiku lines. + :rtype: str + """ return json.dumps(self.lines) @staticmethod def request_haiku(seed: str, url=AI_BASE_URL + AI_GEN_ENDPOINT) -> Haiku: - """This function prompts the ai to generate - the hauku based on the user input""" + """ + Generates a haiku using an AI model based on the + provided seed text. + This function prompts the AI to generate a haiku based on the + user input. + It validates that the response contains exactly 3 lines. + The function will retry until a valid haiku is generated. + + :param seed: The input text used to inspire the haiku generation. + :param url: The URL to the AI endpoint + :type seed: str + :return: A new Haiku object containing the generated three lines. + :rtype: Haiku + + :raises: Possible JSONDecodeError which is caught and handled + with retries. + """ ai_gen_request = { "model": "haiku", "prompt": f"{seed}", @@ -30,6 +120,7 @@ class Haiku: } tries = 0 + while True: tries += 1 try: @@ -62,8 +153,8 @@ class Haiku: lines[1], lines[2] ]) - break + except json.JSONDecodeError as e: logging.error(f"error while reading json from LLM: {e}") raise e diff --git a/senju/main.py b/senju/main.py index 4390363..72e0e65 100644 --- a/senju/main.py +++ b/senju/main.py @@ -1,16 +1,58 @@ +""" +Senju Haiku Web Application +=========================== + +A Flask-based web interface for generating, viewing, and managing haiku poetry. + +This application provides a comprehensive interface between users +and an AI-powered +haiku generation service, with persistent storage capabilities. +Users can interact +with the system through both a web interface and a RESTful API. + +Features +-------- +* **Landing page**: Welcome interface introducing users to the Senju service +* **Browsing interface**: Gallery-style viewing of previously generated haikus +* **Prompt interface**: Text input system for generating haikus from seed text +* **Image scanning**: Experimental interface for creating haikus + from visual inputs +* **RESTful API**: Programmatic access for integration with other services + +Architecture +------------ +The application implements a RESTful architecture using Flask's routing system +and template rendering. All user interactions are handled through +clearly defined +routes, with appropriate error handling for exceptional cases. + +Dependencies +------------ +* future.annotations: Enhanced type hint support +* os, Path: Filesystem operations for storage management +* Flask: Core web application framework +* Haiku: Custom class for poem representation and generation +* StoreManager: Database abstraction for persistence operations + +Implementation +-------------- +The module initializes both a Flask application instance and a StoreManager +with a configured storage location. All routes and view functions required +for the complete web interface are defined within this module. +""" + from __future__ import annotations +import os from pathlib import Path -from flask import (Flask, redirect, render_template, request, url_for, - send_from_directory) +from flask import (Flask, redirect, render_template, request, + send_from_directory, url_for) from senju.haiku import Haiku from senju.image_reco import gen_response from senju.store_manager import StoreManager -import os - app = Flask(__name__) store = StoreManager(Path("/tmp/store.db")) @@ -18,11 +60,24 @@ store = StoreManager(Path("/tmp/store.db")) @app.route("/") def index_view(): + """ + Render the main index page of the application. + + :return: The index.html template with title "Senju". + :rtype: flask.Response + """ return render_template("index.html", title="Senju") @app.route("/haiku/") def haiku_index_view(): + """ + Redirect to the most recently created haiku. + + :return: Redirects to the haiku_view route with the latest haiku ID. + :rtype: flask.Response + :raises KeyError: If no haikus exist in the store yet. + """ haiku_id: int | None = store.get_id_of_latest_haiku() if haiku_id is None: haiku_id = 0 @@ -31,14 +86,29 @@ def haiku_index_view(): @app.route("/haiku/") def haiku_view(haiku_id): - """test""" + """ + Display a specific haiku by its ID. + + Loads the haiku with the given ID from the store and renders it using + the haiku.html template. If no haiku is found with the provided ID, + raises a KeyError. + + :param haiku_id: The ID of the haiku to display. + :type haiku_id: int + :return: The haiku.html template with the haiku data in context. + :rtype: flask.Response + :raises KeyError: If no haiku exists with the given ID. + """ + haiku: Haiku | None = store.load_haiku(haiku_id) + if haiku is None: + # TODO: add "haiku not found" page + raise KeyError("haiku not found") is_default: bool = request.args.get("is_default") == "1" haiku: Haiku = store.load_haiku(haiku_id) context: dict = { "haiku": haiku, "is_default": is_default } - return render_template( "haiku.html", context=context, @@ -47,6 +117,12 @@ def haiku_view(haiku_id): @app.route("/prompt") def prompt_view(): + """ + Render the haiku generation prompt page. + + :return: The prompt.html template with title "Haiku generation". + :rtype: flask.Response + """ return render_template( "prompt.html", title="Haiku generation" @@ -55,6 +131,12 @@ def prompt_view(): @app.route("/scan") def scan_view(): + """ + Render the image scanning page. + + :return: The scan.html template with title "Image scanning". + :rtype: flask.Response + """ return render_template( "scan.html", title="Image scanning" @@ -79,6 +161,17 @@ def image_recognition(): @app.route("/api/v1/haiku", methods=['POST']) def generate_haiku(): + """ + API endpoint to generate a new haiku based on the provided prompt. + + Accepts POST requests with JSON data containing a 'prompt' field. + Generates a haiku using the prompt, saves it to the store, + and returns the ID. + + :return: The ID of the newly created haiku if method is POST. + Error message and status code 405 if method is not POST. + :rtype: Union[str, Tuple[str, int]] + """ if request.method == 'POST': json_data = request.get_json() prompt = json_data["prompt"] @@ -93,6 +186,12 @@ def generate_haiku(): @app.route('/favicon.ico') def favicon(): + """ + Serve the favicon.ico file from the static directory. + + :return: The favicon.ico file with the appropriate MIME type. + :rtype: flask.Response + """ return send_from_directory(os.path.join(app.root_path, 'static/img'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') diff --git a/senju/store_manager.py b/senju/store_manager.py index d55d0c9..e94295b 100644 --- a/senju/store_manager.py +++ b/senju/store_manager.py @@ -1,3 +1,53 @@ +""" +Senju Database Management Module +================================ + +A database interaction layer for the Senju haiku management system. + +This module implements a lightweight document database +abstraction using TinyDB +for persistent storage of haiku poems. It provides a +clean interface for storing, +retrieving, updating, and managing haiku entries in the system. + +Classes +------- +StoreManager + The primary class responsible for all database operations. + Handles connection + management, CRUD operations, and query capabilities for haiku data. + +Functions +--------- +utility_function + Provides simple arithmetic operations to support + database functionalities. + +Constants +--------- +DEFAULT_DB_PATH + The default filesystem location for the TinyDB database file + (/var/lib/senju.json). + +Dependencies +------------ +* future.annotations: Enhanced type hint support +* logging.Logger: Diagnostic and error logging capabilities +* pathlib.Path: Cross-platform filesystem path handling +* typing.Optional: Type annotations for nullable values +* tinydb.TinyDB: Lightweight document database implementation +* tinydb.QueryImpl: Query builder for database searches +* senju.haiku.Haiku: Data model for haiku representation + +Implementation Details +---------------------- +The module uses TinyDB as its storage engine, providing a JSON-based document +storage solution that balances simplicity with functionality. The StoreManager +abstracts all database operations behind a clean API, +handling connection lifecycle +and providing methods for common operations on haiku data. +""" + from __future__ import annotations from logging import Logger @@ -22,11 +72,34 @@ class BadStoreManagerFileError(Exception): class StoreManager: + """ + Manages the storage and retrieval of haiku + data using TinyDB. + + This class provides an interface for saving and + loading haikus from + a TinyDB database file. + + :ivar _db: Database instance for storing haiku data. + :type _db: TinyDB + :ivar logger: Logger for tracking operations and errors. + :type logger: Logger + """ __slots__ = "_db", "logger" _db: TinyDB logger: Logger def __init__(self, path_to_db: Path = DEFAULT_DB_PATH) -> None: + """ + Initialize the StoreManager with a database path. + + :param path_to_db: Path to the TinyDB database file. + Defaults to DEFAULT_DB_PATH. + :type path_to_db: Path, optional + :return: None + """ + self._db = TinyDB(path_to_db) + try: self._db = TinyDB(path_to_db) except Exception as e: @@ -34,9 +107,29 @@ class StoreManager: self.logger = Logger(__name__) def _query(self, query: QueryImpl) -> list[dict]: + """ + Execute a query against the database. + + :param query: TinyDB query to execute. + :type query: QueryImpl + :return: List of documents matching the query. + :rtype: list[dict] + """ return self._db.search(query) def _load(self, id: int) -> Optional[dict]: + """ + Load a document by its ID. + + :param id: Document ID to load. + :type id: int + :return: The document if found, None otherwise. + :rtype: Optional[dict] + + .. note:: + Logs a warning if document with specified + ID is not found. + """ try: return self._db.get(doc_id=id) except IndexError as e: @@ -44,9 +137,25 @@ class StoreManager: return None def _save(self, data: dict) -> int: + """ + Save a document to the database. + + :param data: Document data to save. + :type data: dict + :return: The document ID of the saved document. + :rtype: int + """ return self._db.insert(data) def load_haiku(self, key: Optional[int]) -> Haiku: + """ + Load a haiku by its ID. + + :param key: The ID of the haiku to load. + :type key: int + :return: A Haiku object if found, None otherwise. + :rtype: Optional[Haiku] + """ if key is None: return DEFAULT_HAIKU raw_haiku: dict | None = self._load(key) @@ -56,9 +165,27 @@ class StoreManager: return h def save_haiku(self, data: Haiku) -> int: + """ + Save a haiku to the database. + + :param data: The Haiku object to save. + :type data: Haiku + :return: The document ID of the saved haiku. + :rtype: int + """ return self._save(data.__dict__) def get_id_of_latest_haiku(self) -> Optional[int]: + """ + Get the ID of the most recently added haiku. + + :return: The ID of the latest haiku if any exists, + None otherwise. + :rtype: Optional[int] + + .. note:: + Logs an error if the database is empty. + """ try: id = self._db.all()[-1].doc_id return id diff --git a/senju/templates/haiku.html b/senju/templates/haiku.html index c3d4382..e58b31d 100644 --- a/senju/templates/haiku.html +++ b/senju/templates/haiku.html @@ -1,15 +1,25 @@ {% extends "base.html" %} - {% block content %}

{{ title }}

-

+

{% for line in context.haiku.lines %} {{ line }}
{% endfor %}

+
+ + +
{% if context.is_default %}
@@ -17,9 +27,209 @@
{% endif %} + class="inline-block bg-violet-600 hover:bg-violet-700 text-white font-bold py-2 px-4 rounded-lg mt-6"> Back to Home
+ {% endblock %} diff --git a/senju/templates/prompt.html b/senju/templates/prompt.html index 46befeb..77b2825 100644 --- a/senju/templates/prompt.html +++ b/senju/templates/prompt.html @@ -1,28 +1,25 @@ -{% extends "base.html" %} {% block content %} -
-
-

- Very 1337 prompt input -

-
- - +{% extends "base.html" %} + +{% block content %} +
+
+

Very 1337 prompt input

+
+ + +