Compare commits

...

5 commits

Author SHA1 Message Date
9ec2f1d89a
feat: Add Dockerfile 2025-06-12 22:35:56 +02:00
836e7b565b
fix: Expose app by default 2025-06-12 22:35:56 +02:00
2514ad1296
fix: Make prompts try to adhere to original length, more lefty 2025-06-12 22:35:55 +02:00
63131b7f11
feat: Add single rewrite method to LLM models 2025-06-12 22:35:54 +02:00
333d825cb7
ref: Allow passing custom prompts to Groq methods
Rudimentary implementation currently. Would probably ultimately help to
have a 'prompt' domain model which can be used something like this:

Prompt(type=Prompt.SUGGESTIONS).leaning("left").current(2025).area("politics").view("American")

That way it would be easy to build your own prompt.
2025-06-12 22:35:54 +02:00
5 changed files with 168 additions and 12 deletions

86
Dockerfile Normal file
View file

@ -0,0 +1,86 @@
FROM python:3.13-slim-bookworm AS build
SHELL ["sh", "-exc"]
ENV DEBIAN_FRONTEND=noninteractive
RUN <<EOT
apt-get update -qy
apt-get install -qyy \
build-essential \
ca-certificates \
python3-setuptools \
EOT
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# no warnings about missing hardlinking
ENV UV_LINK_MODE=copy \
# bytecode compilation for fast startup
UV_COMPILE_BYTECODE=1 \
# don't download isolated python setups
UV_PYTHON_DOWNLOADS=never \
# stick to debian available
UV_PYTHON=python3.13 \
# set our working env
UV_PROJECT_ENVIRONMENT=/app
## END of build prepping
# sync all DEPENDENCIES (without actual application)
RUN --mount=type=cache,target=/root/.cache \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync \
--locked \
--no-dev \
--no-install-project
# sync app itself
COPY . /src
WORKDIR /src
RUN --mount=type=cache,target=/root/.cache \
uv sync \
--locked \
--no-dev \
--no-editable
############################ APP CONTAINER ############################
FROM python:3.13-slim-bookworm
SHELL ["sh", "-exc"]
ENV PATH=/app/bin:$PATH
# run app rootless
RUN <<EOT
groupadd -r app
useradd -r -d /app -g app -N app
EOT
ENTRYPOINT ["tini", "-v", "--", "prophet"]
STOPSIGNAL SIGINT
RUN <<EOT
apt-get update -qy
apt-get install -qyy \
python3.11 \
libpython3.11 \
tini
apt-get clean
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
EOT
COPY --from=build --chown=app:app /app /app
COPY --from=build --chown=app:app /src/static /app/static
COPY --from=build --chown=app:app /src/templates /app/templates
USER app
WORKDIR /app
RUN <<EOT
python -V
python -Im site
python -Ic 'import prophet'
EOT

View file

@ -122,7 +122,7 @@ async def fetch_update(debug_print: bool = True):
def start() -> None: def start() -> None:
from uvicorn import run from uvicorn import run
run("prophet.app:app", reload=True) run("prophet.app:app", reload=True, host="0.0.0.0")
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,10 +1,11 @@
from typing import Protocol from typing import Protocol
from prophet.domain.improvement import Improvement
from prophet.domain.original import Original from prophet.domain.original import Original
class LLMClient(Protocol): class LLMClient(Protocol):
def get_alternative_title_suggestions(self, original_content: str) -> str: def rewrite(self, original: Original) -> Improvement:
raise NotImplementedError raise NotImplementedError
def rewrite_title( def rewrite_title(
@ -16,3 +17,6 @@ class LLMClient(Protocol):
self, original: Original, improved_title: str | None = None self, original: Original, improved_title: str | None = None
) -> str: ) -> str:
raise NotImplementedError raise NotImplementedError
def get_alternative_title_suggestions(self, original_content: str) -> str:
raise NotImplementedError

View file

@ -1,9 +1,14 @@
from typing import override
from groq import Groq from groq import Groq
from prophet.config import AiConfig from prophet.config import AiConfig
from prophet.domain.improvement import Improvement
from prophet.domain.llm import LLMClient from prophet.domain.llm import LLMClient
from prophet.domain.original import Original from prophet.domain.original import Original
AVOID_SHOCKING_TURN_OF_EVENTS: bool = True
class GroqClient(LLMClient): class GroqClient(LLMClient):
config_ai: AiConfig config_ai: AiConfig
@ -15,12 +20,42 @@ class GroqClient(LLMClient):
self.config_ai = config_ai if config_ai else AiConfig.from_env() self.config_ai = config_ai if config_ai else AiConfig.from_env()
self.client = client if client else Groq(api_key=self.config_ai.API_KEY) self.client = client if client else Groq(api_key=self.config_ai.API_KEY)
def get_alternative_title_suggestions(self, original_content: str) -> str: @override
def rewrite(self, original: Original) -> Improvement:
suggestions = self.get_alternative_title_suggestions(original.title)
new_title = self.rewrite_title(original.title, suggestions)
new_summary = self.rewrite_summary(original, new_title)
return Improvement(original=original, title=new_title, summary=new_summary)
@override
def get_alternative_title_suggestions(
self, original_content: str, custom_prompt: str | None = None
) -> str:
prompt = (
custom_prompt
if custom_prompt
else """
Political context: We are in the year 2025, Donald Trump is
President of the United States again. There has been a crackdown on
'illegal' immigration, with controversial disappearings happening
almost every day. Many are calling the United States an
increasingly fascist state.
You are a comedy writer at a left-leaning satirical newspaper.
Improve on the following satirical headline. Your new headline is
funny, can involve current political events. and has an edge to it.
It should be roughly the length of the original headline. Print
only new suggestions, with one suggestion on each line.
"""
)
suggestions = self.client.chat.completions.create( suggestions = self.client.chat.completions.create(
messages=[ messages=[
{ {
"role": "system", "role": "system",
"content": "You are a comedy writer at a satirical newspaper. Improve on the following satirical headline. Your new headline is funny, can involve current political events and has an edge to it. Print only the suggestions, with one suggestion on each line.", "content": prompt,
}, },
{ {
"role": "user", "role": "user",
@ -34,16 +69,34 @@ class GroqClient(LLMClient):
raise ValueError raise ValueError
return suggestions_str return suggestions_str
@override
def rewrite_title( def rewrite_title(
self, original_content: str, suggestions: str | None = None self,
original_content: str,
suggestions: str | None = None,
custom_prompt: str | None = None,
) -> str: ) -> str:
prompt = (
custom_prompt
if custom_prompt
else """
You are an editor at a satirical newspaper. Improve on the following
satirical headline. For a given headline, you diligently evaluate: (1)
Whether the headline is funny; (2) Whether the headline follows a clear
satirical goal; (3) Whether the headline has sufficient substance and
bite; (4) Whether the headline is roughly the length of the original
suggestion. Based on the outcomes of your review, you pick your
favorite headline from the given suggestions and you make targeted
revisions to it. Your output consists solely of the revised headline.
"""
)
if not suggestions: if not suggestions:
suggestions = self.get_alternative_title_suggestions(original_content) suggestions = self.get_alternative_title_suggestions(original_content)
winner = self.client.chat.completions.create( winner = self.client.chat.completions.create(
messages=[ messages=[
{ {
"role": "system", "role": "system",
"content": "You are an editor at a satirical newspaper. Improve on the following satirical headline. For a given headline, you diligently evaluate: (1) Whether the headline is funny; (2) Whether the headline follows a clear satirical goal; (3) Whether the headline has sufficient substance and bite. Based on the outcomes of your review, you pick your favorite headline from the given suggestions and you make targeted revisions to it. Keep the length roughly to that of the original suggestions. Your output consists solely of the revised headline.", "content": prompt,
}, },
{ {
"role": "user", "role": "user",
@ -58,21 +111,26 @@ class GroqClient(LLMClient):
raise ValueError raise ValueError
return winner_str.strip(" \"'") return winner_str.strip(" \"'")
@override
def rewrite_summary( def rewrite_summary(
self, original: Original, improved_title: str | None = None self,
original: Original,
improved_title: str | None = None,
custom_prompt: str | None = None,
) -> str: ) -> str:
prompt = (
custom_prompt
if custom_prompt
else f""" Below there is an original title and an original summary. Then follows an improved title. Write an improved summary based on the original summary which fits to the improved title. {"Do not use the phrase: 'in a surprising turn of events' or 'in a shocking turn of events.'" if AVOID_SHOCKING_TURN_OF_EVENTS else ""} Only output the improved summary.\n\nTitle:{original.title}\nSummary:{original.summary}\n---\nTitle:{improved_title}\nSummary:"""
)
if not improved_title: if not improved_title:
improved_title = self.rewrite_title(original.title) improved_title = self.rewrite_title(original.title)
no_shocking_turn: bool = True
summary = self.client.chat.completions.create( summary = self.client.chat.completions.create(
messages=[ messages=[
{ {
"role": "user", "role": "user",
"content": f""" "content": prompt,
Below there is an original title and an original summary. Then follows an improved title. Write an improved summary based on the original summary which fits to the improved title.
{"Do not use the phrase: 'in a surprising turn of events' or 'in a shocking turn of events.'" if no_shocking_turn else ""}
Only output the improved summary.\n\nTitle:{original.title}\nSummary:{original.summary}\n---\nTitle:{improved_title}\nSummary:""",
} }
], ],
model="llama-3.3-70b-versatile", model="llama-3.3-70b-versatile",

View file

@ -31,6 +31,14 @@
</div> </div>
</div> </div>
</li> </li>
<li>
<span class="fab-label">Lefty Bee</span>
<div class="option-btn fab-icon-holder">
<div class="icon">
<i class="fas fa-arrow-left"></i>
</div>
</div>
</li>
<li> <li>
<span class="fab-label">About</span> <span class="fab-label">About</span>
<div class="option-btn fab-icon-holder"> <div class="option-btn fab-icon-holder">