Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a7a00e7
Add unified release-fetching interface to all fetchers with GitHub su…
BrunoV21 Aug 27, 2025
d7b67c9
fixed tests
BrunoV21 Aug 27, 2025
8abc4ac
udpated example
BrunoV21 Aug 27, 2025
4d81bac
Add parse_releases_to_txt utility for summarizing releases and compre…
BrunoV21 Aug 28, 2025
aa7354b
updated parse_releases_to_txt function
BrunoV21 Aug 28, 2025
17cc678
feat(api): add /release_notes endpoint for generating repository rele…
BrunoV21 Sep 9, 2025
1584750
updated .gitignore
BrunoV21 Sep 9, 2025
259c823
fix(api): correct release notes fetching logic to include the specifi…
BrunoV21 Sep 9, 2025
3394dfc
feat(api): enhance WebSocket endpoint to support release notes genera…
BrunoV21 Sep 9, 2025
8456e5a
feat(ui): add release notes generation feature with validation and co…
BrunoV21 Sep 9, 2025
73a46a5
prompts(release-notes): enforce style and type classification
BrunoV21 Sep 14, 2025
40653d9
fix(ui): lock summary and recap buttons during release notes
BrunoV21 Sep 14, 2025
f5fc644
current state
BrunoV21 Sep 18, 2025
45240fe
style(ui): unify recap, release, and counter buttons with accent theme
BrunoV21 Sep 22, 2025
384eb32
refactor(ui): replace button elements with Button component
BrunoV21 Sep 23, 2025
509f70b
style(ui): update button and switcher css for retro look
BrunoV21 Sep 23, 2025
80c7161
refactor(ui): use native button for release counter controls
BrunoV21 Sep 23, 2025
639ec11
style(ui): improve recap and release button layout and responsiveness
BrunoV21 Sep 24, 2025
be7c8e2
style(ui): add responsive heights for recap-release switcher
BrunoV21 Sep 24, 2025
fefd098
chore(build): bump version to 0.0.1 in package files
BrunoV21 Sep 24, 2025
ddb261a
style(ui): adjust recap 3-dots badge position for better layout
BrunoV21 Sep 24, 2025
133cf05
style(ui): add disabled state styles for counter button
BrunoV21 Sep 24, 2025
b1037d8
style(ui): update recap-release switcher breakpoints for layout
BrunoV21 Sep 24, 2025
50378b5
style(ui): add disabled styles for release buttons
BrunoV21 Sep 25, 2025
1394de7
feat(ui): add tooltip for release notes button
BrunoV21 Sep 25, 2025
0de818e
style(ui): set button width to 100 percent for consistency
BrunoV21 Sep 25, 2025
ff77c7e
style(ui): refine tooltip appearance and interaction behavior
BrunoV21 Sep 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,5 @@ cython_debug/
# PyPI configuration file
.pypirc

observability_data/*
observability_data/*
storage/
82 changes: 79 additions & 3 deletions app/api/server/routes.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from fastapi import APIRouter, HTTPException, Request, Query
from pydantic import BaseModel

from models.schemas import ChatRequest
from services.llm_service import initialize_llm_session, set_llm, get_llm, trim_messages
from services.llm_service import set_llm, get_llm, trim_messages
from services.fetcher_service import store_fetcher, get_fetcher
from git_recap.utils import parse_entries_to_txt
from git_recap.utils import parse_entries_to_txt, parse_releases_to_txt
from aicore.llm.config import LlmConfig
from datetime import datetime, timezone
from typing import Optional, List
Expand Down Expand Up @@ -197,6 +196,83 @@ async def get_actions(

return {"actions": parse_entries_to_txt(actions)}

@router.get("/release_notes")
async def get_release_notes(
session_id: str,
repo_filter: Optional[List[str]] = Query(None),
num_old_releases: int = Query(..., ge=1)
):
"""
Generate release notes for the latest release of a single repository.
Validates input, fetches releases, fetches actions since latest release, and returns compiled release notes.
"""
# Validate repo_filter: must be a single repo
if repo_filter is None or len(repo_filter) != 1:
raise HTTPException(status_code=400, detail="repo_filter must be a list containing exactly one repository name.")
repo = repo_filter[0]

# Get fetcher for session
try:
fetcher = get_fetcher(session_id)
except HTTPException as e:
raise

# Check if fetcher supports fetch_releases
try:
releases = fetcher.fetch_releases()
except NotImplementedError:
raise HTTPException(status_code=400, detail="Release fetching is not supported for this provider.")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching releases: {str(e)}")

releases_txt = parse_releases_to_txt(releases[:num_old_releases])
# Filter releases for the requested repo
repo_releases = [r for r in releases if r.get("repo") == repo]
n_releases = len(repo_releases)
if n_releases < 1:
raise HTTPException(status_code=400, detail="Not enough releases found for the specified repository (need at least 1).")
if num_old_releases < 1 or num_old_releases >= n_releases:
raise HTTPException(
status_code=400,
detail=f"num_old_releases must be at least 1 and less than the number of releases available ({n_releases}) for this repository."
)

# Sort releases by published_at descending (latest first)
try:
repo_releases.sort(key=lambda r: r.get("published_at") or r.get("created_at"), reverse=True)
except Exception:
raise HTTPException(status_code=500, detail="Failed to sort releases by date.")

latest_release = repo_releases[0]

# Determine the start_date for actions (latest release date)
release_date = latest_release.get("published_at") or latest_release.get("created_at")
if not release_date:
raise HTTPException(status_code=500, detail="Latest release does not have a valid date.")
# Accept both datetime and string
if isinstance(release_date, datetime):
start_date_iso = release_date.astimezone(timezone.utc).isoformat()
else:
try:
dt = datetime.fromisoformat(release_date)
start_date_iso = dt.astimezone(timezone.utc).isoformat()
except Exception:
raise HTTPException(status_code=500, detail="Release date is not a valid ISO format.")

# Fetch actions since latest release for this repo
# Reuse get_actions logic, but inline to avoid async call
# Set fetcher filters
fetcher.start_date = datetime.fromisoformat(start_date_iso)
fetcher.end_dt = None
fetcher.repo_filter = [repo]

llm = get_llm(session_id)
actions = fetcher.get_authored_messages()
actions = trim_messages(actions, llm.tokenizer)
actions_txt = parse_entries_to_txt(actions)

return {"actions": "\n\n".join([actions_txt, releases_txt])}

# @router.post("/chat")
# async def chat(
# chat_request: ChatRequest
Expand Down
55 changes: 42 additions & 13 deletions app/api/server/websockets.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
import json
from typing import Optional

from services.llm_service import initialize_llm_session, trim_messages, run_concurrent_tasks, get_llm
from services.prompts import SELECT_QUIRKY_REMARK_SYSTEM, SYSTEM, RELEASE_NOTES_SYSTEM, quirky_remarks
from services.llm_service import get_random_quirky_remarks, run_concurrent_tasks, get_llm
from aicore.const import SPECIAL_TOKENS, STREAM_END_TOKEN
import ulid
import asyncio

router = APIRouter()
Expand All @@ -14,17 +14,39 @@
active_histories = {}

TRIGGER_PROMPT = """
Consider the following history of actionables from Git and in return me the summary with N = '{N}' bullet points:
Consider the following history of actionables from Git and return me the summary with N = '{N}' bullet points:

{ACTIONS}
"""

@router.websocket("/ws/{session_id}")
TRIGGER_RELEASE_PROMPT = """
Consider the following history of actionables from Git and the previous Release Notes (if available).
Generate me the next Release Notes based on the new Git Actionables matching the format of the previous releases:

{ACTIONS}
"""


@router.websocket("/ws/{session_id}/{action_type}")
async def websocket_endpoint(
websocket: WebSocket,
session_id: Optional[str] = None
session_id: Optional[str] = None,
action_type: str="recap"
):
await websocket.accept()

if action_type == "recap":
QUIRKY_SYSTEM = SELECT_QUIRKY_REMARK_SYSTEM.format(
examples=json.dumps(get_random_quirky_remarks(quirky_remarks), indent=4)
)

system = [SYSTEM, QUIRKY_SYSTEM]

elif action_type == "release":
system = RELEASE_NOTES_SYSTEM

else:
raise HTTPException(status_code=404)

# Store the connection
active_connections[session_id] = websocket
Expand All @@ -40,16 +62,23 @@ async def websocket_endpoint(
N = msg_json.get("n", 5)
assert int(N) <= 15
assert message
history = [
TRIGGER_PROMPT.format(
N=N,
ACTIONS=message
)
]
if action_type == "recap":
history = [
TRIGGER_PROMPT.format(
N=N,
ACTIONS=message
)
]
elif action_type == "release":
history = [
TRIGGER_RELEASE_PROMPT.format(ACTIONS=message)
]

response = []
async for chunk in run_concurrent_tasks(
llm,
message=history
message=history,
system_prompt=system
):
if chunk == STREAM_END_TOKEN:
await websocket.send_text(json.dumps({"chunk": chunk}))
Expand Down
10 changes: 3 additions & 7 deletions app/api/services/llm_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import os
import uuid
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Union
from fastapi import HTTPException
import asyncio
import random
Expand All @@ -10,7 +10,6 @@
from aicore.config import Config
from aicore.llm import Llm
from aicore.llm.config import LlmConfig
from services.prompts import SELECT_QUIRKY_REMARK_SYSTEM, SYSTEM, quirky_remarks

def get_random_quirky_remarks(remarks_list, n=5):
"""
Expand Down Expand Up @@ -113,7 +112,7 @@ def trim_messages(messages, tokenizer_fn, max_tokens: Optional[int] = None):
messages.pop(0) # Remove from the beginning
return messages

async def run_concurrent_tasks(llm, message):
async def run_concurrent_tasks(llm, message, system_prompt :Union[str, List[str]]):
"""
Run concurrent tasks for the LLM and logger.

Expand All @@ -124,10 +123,7 @@ async def run_concurrent_tasks(llm, message):
Yields:
Chunks of logs from the logger.
"""
QUIRKY_SYSTEM = SELECT_QUIRKY_REMARK_SYSTEM.format(
examples=json.dumps(get_random_quirky_remarks(quirky_remarks), indent=4)
)
asyncio.create_task(llm.acomplete(message, system_prompt=[SYSTEM, QUIRKY_SYSTEM]))
asyncio.create_task(llm.acomplete(message, system_prompt=system_prompt))
asyncio.create_task(_logger.distribute())
# Stream logger output while LLM is running.
while True:
Expand Down
36 changes: 35 additions & 1 deletion app/api/services/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,38 @@
"The code was optimized so hard it ascended to another paradigm.",
"A linter ran — and it judged the code as a whole.",
"The logic branch spiraled — and so did the afternoon."
]
]

### TODO improve prompts to infer if release is major, minor or whatever
RELEASE_NOTES_SYSTEM = """
### System Prompt for Release Notes Generation

You are an AI assistant tasked with generating professional, concise, and informative release notes for a software project. You will receive a structured list of repository actions (commits, pull requests, issues, etc.) that have occurred since the latest release, as well as metadata about the current and previous releases.

#### Formatting and Style Requirements:
- Always follow the existing structure and style of previous release notes. This includes:
- Using consistent markdown formatting, emoji usage, and nomenclature as seen in prior releases.
- Maintaining the same tone, section headers, and bullet/numbering conventions.
- Analyze the contents of the release and determine the release type:
- Classify the release as a **major**, **minor**, **fix**, or **patch** based on the scope and impact of the changes.
- Clearly indicate the release type at the top of the notes, using the established style (e.g., with an emoji or header).
- Ensure the summary and highlights reflect the chosen release type.

#### Your response should:
1. **Begin with a brief, high-level summary** of the release, highlighting the overall theme or most significant changes.
2. **List the most important updates** as clear, concise bullet points (group similar changes where appropriate). Each bullet should reference the type of change (e.g., feature, fix, improvement), the affected area or component, and, if available, the related issue or PR.
3. **Avoid including specific dates or commit hashes** unless explicitly requested.
4. **Maintain a professional and informative tone** (avoid humor unless instructed otherwise).
5. **End with a short call to action or note for users** (e.g., upgrade instructions, thanks to contributors, or next steps).

#### Example Output:

**Release v2.3.0 : Major Improvements and Bug Fixes**

- Added support for multi-repo tracking in the dashboard (PR #42)
- Fixed authentication bug affecting GitLab users (Issue #101)
- Improved performance of release notes generation
- Updated documentation for new API endpoints

Thank you to all contributors! Please upgrade to enjoy the latest features and improvements.
"""
2 changes: 1 addition & 1 deletion app/git-recap/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/git-recap/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "git-recap",
"homepage": "https://BrunoV21.github.io/GitRecap/",
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
Loading
Loading