commit 751bfe568c8a0f8857a4f29f2bcacc7eb40edaae Author: Patryk Hegenberg Date: Fri Sep 20 16:00:11 2024 +0200 initial commit to push copy to codeberg diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..25d8c22 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.12-bookworm + +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY changelog2 /app/changelog2 + +WORKDIR /app/changelog2 + +ENTRYPOINT ["python", "main.py"] + diff --git a/README.md b/README.md new file mode 100644 index 0000000..f2200b2 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# Changelog Generator Tool + +This project provides an AI-powered tool that automates the generation of changelogs by collecting information from Git logs and Redmine tickets. It uses a crew of AI agents to improve the quality and efficiency of the final changelog. + +## Project Overview + +The system uses multiple AI agents, each responsible for a specific part of the changelog generation process. This tool is designed to: + +- Gather data from Git logs and Redmine tickets. +- Analyze and categorize the collected information. +- Generate a well-formatted changelog or release notes. +- Ensure the output is consistent with project guidelines and requirements. + +## Key Components + +### Main Script (`main.py`) + +The main entry point of the tool. It handles command-line arguments, sets up the environment, and initiates the changelog generation process. + +### Crew Definition (`crew.py`) + +Defines the AI agents and their tasks using the `crewai` framework. It includes: + +- `GitlogAnalyst`: Analyzes Git commit logs. +- `RedmineAnalyst`: Retrieves and analyzes Redmine issues. +- `WritingAgent`: Generates the final changelog or release notes. + +### Configuration + +- `config/config.py`: Parses configuration settings. +- `config/agents.yaml`: Defines agent configurations. +- `config/tasks.yaml`: Defines task configurations. + +### Tools + +- `tools/tools.py`: Contains custom tools for retrieving Git commits and Redmine issues. + +### Data + +- `data/CodeOfConduct.md`: Contains the project's code of conduct (used for context). +- `data/template2.txt`: Changelog template used by the writing agent. + +## Usage + +1. **Environment Setup for development**: + + - This project uses Devbox for development environment management and Poetry for dependency management. + - Set up the environment using: + ``` + devbox shell + poetry install + ``` + +2. **Running the Tool**: + + - The tool is started using a shell script that manages Docker containers. + - Basic usage: + ``` + ./create_changelog.sh [options] + ``` + - Options: + - `--project`: Specify the Redmine project ID + - `--version`: Specify the version ID + - `--repo`: Path to the Git repository (optional) + - `--type`: Type of output (Changelog/ReleaseNotes) + - `--local`: Use local LLM instead of OpenAI (optional) + - If an error occurs that the docker volume ollama-local cannot be found, create it with `docker volume create ollama-local` +3. **Example Usage**: + +```bash +./create_changelog.sh -projects 123 -versions 456 -type Changelog -repo /path/to/repo +``` + +## Dependencies + +- Python 3.x +- Poetry for dependency management +- Devbox for development environment +- Docker for running LLM services +- External services: +- Redmine API +- Git repository +- OpenAI API (optional) +- Local LLM (optional, e.g., Ollama) + +## Configuration + +The project uses a TOML configuration file named changelog-config.toml for storing essential settings. This file should contain the following entries: + +```toml +[DEFAULT] +OPENAI_API_KEY = your_openai_api_key_here +REDMINE_API_KEY = your_redmine_api_key_here +REDMINE_HOST = https://your_redmine_host.com +GIT_REPO_PATH = /path/to/your/git/repo +``` + +### Configuration File Location + +The config.py script searches for the changelog-config.toml file in the following locations, in order: + +- Current working directory +- changelog2 subdirectory of the current working directory +- '.config' subdirectory of the current working directory +- User's home directory +- Location specified by the CHANGELOG_CONFIG environment variable + +Make sure to create the changelog-config.toml file with your actual API keys and settings before running the tool. Keep this file secure and do not commit it to version control. + +However, a copy of changelog-config.toml should be located in the changelog2 subfolder for execution in the docker container. + +## Development + +- To add new dependencies: `poetry add ` +- To update dependencies: `poetry update` + +## Notes + +- The tool can use either OpenAI's models or a local LLM (like Ollama) for text generation. +- Ensure all necessary Docker containers are running before starting the tool. diff --git a/changelog2/__init__.py b/changelog2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/changelog2/__main__.py b/changelog2/__main__.py new file mode 100644 index 0000000..40e2b01 --- /dev/null +++ b/changelog2/__main__.py @@ -0,0 +1,4 @@ +from .main import main + +if __name__ == "__main__": + main() diff --git a/changelog2/config/agents.yaml b/changelog2/config/agents.yaml new file mode 100644 index 0000000..c70931e --- /dev/null +++ b/changelog2/config/agents.yaml @@ -0,0 +1,29 @@ +gitlog_analyst: + role: > + Git Log Analyst + goal: > + Collect all relevant informations from Git logs + backstory: > + You specialize in analyzing Git Logs. You extract important Information such as commit messages, + dates and categorize the commits by new, changed, and fixed based on their commit message. + +redmine_analyst: + role: > + Redmine Analyst + goal: > + Gather information from Redmine tickets + backstory: > + You are an expert in Redmine. You collect relevant information from Redmine tickets for the changelog + +writing_agent: + role: > + Technical Writer + goal: > + Transform technical information into clear, concise, and user-friendly documentation + backstory: > + You are an experienced technical writer with a strong background in software development + and documentation. Your expertise lies in translating complex technical concepts into + easily understandable content for various audiences, from end-users to developers. + You have a keen eye for detail and a passion for creating documentation that enhances + user experience and product understanding. + diff --git a/changelog2/config/config.py b/changelog2/config/config.py new file mode 100644 index 0000000..8a9fb1a --- /dev/null +++ b/changelog2/config/config.py @@ -0,0 +1,34 @@ +import configparser +import os +from pathlib import Path + + +def find_config(): + possible_locations = [ + Path.cwd() / "changelog-config.toml", + Path.cwd() / "changelog2" / "changelog-config.toml", + Path.cwd() / ".config" / "changelog-config.toml", + Path.cwd() / ".config" / "changelog-config.toml", + Path.home() / "changelog-config.toml", + Path(os.getenv("CHANGELOG_CONFIG", "")), + ] + + for location in possible_locations: + if location.is_file(): + return str(location) + + raise FileNotFoundError("changelog-config.toml nicht gefunden") + + +config_path = find_config() + + +def parse_config(): + config = configparser.ConfigParser() + config.read(config_path) + return { + "REDMINE_HOST": config["DEFAULT"]["REDMINE_HOST"], + "REDMINE_API_KEY": config["DEFAULT"]["REDMINE_API_KEY"], + "OPENAI_API_KEY": config["DEFAULT"]["OPENAI_API_KEY"], + "GIT_REPO_PATH": config["DEFAULT"]["GIT_REPO_PATH"], + } diff --git a/changelog2/config/tasks.yaml b/changelog2/config/tasks.yaml new file mode 100644 index 0000000..3b8ce11 --- /dev/null +++ b/changelog2/config/tasks.yaml @@ -0,0 +1,47 @@ +gitlog_analysis_task: + description: > + Collect all relevant Git commits since the last tag for the projects {project_id} which are seperated by ",". + Analyze the commits and categorize them according to new features, changes, and fixes. + + Git Repo: {repo_path} + expected_output: > + A JSON string containing a list of relevant Git commits with their hash, date, message, a {changelog_type} entry understandable for end users + and category (new feature, change, or fix). + +redmine_analysis_task: + description: > + Collect all relevant Redmine tickets for the projects {project_id} and their versions {version_id}. + Analyze the tickets and categorize them based on their type and importance. + expected_output: > + A JSON string containing a list of relevant Redmine tickets with their ID, subject, tracker, + a {changelog_type} entry understandable for end users and a brief summary of their content and importance. + +formatting_task: + description: > + Format the collected Git commits and Redmine tickets into a structured {changelog_type}. + Ensure that the information is organized logically and easy to read. + expected_output: > + A well-formatted {changelog_type} text that includes both Git commits and Redmine tickets + in a clear and organized manner, with proper categorization and highlighting of key changes. + +writing_task: + description: > + Using the information from gitlog and redmine tickets, create a user-friendly and informative {changelog_type} document. + Translate technical details into clear, concise language suitable for both technical + and non-technical readers. Highlight key features, changes, and fixes. + expected_output: > + A polished {changelog_type} document that effectively communicates all relevant changes, + improvements, and fixes in a way that enhances user understanding and engagement. + The document should maintain consistency with the rules provided by the RAG tool and should fit the template from the template_tool + while accurately reflecting the current changelog's content. + instructions: > + 1. Review the template provided by the template tool. + 2. Identify the key sections and formatting used. + 3. Analyze the content of the gitlog and redmine issues. + 4. Structure the new changelog following the templates format, including similar sections. + 5. Translate technical details into user-friendly language. + 6. Ensure all significant changes, features, and fixes are included and highlighted. + 7. Maintain a consistent tone and style throughout the document. + 8. Double-check that the final document is informative, clear, and engaging for all readers. + 9. Skip issues/log entries not relevant for the user + 10. Do not include any additional output diff --git a/changelog2/crew.py b/changelog2/crew.py new file mode 100644 index 0000000..ead432a --- /dev/null +++ b/changelog2/crew.py @@ -0,0 +1,153 @@ +import os +from typing import List + +from config.config import parse_config +from crewai import Agent, Crew, Process, Task +from crewai.project import CrewBase, agent, crew, task +from crewai.tasks.conditional_task import ConditionalTask +from crewai_tools import FileReadTool +from langchain_ollama import ChatOllama +from langchain_openai import ChatOpenAI +from pydantic import BaseModel +from tools.tools import ( + get_categorized_commits_since_last_tag, + get_commits_since_last_tag, + get_redmine_issues, +) + +config = parse_config() +os.environ["OPENAI_API_KEY"] = config["OPENAI_API_KEY"] +rag_tool = FileReadTool(file_path="./data/CodeOfConduct.md") +template_tool = FileReadTool(file_path="./data/template2.txt") +llmLocal = ChatOllama(model="gemma2", base_url="http://ollama:11434") +llmOpenAI = ChatOpenAI( + base_url="https://api.openai.com/v1/", + model_name="gpt-4o", + temperature=0.4, +) + + +@CrewBase +class ChangelogCrew: + """Changelog writing crew""" + + agents_config = "config/agents.yaml" + tasks_config = "config/tasks.yaml" + + def __init__(self, git_task: bool = False, local: bool = True): + self.git_task = git_task + self.local = local + if self.local: + os.environ["OPENAI_API_BASE"] = "http://ollama:11434" + os.environ["OPENAI_MODEL_NAME"] = "gemma2" + else: + os.environ["OPENAI_API_BASE"] = "https://api.openai.com/v1/chat/completions" + + @agent + def gitlog_analyst(self) -> Agent: + if self.local: + llm = llmLocal + else: + llm = llmOpenAI + return Agent( + config=self.agents_config["gitlog_analyst"], + tools=[get_categorized_commits_since_last_tag], + llm=llm, + max_iter=5, + verbose=False, + memory=False, + ) + + @agent + def redmine_analyst(self) -> Agent: + if self.local: + llm = llmLocal + else: + llm = llmOpenAI + return Agent( + config=self.agents_config["redmine_analyst"], + tools=[get_redmine_issues], + llm=llm, + max_iter=5, + verbose=False, + memory=False, + ) + + @agent + def writing_agent(self) -> Agent: + if self.local: + llm = llmLocal + else: + llm = llmOpenAI + with open("./data/template2.txt", "r") as template_file: + template_content = template_file.read() + return Agent( + config=self.agents_config["writing_agent"], + tools=[template_tool], + llm=llm, + max_iter=5, + verbose=False, + memory=False, + template=template_content, + ) + + @task + def redmine_analysis_task(self) -> Task: + return Task( + config=self.tasks_config["redmine_analysis_task"], + agent=self.redmine_analyst(), + ) + + @task + def gitlog_analysis_task(self) -> ConditionalTask: + return ConditionalTask( + config=self.tasks_config["gitlog_analysis_task"], + condition=lambda context: self.git_task, + agent=self.gitlog_analyst(), + ) + + @task + def writing_task(self) -> Task: + context = [] + context.append(self.redmine_analysis_task()) + if self.git_task: + context.append(self.gitlog_analysis_task()) + + return Task( + config=self.tasks_config["writing_task"], + agent=self.writing_agent(), + context=context, + ) + + @crew + def crew(self) -> Crew: + """Creates the Changelog crew""" + return Crew( + agents=self.agents, + tasks=self.tasks, + process=Process.sequential, + verbose=False, + ) + + +class GitCommit(BaseModel): + hash: str + author: str + date: str + message: str + category: str + + +class RedmineTicket(BaseModel): + id: int + subject: str + tracker: str + summary: str + + +class Changelog(BaseModel): + version: str + date: str + changes: List[str] + fixes: List[str] + new_features: List[str] diff --git a/changelog2/data/CodeOfConduct.md b/changelog2/data/CodeOfConduct.md new file mode 100644 index 0000000..61aa50f --- /dev/null +++ b/changelog2/data/CodeOfConduct.md @@ -0,0 +1,57 @@ +# Code of Conduct für Änderungsprotokolle + +## Allgemeine Richtlinien + +1. Klarheit und Konsistenz: Alle Einträge müssen klar und konsistent formuliert sein, um Verwirrung zu vermeiden. Verwenden Sie klare, präzise Sprache und folgen Sie den unten aufgeführten Formatierungsrichtlinien. +2. Abkürzungen und Akronyme: Verwenden Sie standardisierte Abkürzungen für Module und Komponenten. Eine Liste der häufig verwendeten Abkürzungen finden Sie im Abschnitt "Abkürzungen". +3. Versionsnummern: Geben Sie die Versionsnummern der betroffenen Komponenten klar an. Verwenden Sie die Formatierung Komponente: alte Version -> neue Version. +4. Kategorien von Änderungen: Gliedern Sie Änderungen in die Kategorien NEW, CHANGED, FIXED, und DOCUMENTATION. Jede Kategorie sollte klar vom Rest des Dokuments abgegrenzt sein. + +## Struktur und Formatierung + +### Titel und Metadaten + +Titel: Verwenden Sie das Format = [Produktname] - Changelog. Beispiel: = TIXstream FX - Changelog. +Website und Sprache: Geben Sie die Website und die Sprache an. Beispiel: + + +### Copyright und Versionsinfo + +- Copyright: Fügen Sie das Copyright-Dokument ein. Beispiel: + + +- Versionsinfo: Geben Sie die Versionsinformationen ein. Beispiel: + + +### Änderungsprotokoll + +- Einführung: Erklären Sie den Zweck des Dokuments und die Bedeutung der einzelnen Einträge. Beispiel: + + +- Versionseinträge: Jede Version sollte mit der Versionsnummer und dem Datum beginnen. Beispiel: + + +- Neue Funktionen (NEW): Listen Sie neue Funktionen und Features auf. Verwenden Sie nummerierte Punkte oder Aufzählungen. +- Änderungen (CHANGED): Dokumentieren Sie wesentliche Änderungen an bestehenden Funktionen oder Systemen. +- Fehlerbehebungen (FIXED): Beschreiben Sie behobene Fehler und Probleme. +- Dokumentation (DOCUMENTATION): Erwähnen Sie Änderungen an der Dokumentation oder neuen Dokumenten. + +### Eintragsformatierung + +- Klarheit der Beschreibungen: Beschreibungen sollten so präzise wie möglich sein. Vermeiden Sie unnötigen Jargon und halten Sie die Erklärungen einfach. +- Verweise auf Tickets: Falls zutreffend, verwenden Sie Ticket-IDs zur Referenz. Beispiel: (_#12345_). +- Modul- und Komponentennamen: Verwenden Sie die offiziellen Namen und Abkürzungen für Komponenten. Beispiel: + + +### Abkürzungen + +Use standard abbreviations for components: + +- AM: Access Manager +- FXweb: FX Web Tools +- TCC: TIXEL Control Center +- TXEC: TIXstream Express Client +- TXEJM: TIXstream Express Job Manager +- TXSFX: TIXstream FX +- TXY: TIXway + diff --git a/changelog2/data/template2.txt b/changelog2/data/template2.txt new file mode 100644 index 0000000..8cb43d0 --- /dev/null +++ b/changelog2/data/template2.txt @@ -0,0 +1,57 @@ += {Produktname} - Changelog +:website: http://www.example.com +:lang: en +:encoding: utf-8 + +.COPYRIGHT +*********************************************************************** +include::../shared-doc/src/copyright.adoc[] +*********************************************************************** + +Software Version -- Document Version: +include::version.string[] + +== Redmine Issues + +// Hier können Sie die Redmine-Issues auflisten +// Beispiel: +* #12345 - Beschreibung des Issues +* #12346 - Beschreibung des Issues +* #12347 - Beschreibung des Issues + +== Changelog + +This document lists all relevant changes applied over time to {Produktname} packages and their software components. +Each entry in the list shows changes made to the *Services*, the installation *Bundle* or the *Documentation*. +Entries may have an _ID_ at the end of the line for internal reference. + +== Version {Versionsnummer} ({Datum}) + +*NEW* + +{{#each ticket or log entry}} +=== _({{id/hash}})_ {{subject}} === +* {{Your generated changelog entry}} _({{ticket_id/git hash}})_ +{{/each}} + +*CHANGED* + +{{#each ticket or log entry}} +=== _({{id/hash}})_ {{subject}} === +* {{Your generated changelog entry}} _({{ticket_id/git hash}})_ +{{/each}} + +*FIXED* + +{{#each ticket or log entry}} +=== _({{id/hash}})_ {{subject}} === +* {{Your generated changelog entry}} _({{ticket_id/git hash}})_ +{{/each}} + +*DOCUMENTATION* + +{{#each ticket or log entry}} +=== _({{id/hash}})_ {{subject}} === +* {{Your generated changelog entry}} _({{ticket_id/git hash}})_ +{{/each}} + diff --git a/changelog2/gittest.sh b/changelog2/gittest.sh new file mode 100755 index 0000000..c76ea3a --- /dev/null +++ b/changelog2/gittest.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +if ! git describe --tags --abbrev=0 &>/dev/null; then + echo "Keine Tags gefunden. Verwende den ersten Commit als Startpunkt." + START_POINT=$(git rev-list --max-parents=0 HEAD) +else + START_POINT=$(git describe --tags --abbrev=0) +fi + +echo "Changelog seit $START_POINT:" +echo "" +echo "Neue Features:" +git log --no-merges --pretty=format:"- %s" $START_POINT..HEAD | grep "^feat:" | sort +echo "" +echo "Fehlerbehebungen:" +git log --no-merges --pretty=format:"- %s" $START_POINT..HEAD | grep "^fix:" | sort +echo "" +echo "Dokumentation:" +git log --no-merges --pretty=format:"- %s" $START_POINT..HEAD | grep "^docs:" | sort +echo "" +echo "Andere Änderungen:" +git log --no-merges --pretty=format:"- [%h] %s (%an)" $START_POINT..HEAD | grep -vE "^- (feat|fix|docs):" | sort diff --git a/changelog2/main.py b/changelog2/main.py new file mode 100644 index 0000000..ac722f7 --- /dev/null +++ b/changelog2/main.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +import argparse +import json +import os +import ssl +import urllib.request as request + +from config.config import parse_config +from crew import ChangelogCrew +from langchain_ollama import ChatOllama +from langchain_openai import ChatOpenAI + +config = parse_config() +os.environ["OPENAI_API_KEY"] = config["OPENAI_API_KEY"] + +# os.environ["OPENAI_API_BASE"] = "http://ollama:11434" +# os.environ["OPENAI_MODEL_NAME"] = "llama3.1:8b" # Adjust based on available model + +llmLocal = ChatOllama( + model="llama3.1:8b", base_url="http://ollama:11434", temperatur=0.4 +) +llmOpenAI = ChatOpenAI( + model_name="gpt-4o", + temperature=0.4, +) +config["git_task"] = False + + +def makeRedmineRequest(url: str, APIKey: str): + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + req = request.Request(url, method="GET") + req.add_header("Content-Type", "application/json") + req.add_header("X-Redmine-API-Key", APIKey) + return request.urlopen(req, context=context) + + +def makeProjectRequest(url: str, APIKey: str) -> dict: + res = makeRedmineRequest(url, APIKey) + json_data = json.load(res) + project_info = {project["name"]: project["id"] for project in json_data["projects"]} + + return project_info + + +def makeVersionRequest(url: str, APIKey: str, project_id: int) -> dict: + res = makeRedmineRequest(url, APIKey) + json_data = json.load(res) + + version_info = { + version["name"]: version["id"] + for version in json_data["versions"] + if version["project"]["id"] == project_id and version["status"] == "open" + } + + return version_info + + +def main(): + desc = "This is a tool that generates changelog messages from Redmine and Git using AI." + parser = argparse.ArgumentParser(description=desc, add_help=True) + parser.add_argument( + "-p", + "--project", + help="The project ID for which text is to be generated", + # type=int, + type=str, + ) + parser.add_argument( + "-f", + "--fixed-version", + help="The version ID of the project for which text is to be generated", + # type=int, + type=str, + ) + parser.add_argument( + "-r", + "--repo", + help="The path to the Git repository", + type=str, + ) + parser.add_argument( + "-t", + "--type", + help="The type of text to be generated", + choices=["Changelog", "ReleaseNotes", "test"], + type=str, + ) + parser.add_argument( + "-l", + "--local", + help="Execute the generator using a local llm", + default=False, + action="store_true", + ) + args = parser.parse_args() + if not args.project: + project_infos = makeProjectRequest( + url=config["REDMINE_HOST"] + "/projects.json", + APIKey=config["REDMINE_API_KEY"], + ) + print("Enter a project id when calling. There is a choice:") + for project_name, project_id in project_infos.items(): + print(f"{project_name} (ID: {project_id})") + exit(0) + if args.project and not args.fixed_version: + print("Enter a version id when calling. There is a choice:") + fixed_version = makeVersionRequest( + url=config["REDMINE_HOST"] + + "/projects/" + + str(args.project) + + "/versions.json", + APIKey=config["REDMINE_API_KEY"], + project_id=args.project, + ) + for version_name, version_id in fixed_version.items(): + print(f"{version_name}: {version_id}") + exit(0) + if args.project and args.fixed_version and not args.type: + args.type = input("Please choose a type (Changelog/ReleaseNotes)") + customField = "" + prompt = "" + if args.project and args.fixed_version: + if args.type == "Changelog": + customField = "&cf_21=Changelog" + elif args.type == "ReleaseNotes": + customField = "&cf_21=Release_notes" + else: + customField = "" + + if args.repo != None: + os.environ["GIT_REPO_PATH"] = args.repo + config["git_task"] = True + else: + os.environ["GIT_REPO_PATH"] = "" + + changelog_crew = ChangelogCrew(git_task=config["git_task"], local=args.local) + result = changelog_crew.crew().kickoff( + inputs={ + "project_id": args.project, + "version_id": args.fixed_version, + "repo_path": args.repo, + "changelog_type": args.type, + } + ) + + print("######################") + print(result) + + +if __name__ == "__main__": + main() diff --git a/changelog2/tools/tools.py b/changelog2/tools/tools.py new file mode 100644 index 0000000..d58188a --- /dev/null +++ b/changelog2/tools/tools.py @@ -0,0 +1,159 @@ +import json +import re +import ssl +import urllib.request as request +from typing import Dict, List + +import git +from config.config import parse_config +from crewai_tools import tool + +config = parse_config() + + +@tool("Get Categorized Git Commits Since Last Tag") +def get_categorized_commits_since_last_tag( + repo_path: str = config["GIT_REPO_PATH"], +) -> str: + """ + Retrieves and categorizes commits since the last tag in the Git repository using git shortlog. + + Args: + repo_path (str): Path to the Git repository. + + Returns: + str: JSON string containing categorized commit information. + """ + repo = git.Repo(repo_path) + tags = sorted(repo.tags, key=lambda t: t.commit.committed_datetime) + last_tag = tags[-1] if tags else None + + if not last_tag: + range_spec = "HEAD" + else: + range_spec = f"{last_tag.name}..HEAD" + + shortlog = repo.git.shortlog("-sne", range_spec) + + author_commits = parse_shortlog(shortlog) + + commits = list(repo.iter_commits(range_spec)) + + categorized_commits = categorize_commits(commits) + + result = { + "last_tag": last_tag.name if last_tag else "No tags", + "author_commits": author_commits, + "categorized_commits": categorized_commits, + } + + return json.dumps(result, indent=2) + + +def parse_shortlog(shortlog: str) -> List[Dict[str, str]]: + """Parse git shortlog output.""" + author_commits = [] + for line in shortlog.split("\n"): + if line.strip(): + count, author = line.strip().split("\t") + author_commits.append( + {"commit_count": int(count.strip()), "author": author.strip()} + ) + return author_commits + + +def categorize_commits(commits: List[git.Commit]) -> Dict[str, List[Dict[str, str]]]: + """Categorize commits based on their message.""" + categories = {"features": [], "fixes": [], "documentation": [], "others": []} + + for commit in commits: + commit_info = { + "hash": commit.hexsha[:7], + "message": commit.summary, + "date": commit.committed_datetime.isoformat(), + } + + if commit.summary.startswith("feat:"): + categories["features"].append(commit_info) + elif commit.summary.startswith("fix:"): + categories["fixes"].append(commit_info) + elif commit.summary.startswith("docs:"): + categories["documentation"].append(commit_info) + else: + categories["others"].append(commit_info) + + return categories + + +@tool("Get Git Commits Since Last Tag") +def get_commits_since_last_tag(repo_path: str = config["GIT_REPO_PATH"]) -> str: + """ + Retrieves all commits since the last tag in the Git repository. + + Args: + repo_path (str): Path to the Git repository. + + Returns: + str: JSON string containing commit information. + """ + repo = git.Repo(repo_path) + tags = sorted(repo.tags, key=lambda t: t.commit.committed_datetime) + last_tag = tags[-1] if tags else None + + if not last_tag: + commits = list(repo.iter_commits()) + else: + commits = list(repo.iter_commits(f"{last_tag.name}..HEAD")) + + commit_info = [ + { + "hash": commit.hexsha, + "date": commit.committed_datetime.isoformat(), + "message": commit.message.strip(), + } + for commit in commits + ] + return json.dumps(commit_info) + + +@tool("Get Redmine Issues") +def get_redmine_issues(project_id: int, version_id: int, changelog_type: str) -> str: + """ + Retrieves issues for a specific Redmine project and version. + + Args: + project_id (int): ID of the Redmine project. + version_id (int): ID of the project version. + changelog_type (str): Type of changelog ("Changelog" or "Release_notes"). + + Returns: + str: JSON string containing issue information. + """ + if changelog_type == "Changelog": + custom_field = "&cf_21=Changelog" + elif changelog_type == "ReleaseNotes": + custom_field = "&cf_21=Release_notes" + else: + custom_field = "" + url = f"{config['REDMINE_HOST']}/issues.json?project_id={project_id}&fixed_version_id={version_id}&status_id=*{custom_field}" + data = make_redmine_request(url) + issues = [ + { + "id": issue["id"], + "subject": issue["subject"], + "tracker": issue["tracker"]["name"], + } + for issue in data["issues"] + ] + return json.dumps(issues) + + +def make_redmine_request(url): + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + req = request.Request(url, method="GET") + req.add_header("Content-Type", "application/json") + req.add_header("X-Redmine-API-Key", config["REDMINE_API_KEY"]) + return json.load(request.urlopen(req, context=context)) diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..0c5ab19 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,31 @@ +services: + ollama: + image: ollama/ollama:latest + container_name: ollama + ports: + - "11434:11434" + volumes: + - ollama-local:/root/.ollama + networks: + - app-network + + changelog: + build: . + container_name: changelog + # depends_on: + # - ollama + environment: + - OPENAI_API_BASE=http://ollama:11434 + volumes: + - ./changelog2:/app/changelog2 + networks: + - app-network + command: [""] + +volumes: + ollama-local: + external: true + +networks: + app-network: + driver: bridge diff --git a/create-changelog.sh b/create-changelog.sh new file mode 100755 index 0000000..58bc0da --- /dev/null +++ b/create-changelog.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -e + +# Funktion zur Anzeige der Verwendung +usage() { + echo "Verwendung: $0 --projects --versions [--repo ] [--local ] [--type --versions [--repo ] [--local ] [--type dict: + res = makeRedmineRequest(url, APIKey) + json_data = json.load(res) + project_info = {project["name"]: project["id"] for project in json_data["projects"]} + + return project_info + + +def makeVersionRequest(url: str, APIKey: str, project_id: int) -> dict: + res = makeRedmineRequest(url, APIKey) + json_data = json.load(res) + + version_info = { + version["name"]: version["id"] + for version in json_data["versions"] + if version["project"]["id"] == project_id and version["status"] == "open" + } + + return version_info + + +def main(): + try: + load_dotenv("./.env") + rdmnHost = os.getenv("REDMINE_HOST") + rdmnAPIKey = os.getenv("REDMINE_API_KEY") + openaiAPIKey = os.getenv("OPENAI_API_KEY") + except Exception as e: + print("No Env_variables set: ", e) + finally: + rdmnHost = "https://redmine.tixeltec.de/redmine" + if rdmnAPIKey is None or openaiAPIKey is None: + print( + "Please set environment variables for REDMINE_API_KEY and/or OPENAI_API_KEY" + ) + exit(1) + + desc = """This is a simple tool that allows you to generate changelog messages from Redmine using AI. + +Usage: + 1. Create a `.env` file . Fill in the environment variables as follows: + ``` + REDMINE_HOST=your_redmine_host + REDMINE_API_KEY=your_redmine_api_key + OPENAI_API_KEY=your_openai_api_key + ``` + 2. Run the script with the following arguments:\n + - `-p, --project`: The project ID for which the changelog should be generated. + - `-f, --fixed-version`: The version ID for which the changelog should be generated. + - `-t, --type`: The type of text to be generated (Changelog or Release Notes). + 3. If you don't provide a project ID or version ID, a list of available projects and versions will be displayed for you to choose from. + 4. The generated changelog will be output in AsciiDoc format on the console. + +Examples: + python3 changelog_generator.py -p 47 -f 755 -t Changelog + python3 changelog_generator.py -p 47 -f 755 -t ReleaseNotes +""" + clp = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent(desc), + add_help=True, + ) + clp.add_argument( + "-p", + "--project", + help="The project for which text is to be generated", + type=int, + ) + clp.add_argument( + "-f", + "--fixed-version", + help="The version of the project for which text is to be generated", + type=int, + ) + clp.add_argument( + "-t", + "--type", + help="The type of text to be generated", + choices=["Changelog", "ReleaseNotes"], + type=str, + ) + args = clp.parse_args() + if not args.project: + project_infos = makeProjectRequest( + url=rdmnHost + "/projects.json", APIKey=rdmnAPIKey + ) + print("Enter a project id when calling. There is a choice:") + for project_name, project_id in project_infos.items(): + print(f"{project_name} (ID: {project_id})") + if args.project and not args.fixed_version: + print("Enter a version id when calling. There is a choice:") + fixed_version = makeVersionRequest( + url=rdmnHost + "/projects/" + str(args.project) + "/versions.json", + APIKey=rdmnAPIKey, + project_id=args.project, + ) + for version_name, version_id in fixed_version.items(): + print(f"{version_name}: {version_id}") + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a1755eb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[tool.poetry] +name = "changelog2" +version = "0.0.1" +description = "An AI-Agent based tool for creating changelogs" +authors = ["Patryk Hegenberg "] +readme = "README.md" + +[tool.poetry.scripts] +changelog2 = "changelog2.main:main" + +[tool.poetry.dependencies] +python = ">=3.11,<=3.13" +langchain = "^0.2.16" +llama-index = "^0.11.8" +crewai = { extras = ["tools"], version = "^0.55.2" } +gitpython = "^3.1.43" +langchain-ollama = "^0.1.3" +nltk = "^3.9.1" + +[tool.poetry.group.dev.dependencies] +black = "^24.8.0" +flake8 = "^7.1.1" +pytest = "^8.3.2" +pyinstaller = "^6.10.0" +staticx = "^0.14.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d2d1f1b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,230 @@ +aiohappyeyeballs==2.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +aiohttp==3.10.5 ; python_version >= "3.11" and python_full_version <= "3.13.0" +aiosignal==1.3.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +alembic==1.13.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +annotated-types==0.7.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +anyio==4.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +appdirs==1.4.4 ; python_version >= "3.11" and python_full_version <= "3.13.0" +asgiref==3.8.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +attrs==24.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +auth0-python==4.7.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +backoff==2.2.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +bcrypt==4.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +beautifulsoup4==4.12.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +boto3==1.35.16 ; python_version >= "3.11" and python_full_version <= "3.13.0" +botocore==1.35.16 ; python_version >= "3.11" and python_full_version <= "3.13.0" +build==1.2.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +cachetools==5.5.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +certifi==2024.8.30 ; python_version >= "3.11" and python_full_version <= "3.13.0" +cffi==1.17.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" and (platform_python_implementation != "PyPy" or os_name == "nt") and (platform_python_implementation != "PyPy" or implementation_name != "pypy") +charset-normalizer==3.3.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +chroma-hnswlib==0.7.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +chromadb==0.4.24 ; python_version >= "3.11" and python_full_version <= "3.13.0" +click==8.1.7 ; python_version >= "3.11" and python_full_version <= "3.13.0" +cohere==5.9.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +colorama==0.4.6 ; python_version >= "3.11" and python_full_version <= "3.13.0" and (platform_system == "Windows" or sys_platform == "win32" or os_name == "nt") +coloredlogs==15.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +crewai-tools==0.12.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +crewai[tools]==0.55.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +cryptography==43.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +dataclasses-json==0.6.7 ; python_version >= "3.11" and python_full_version <= "3.13.0" +decorator==5.1.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +deprecated==1.2.14 ; python_version >= "3.11" and python_full_version <= "3.13.0" +deprecation==2.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +dirtyjson==1.0.8 ; python_version >= "3.11" and python_full_version <= "3.13.0" +distro==1.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +docker==7.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +docstring-parser==0.16 ; python_version >= "3.11" and python_full_version <= "3.13.0" +docx2txt==0.8 ; python_version >= "3.11" and python_full_version <= "3.13.0" +embedchain==0.1.121 ; python_version >= "3.11" and python_full_version <= "3.13.0" +fastapi==0.114.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +fastavro==1.9.7 ; python_version >= "3.11" and python_full_version <= "3.13.0" +filelock==3.16.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +flatbuffers==24.3.25 ; python_version >= "3.11" and python_full_version <= "3.13.0" +frozenlist==1.4.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +fsspec==2024.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +gitdb==4.0.11 ; python_version >= "3.11" and python_full_version <= "3.13.0" +gitpython==3.1.43 ; python_version >= "3.11" and python_full_version <= "3.13.0" +google-api-core==2.19.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +google-api-core[grpc]==2.19.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +google-auth==2.34.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +google-cloud-aiplatform==1.66.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +google-cloud-bigquery==3.25.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +google-cloud-core==2.4.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +google-cloud-resource-manager==1.12.5 ; python_version >= "3.11" and python_full_version <= "3.13.0" +google-cloud-storage==2.18.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +google-crc32c==1.6.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +google-resumable-media==2.7.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +googleapis-common-protos==1.65.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +googleapis-common-protos[grpc]==1.65.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +gptcache==0.1.44 ; python_version >= "3.11" and python_full_version <= "3.13.0" +greenlet==3.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +grpc-google-iam-v1==0.13.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +grpcio-status==1.62.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +grpcio-tools==1.62.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +grpcio==1.66.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +h11==0.14.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +h2==4.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +hpack==4.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +httpcore==1.0.5 ; python_version >= "3.11" and python_full_version <= "3.13.0" +httptools==0.6.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +httpx-sse==0.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +httpx==0.27.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +httpx[http2]==0.27.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +huggingface-hub==0.24.6 ; python_version >= "3.11" and python_full_version <= "3.13.0" +humanfriendly==10.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +hyperframe==6.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +idna==3.8 ; python_version >= "3.11" and python_full_version <= "3.13.0" +importlib-metadata==8.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +importlib-resources==6.4.5 ; python_version >= "3.11" and python_full_version <= "3.13.0" +iniconfig==2.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +instructor==1.3.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +jiter==0.4.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +jmespath==1.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +joblib==1.4.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +json-repair==0.25.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +jsonpatch==1.33 ; python_version >= "3.11" and python_full_version <= "3.13.0" +jsonpointer==3.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +jsonref==1.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +kubernetes==30.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +lancedb==0.5.7 ; python_version >= "3.11" and python_full_version <= "3.13.0" +langchain-cohere==0.1.9 ; python_version >= "3.11" and python_full_version <= "3.13.0" +langchain-community==0.2.16 ; python_version >= "3.11" and python_full_version <= "3.13.0" +langchain-core==0.2.39 ; python_version >= "3.11" and python_full_version <= "3.13.0" +langchain-experimental==0.0.65 ; python_version >= "3.11" and python_full_version <= "3.13.0" +langchain-ollama==0.1.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +langchain-openai==0.1.23 ; python_version >= "3.11" and python_full_version <= "3.13.0" +langchain-text-splitters==0.2.4 ; python_version >= "3.11" and python_full_version <= "3.13.0" +langchain==0.2.16 ; python_version >= "3.11" and python_full_version <= "3.13.0" +langsmith==0.1.117 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-cloud==0.0.17 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-agent-openai==0.3.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-cli==0.3.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-core==0.11.8 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-embeddings-openai==0.2.4 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-indices-managed-llama-cloud==0.3.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-legacy==0.9.48.post3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-llms-openai==0.2.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-multi-modal-llms-openai==0.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-program-openai==0.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-question-gen-openai==0.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-readers-file==0.2.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index-readers-llama-parse==0.3.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-index==0.11.8 ; python_version >= "3.11" and python_full_version <= "3.13.0" +llama-parse==0.5.5 ; python_version >= "3.11" and python_full_version <= "3.13.0" +mako==1.3.5 ; python_version >= "3.11" and python_full_version <= "3.13.0" +markdown-it-py==3.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +markupsafe==2.1.5 ; python_version >= "3.11" and python_full_version <= "3.13.0" +marshmallow==3.22.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +mdurl==0.1.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +mem0ai==0.0.20 ; python_version >= "3.11" and python_full_version <= "3.13.0" +mmh3==4.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +monotonic==1.6 ; python_version >= "3.11" and python_full_version <= "3.13.0" +mpmath==1.3.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +multidict==6.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +mypy-extensions==1.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +nest-asyncio==1.6.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +networkx==3.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +nltk==3.9.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +nodeenv==1.9.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +numpy==1.26.4 ; python_version >= "3.11" and python_full_version <= "3.13.0" +oauthlib==3.2.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +ollama==0.3.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +onnxruntime==1.19.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +openai==1.44.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +opentelemetry-api==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +opentelemetry-exporter-otlp-proto-common==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +opentelemetry-exporter-otlp-proto-grpc==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +opentelemetry-exporter-otlp-proto-http==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +opentelemetry-instrumentation-asgi==0.48b0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +opentelemetry-instrumentation-fastapi==0.48b0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +opentelemetry-instrumentation==0.48b0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +opentelemetry-proto==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +opentelemetry-sdk==1.27.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +opentelemetry-semantic-conventions==0.48b0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +opentelemetry-util-http==0.48b0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +orjson==3.10.7 ; python_version >= "3.11" and python_full_version <= "3.13.0" +outcome==1.3.0.post0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +overrides==7.7.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +packaging==24.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pandas==2.2.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +parameterized==0.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pillow==10.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pluggy==1.5.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +portalocker==2.10.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +posthog==3.6.5 ; python_version >= "3.11" and python_full_version <= "3.13.0" +proto-plus==1.24.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +protobuf==4.25.4 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pulsar-client==3.5.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +py==1.11.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pyarrow==17.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pyasn1-modules==0.4.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pyasn1==0.6.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pycparser==2.22 ; python_version >= "3.11" and python_full_version <= "3.13.0" and (platform_python_implementation != "PyPy" or os_name == "nt") and (platform_python_implementation != "PyPy" or implementation_name != "pypy") +pydantic-core==2.23.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pydantic==2.9.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pygments==2.18.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pyjwt==2.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pylance==0.9.18 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pypdf==4.3.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pypika==0.48.9 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pyproject-hooks==1.1.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pyreadline3==3.4.1 ; sys_platform == "win32" and python_version >= "3.11" and python_full_version <= "3.13.0" +pyright==1.1.380 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pysbd==0.3.4 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pysocks==1.7.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pytest==8.3.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +python-dateutil==2.9.0.post0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +python-dotenv==1.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pytube==15.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pytz==2024.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +pywin32==306 ; python_version >= "3.11" and python_full_version <= "3.13.0" and (sys_platform == "win32" or platform_system == "Windows") +pyyaml==6.0.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +qdrant-client==1.11.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +ratelimiter==1.2.0.post0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +regex==2024.7.24 ; python_version >= "3.11" and python_full_version <= "3.13.0" +requests-oauthlib==2.0.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +requests==2.32.3 ; python_version >= "3.11" and python_full_version <= "3.13.0" +retry==0.9.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +rich==13.8.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +rsa==4.9 ; python_version >= "3.11" and python_full_version <= "3.13.0" +s3transfer==0.10.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +schema==0.7.7 ; python_version >= "3.11" and python_full_version <= "3.13.0" +selenium==4.24.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +semver==3.0.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +setuptools==74.1.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +shapely==2.0.6 ; python_version >= "3.11" and python_full_version <= "3.13.0" +shellingham==1.5.4 ; python_version >= "3.11" and python_full_version <= "3.13.0" +six==1.16.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +smmap==5.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +sniffio==1.3.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +sortedcontainers==2.4.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +soupsieve==2.6 ; python_version >= "3.11" and python_full_version <= "3.13.0" +sqlalchemy==2.0.34 ; python_version >= "3.11" and python_full_version <= "3.13.0" +sqlalchemy[asyncio]==2.0.34 ; python_version >= "3.11" and python_full_version <= "3.13.0" +starlette==0.38.5 ; python_version >= "3.11" and python_full_version <= "3.13.0" +striprtf==0.0.26 ; python_version >= "3.11" and python_full_version <= "3.13.0" +sympy==1.13.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +tabulate==0.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +tenacity==8.5.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +tiktoken==0.7.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +tokenizers==0.20.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +tqdm==4.66.5 ; python_version >= "3.11" and python_full_version <= "3.13.0" +trio-websocket==0.11.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +trio==0.26.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +typer==0.12.5 ; python_version >= "3.11" and python_full_version <= "3.13.0" +types-requests==2.32.0.20240907 ; python_version >= "3.11" and python_full_version <= "3.13.0" +typing-extensions==4.12.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +typing-inspect==0.9.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +tzdata==2024.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +urllib3==2.2.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +urllib3[socks]==2.2.2 ; python_version >= "3.11" and python_full_version <= "3.13.0" +uvicorn[standard]==0.30.6 ; python_version >= "3.11" and python_full_version <= "3.13.0" +uvloop==0.20.0 ; (sys_platform != "win32" and sys_platform != "cygwin") and platform_python_implementation != "PyPy" and python_version >= "3.11" and python_full_version <= "3.13.0" +watchfiles==0.24.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +websocket-client==1.8.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +websockets==13.0.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +wrapt==1.16.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +wsproto==1.2.0 ; python_version >= "3.11" and python_full_version <= "3.13.0" +yarl==1.11.1 ; python_version >= "3.11" and python_full_version <= "3.13.0" +zipp==3.20.1 ; python_version >= "3.11" and python_full_version <= "3.13.0"