From 42898080d785fbeb58e9f3b9af0f9259d1dabdcb Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Sun, 3 Aug 2025 23:23:55 +0800 Subject: [PATCH 01/55] Add IssueRequest model to handle issue data in API --- prometheus/app/api/issue.py | 79 +--------------------- prometheus/app/models/__init__.py | 0 prometheus/app/models/requests/__init__.py | 0 prometheus/app/models/requests/issue.py | 77 +++++++++++++++++++++ prometheus/app/models/response/__init__.py | 0 5 files changed, 78 insertions(+), 78 deletions(-) create mode 100644 prometheus/app/models/__init__.py create mode 100644 prometheus/app/models/requests/__init__.py create mode 100644 prometheus/app/models/requests/issue.py create mode 100644 prometheus/app/models/response/__init__.py diff --git a/prometheus/app/api/issue.py b/prometheus/app/api/issue.py index 19963fb9..00fd8b05 100644 --- a/prometheus/app/api/issue.py +++ b/prometheus/app/api/issue.py @@ -1,87 +1,10 @@ -from typing import Mapping, Optional, Sequence - from fastapi import APIRouter, HTTPException, Request -from litellm import Field -from pydantic import BaseModel -from prometheus.lang_graph.graphs.issue_state import IssueType +from prometheus.app.models.requests.issue import IssueRequest router = APIRouter() -class IssueRequest(BaseModel): - issue_number: int = Field(description="The number of the issue", examples=[42]) - issue_title: str = Field( - description="The title of the issue", examples=["There is a memory leak"] - ) - issue_body: str = Field( - description="The description of the issue", examples=["foo/bar.c is causing a memory leak"] - ) - issue_comments: Optional[Sequence[Mapping[str, str]]] = Field( - default=None, - description="Comments on the issue", - examples=[ - [ - {"username": "user1", "comment": "I've experienced this issue as well."}, - { - "username": "user2", - "comment": "A potential fix is to adjust the memory settings.", - }, - ] - ], - ) - issue_type: IssueType = Field( - default=IssueType.AUTO, - description="The type of the issue, set to auto if you do not know", - examples=[IssueType.AUTO], - ) - run_build: Optional[bool] = Field( - default=False, - description="When editing the code, whenver we should run the build to verify the fix", - examples=[False], - ) - run_existing_test: Optional[bool] = Field( - default=False, - description="When editing the code, whenver we should run the existing test to verify the fix", - examples=[False], - ) - number_of_candidate_patch: Optional[int] = Field( - default=4, - description="When the patch is not verfied (through build or test), number of candidate patches we generate to select the best one", - examples=[4], - ) - dockerfile_content: Optional[str] = Field( - default=None, - description="Specify the containerized environment with dockerfile content", - examples=["FROM python:3.11\nWORKDIR /app\nCOPY . /app"], - ) - image_name: Optional[str] = Field( - default=None, - description="Specify the containerized environment with image name that should be pulled from dockerhub", - examples=["python:3.11-slim"], - ) - workdir: Optional[str] = Field( - default=None, - description="If you specified the container environment, you must also specify the workdir", - examples=["/app"], - ) - build_commands: Optional[Sequence[str]] = Field( - default=None, - description="If you specified dockerfile_content and run_build is True, you must also specify the build commands.", - examples=[["pip install -r requirements.txt", "python -m build"]], - ) - test_commands: Optional[Sequence[str]] = Field( - default=None, - description="If you specified dockerfile_content and run_test is True, you must also specify the test commands.", - examples=[["pytest ."]], - ) - push_to_remote: Optional[bool] = Field( - default=False, - description="When editing the code, whenver we should push the changes to a remote branch", - examples=[True], - ) - - @router.post( "/answer/", summary="Process and generate a response for an issue", diff --git a/prometheus/app/models/__init__.py b/prometheus/app/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prometheus/app/models/requests/__init__.py b/prometheus/app/models/requests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prometheus/app/models/requests/issue.py b/prometheus/app/models/requests/issue.py new file mode 100644 index 00000000..52842775 --- /dev/null +++ b/prometheus/app/models/requests/issue.py @@ -0,0 +1,77 @@ +from pydantic import BaseModel, Field +from typing import Mapping, Optional, Sequence + +from prometheus.lang_graph.graphs.issue_state import IssueType + + +class IssueRequest(BaseModel): + issue_number: int = Field(description="The number of the issue", examples=[42]) + issue_title: str = Field( + description="The title of the issue", examples=["There is a memory leak"] + ) + issue_body: str = Field( + description="The description of the issue", examples=["foo/bar.c is causing a memory leak"] + ) + issue_comments: Optional[Sequence[Mapping[str, str]]] = Field( + default=None, + description="Comments on the issue", + examples=[ + [ + {"username": "user1", "comment": "I've experienced this issue as well."}, + { + "username": "user2", + "comment": "A potential fix is to adjust the memory settings.", + }, + ] + ], + ) + issue_type: IssueType = Field( + default=IssueType.AUTO, + description="The type of the issue, set to auto if you do not know", + examples=[IssueType.AUTO], + ) + run_build: Optional[bool] = Field( + default=False, + description="When editing the code, whenver we should run the build to verify the fix", + examples=[False], + ) + run_existing_test: Optional[bool] = Field( + default=False, + description="When editing the code, whenver we should run the existing test to verify the fix", + examples=[False], + ) + number_of_candidate_patch: Optional[int] = Field( + default=4, + description="When the patch is not verfied (through build or test), number of candidate patches we generate to select the best one", + examples=[4], + ) + dockerfile_content: Optional[str] = Field( + default=None, + description="Specify the containerized environment with dockerfile content", + examples=["FROM python:3.11\nWORKDIR /app\nCOPY . /app"], + ) + image_name: Optional[str] = Field( + default=None, + description="Specify the containerized environment with image name that should be pulled from dockerhub", + examples=["python:3.11-slim"], + ) + workdir: Optional[str] = Field( + default=None, + description="If you specified the container environment, you must also specify the workdir", + examples=["/app"], + ) + build_commands: Optional[Sequence[str]] = Field( + default=None, + description="If you specified dockerfile_content and run_build is True, you must also specify the build commands.", + examples=[["pip install -r requirements.txt", "python -m build"]], + ) + test_commands: Optional[Sequence[str]] = Field( + default=None, + description="If you specified dockerfile_content and run_test is True, you must also specify the test commands.", + examples=[["pytest ."]], + ) + push_to_remote: Optional[bool] = Field( + default=False, + description="When editing the code, whenver we should push the changes to a remote branch", + examples=[True], + ) \ No newline at end of file diff --git a/prometheus/app/models/response/__init__.py b/prometheus/app/models/response/__init__.py new file mode 100644 index 00000000..e69de29b From c83f10f1e8ea2e77488da87646fb439d39d2f37b Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Sun, 3 Aug 2025 23:24:21 +0800 Subject: [PATCH 02/55] Fix formatting in issue.py by adding missing newline at end of file --- prometheus/app/models/requests/issue.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/prometheus/app/models/requests/issue.py b/prometheus/app/models/requests/issue.py index 52842775..f0d1f2b7 100644 --- a/prometheus/app/models/requests/issue.py +++ b/prometheus/app/models/requests/issue.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, Field from typing import Mapping, Optional, Sequence +from pydantic import BaseModel, Field + from prometheus.lang_graph.graphs.issue_state import IssueType @@ -74,4 +75,4 @@ class IssueRequest(BaseModel): default=False, description="When editing the code, whenver we should push the changes to a remote branch", examples=[True], - ) \ No newline at end of file + ) From 1c5a2745fa920b82e039cb97ee9abd09de9b6fb8 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Sun, 3 Aug 2025 23:27:00 +0800 Subject: [PATCH 03/55] Remove upload_local_repository endpoint from repository.py --- prometheus/app/api/repository.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/prometheus/app/api/repository.py b/prometheus/app/api/repository.py index 65655cf4..9f4af412 100644 --- a/prometheus/app/api/repository.py +++ b/prometheus/app/api/repository.py @@ -1,35 +1,9 @@ -from pathlib import Path - import git from fastapi import APIRouter, HTTPException, Request router = APIRouter() -@router.get( - "/local/", - description=""" - Upload a local codebase to Prometheus. - """, - responses={ - 404: {"description": "Local repository not found"}, - 200: {"description": "Repository uploaded successfully"}, - }, -) -def upload_local_repository(local_repository: str, request: Request): - local_path = Path(local_repository) - - if not local_path.exists(): - raise HTTPException( - status_code=404, - detail=f"Local repository not found at path: {local_repository}", - ) - - request.app.state.service_coordinator.upload_local_repository(local_path) - - return {"message": "Repository uploaded successfully"} - - @router.get( "/github/", description=""" From 1256c33fb9201a70e442b5bdae50685cb8509d2d Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 00:43:14 +0800 Subject: [PATCH 04/55] Add Settings class for configuration management using Pydantic --- prometheus/configuration/config.py | 53 ++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/prometheus/configuration/config.py b/prometheus/configuration/config.py index ea0b2860..ef3cce2e 100644 --- a/prometheus/configuration/config.py +++ b/prometheus/configuration/config.py @@ -1,3 +1,52 @@ -from dynaconf import Dynaconf +from typing import Literal, List +from pydantic_settings import BaseSettings, SettingsConfigDict -settings = Dynaconf(envvar_prefix="PROMETHEUS", load_dotenv=True) + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file='.env', + env_file_encoding='utf-8', + env_prefix='PROMETHEUS_' + ) + # General settings + version: str = "1.1" + BASE_URL: str = f"/api/v{version}" + ENVIRONMENT: Literal["local", "production"] + BACKEND_CORS_ORIGINS: List[str] + PROJECT_NAME: str = "Prometheus" + + # Logging + LOGGING_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + + # Neo4j + NEO4J_URI: str + NEO4J_USERNAME: str + NEO4J_PASSWORD: str + NEO4J_BATCH_SIZE: int + + # Knowledge Graph + WORKING_DIRECTORY: str + KNOWLEDGE_GRAPH_MAX_AST_DEPTH: int + KNOWLEDGE_GRAPH_CHUNK_SIZE: int + KNOWLEDGE_GRAPH_CHUNK_OVERLAP: int + MAX_TOKEN_PER_NEO4J_RESULT: int + + # LLM models + ADVANCED_MODEL: str + BASE_MODEL: str + + # API Keys + ANTHROPIC_API_KEY: str + GEMINI_API_KEY: str + OPENAI_FORMAT_BASE_URL: str + OPENAI_API_KEY: str + + # Model parameters + MAX_INPUT_TOKENS: int + TEMPERATURE: float + MAX_OUTPUT_TOKENS: int + + # Database + DATABASE_URL: str + + +settings = Settings() From 202c28c0cd6ecd69788243b6fae932050f83119b Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 00:43:38 +0800 Subject: [PATCH 05/55] Refactor config.py for improved formatting and readability --- prometheus/configuration/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/prometheus/configuration/config.py b/prometheus/configuration/config.py index ef3cce2e..11b7432c 100644 --- a/prometheus/configuration/config.py +++ b/prometheus/configuration/config.py @@ -1,12 +1,12 @@ -from typing import Literal, List +from typing import List, Literal + from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): - model_config = SettingsConfigDict(env_file='.env', - env_file_encoding='utf-8', - env_prefix='PROMETHEUS_' - ) + model_config = SettingsConfigDict( + env_file=".env", env_file_encoding="utf-8", env_prefix="PROMETHEUS_" + ) # General settings version: str = "1.1" BASE_URL: str = f"/api/v{version}" From 01b6530851beba1ce576ed34465bec2fb469eb73 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 00:59:12 +0800 Subject: [PATCH 06/55] Implement BaseService class and refactor service classes to inherit from it --- prometheus/app/services/base_service.py | 11 ++ prometheus/app/services/issue_service.py | 140 +++++++++++------- .../app/services/knowledge_graph_service.py | 7 +- prometheus/app/services/llm_service.py | 3 +- prometheus/app/services/neo4j_service.py | 4 +- prometheus/app/services/repository_service.py | 7 +- 6 files changed, 116 insertions(+), 56 deletions(-) create mode 100644 prometheus/app/services/base_service.py diff --git a/prometheus/app/services/base_service.py b/prometheus/app/services/base_service.py new file mode 100644 index 00000000..9b55a597 --- /dev/null +++ b/prometheus/app/services/base_service.py @@ -0,0 +1,11 @@ +class BaseService: + """ + Base class for all services in the Prometheus application. + """ + + def close(self): + """ + Close the service and release any resources. + This method should be overridden by subclasses to implement specific cleanup logic. + """ + pass diff --git a/prometheus/app/services/issue_service.py b/prometheus/app/services/issue_service.py index cebf7a87..924f223c 100644 --- a/prometheus/app/services/issue_service.py +++ b/prometheus/app/services/issue_service.py @@ -1,5 +1,10 @@ +import logging +import traceback +from datetime import datetime +from pathlib import Path from typing import Mapping, Optional, Sequence +from prometheus.app.services.base_service import BaseService from prometheus.app.services.knowledge_graph_service import KnowledgeGraphService from prometheus.app.services.llm_service import LLMService from prometheus.app.services.neo4j_service import Neo4jService @@ -10,7 +15,7 @@ from prometheus.lang_graph.graphs.issue_state import IssueType -class IssueService: +class IssueService(BaseService): def __init__( self, kg_service: KnowledgeGraphService, @@ -18,15 +23,20 @@ def __init__( neo4j_service: Neo4jService, llm_service: LLMService, max_token_per_neo4j_result: int, + working_directory: Path, ): self.kg_service = kg_service self.repository_service = repository_service self.neo4j_service = neo4j_service self.llm_service = llm_service self.max_token_per_neo4j_result = max_token_per_neo4j_result + self.working_directory = working_directory + self.answer_issue_log_dir = self.working_directory / "answer_issue_logs" + self.answer_issue_log_dir.mkdir(parents=True, exist_ok=True) def answer_issue( self, + issue_number: int, issue_title: str, issue_body: str, issue_comments: Sequence[Mapping[str, str]], @@ -39,11 +49,13 @@ def answer_issue( workdir: Optional[str] = None, build_commands: Optional[Sequence[str]] = None, test_commands: Optional[Sequence[str]] = None, + push_to_remote: Optional[bool] = None, ): """ Processes an issue, generates patches if needed, runs optional builds and tests, and returning the results. Args: + issue_number (int): The number of the issue. issue_title (str): The title of the issue. issue_body (str): The body of the issue. issue_comments (Sequence[Mapping[str, str]]): Comments on the issue. @@ -56,6 +68,7 @@ def answer_issue( workdir (Optional[str]): Working directory for the container. build_commands (Optional[Sequence[str]]): Commands to build the project. test_commands (Optional[Sequence[str]]): Commands to test the project. + push_to_remote (Optional[bool]): Whether to push changes to a remote branch. Returns: Tuple containing: - edit_patch (str): The generated patch for the issue. @@ -64,58 +77,81 @@ def answer_issue( - passed_existing_test (bool): Whether the existing tests passed. - issue_response (str): Response generated for the issue. """ - # Construct the working directory - if dockerfile_content or image_name: - container = UserDefinedContainer( - self.kg_service.kg.get_local_path(), - workdir, - build_commands, - test_commands, - dockerfile_content, - image_name, - ) - else: - container = GeneralContainer(self.kg_service.kg.get_local_path()) - # Initialize the issue graph with the necessary services and parameters - issue_graph = IssueGraph( - advanced_model=self.llm_service.advanced_model, - base_model=self.llm_service.base_model, - kg=self.kg_service.kg, - git_repo=self.repository_service.git_repo, - neo4j_driver=self.neo4j_service.neo4j_driver, - max_token_per_neo4j_result=self.max_token_per_neo4j_result, - container=container, - build_commands=build_commands, - test_commands=test_commands, - ) - # Invoke the issue graph with the provided parameters - output_state = issue_graph.invoke( - issue_title, - issue_body, - issue_comments, - issue_type, - run_build, - run_existing_test, - number_of_candidate_patch, - ) + logger = logging.getLogger("prometheus") + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + log_file = self.answer_issue_log_dir / f"{timestamp}.log" + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) - if output_state["issue_type"] == IssueType.BUG: - return ( - output_state["edit_patch"], - output_state["passed_reproducing_test"], - output_state["passed_build"], - output_state["passed_existing_test"], - output_state["issue_response"], + try: + # Construct the working directory + if dockerfile_content or image_name: + container = UserDefinedContainer( + self.kg_service.kg.get_local_path(), + workdir, + build_commands, + test_commands, + dockerfile_content, + image_name, + ) + else: + container = GeneralContainer(self.kg_service.kg.get_local_path()) + # Initialize the issue graph with the necessary services and parameters + issue_graph = IssueGraph( + advanced_model=self.llm_service.advanced_model, + base_model=self.llm_service.base_model, + kg=self.kg_service.kg, + git_repo=self.repository_service.git_repo, + neo4j_driver=self.neo4j_service.neo4j_driver, + max_token_per_neo4j_result=self.max_token_per_neo4j_result, + container=container, + build_commands=build_commands, + test_commands=test_commands, ) - elif output_state["issue_type"] == IssueType.QUESTION: - return ( - None, - False, - False, - False, - output_state["issue_response"], + # Invoke the issue graph with the provided parameters + output_state = issue_graph.invoke( + issue_title, + issue_body, + issue_comments, + issue_type, + run_build, + run_existing_test, + number_of_candidate_patch, ) - raise ValueError( - f"Unknown issue type: {output_state['issue_type']}. Expected BUG or QUESTION." - ) + if output_state["issue_type"] == IssueType.BUG: + # push to remote if requested + remote_branch_name = None + if output_state["edit_patch"] and push_to_remote: + remote_branch_name = self.repository_service.push_change_to_remote( + f"Fixes #{issue_number}", output_state["edit_patch"] + ) + + return ( + remote_branch_name, + output_state["edit_patch"], + output_state["passed_reproducing_test"], + output_state["passed_build"], + output_state["passed_existing_test"], + output_state["issue_response"], + ) + elif output_state["issue_type"] == IssueType.QUESTION: + return ( + None, + False, + False, + False, + output_state["issue_response"], + ) + + raise ValueError( + f"Unknown issue type: {output_state['issue_type']}. Expected BUG or QUESTION." + ) + except Exception as e: + logger.error(f"Error in answer_issue: {str(e)}\n{traceback.format_exc()}") + return None, None, False, False, False, None + finally: + logger.removeHandler(file_handler) + file_handler.close() diff --git a/prometheus/app/services/knowledge_graph_service.py b/prometheus/app/services/knowledge_graph_service.py index 5ccaee41..f85751ec 100644 --- a/prometheus/app/services/knowledge_graph_service.py +++ b/prometheus/app/services/knowledge_graph_service.py @@ -3,12 +3,13 @@ from pathlib import Path from typing import Optional +from prometheus.app.services.base_service import BaseService from prometheus.app.services.neo4j_service import Neo4jService from prometheus.graph.knowledge_graph import KnowledgeGraph from prometheus.neo4j import knowledge_graph_handler -class KnowledgeGraphService: +class KnowledgeGraphService(BaseService): """Manages the lifecycle and operations of Knowledge Graphs. This service handles the creation, persistence, and management of Knowledge Graphs @@ -84,3 +85,7 @@ def exists(self) -> bool: def clear(self): self.kg_handler.clear_knowledge_graph() self.kg = None + + def close(self): + """Clear the knowledge graph before closing the service.""" + self.clear() diff --git a/prometheus/app/services/llm_service.py b/prometheus/app/services/llm_service.py index 33567807..143a5669 100644 --- a/prometheus/app/services/llm_service.py +++ b/prometheus/app/services/llm_service.py @@ -4,10 +4,11 @@ from langchain_core.language_models.chat_models import BaseChatModel from langchain_google_genai import ChatGoogleGenerativeAI +from prometheus.app.services.base_service import BaseService from prometheus.chat_models.custom_chat_openai import CustomChatOpenAI -class LLMService: +class LLMService(BaseService): def __init__( self, advanced_model_name: str, diff --git a/prometheus/app/services/neo4j_service.py b/prometheus/app/services/neo4j_service.py index 2ee7f08e..bdb1cfd7 100644 --- a/prometheus/app/services/neo4j_service.py +++ b/prometheus/app/services/neo4j_service.py @@ -2,8 +2,10 @@ from neo4j import GraphDatabase +from prometheus.app.services.base_service import BaseService -class Neo4jService: + +class Neo4jService(BaseService): def __init__(self, neo4j_uri: str, neo4j_username: str, neo4j_password: str): self.neo4j_driver = GraphDatabase.driver( neo4j_uri, diff --git a/prometheus/app/services/repository_service.py b/prometheus/app/services/repository_service.py index 8a02717d..5e6d88d6 100644 --- a/prometheus/app/services/repository_service.py +++ b/prometheus/app/services/repository_service.py @@ -5,11 +5,12 @@ from pathlib import Path from typing import Optional +from prometheus.app.services.base_service import BaseService from prometheus.app.services.knowledge_graph_service import KnowledgeGraphService from prometheus.git.git_repository import GitRepository -class RepositoryService: +class RepositoryService(BaseService): """Manages repository operations. This service provides functionality for Git repository operations including @@ -93,3 +94,7 @@ def clean(self): self.git_repo = None shutil.rmtree(self.target_directory) self.target_directory.mkdir(parents=True) + + def close(self): + """Cleans up the repository service before shutdown.""" + self.clean() From e17a25c12546bdbc84c49d95821f174561e45da7 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 00:59:32 +0800 Subject: [PATCH 07/55] Refactor service initialization to return a dictionary of services instead of a ServiceCoordinator --- prometheus/app/dependencies.py | 25 +++++++++---------------- prometheus/app/main.py | 7 ++++--- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/prometheus/app/dependencies.py b/prometheus/app/dependencies.py index 5b3986d1..684dfe04 100644 --- a/prometheus/app/dependencies.py +++ b/prometheus/app/dependencies.py @@ -1,17 +1,15 @@ """Initializes and configures all prometheus services.""" -from pathlib import Path - +from prometheus.app.services.base_service import BaseService from prometheus.app.services.issue_service import IssueService from prometheus.app.services.knowledge_graph_service import KnowledgeGraphService from prometheus.app.services.llm_service import LLMService from prometheus.app.services.neo4j_service import Neo4jService from prometheus.app.services.repository_service import RepositoryService -from prometheus.app.services.service_coordinator import ServiceCoordinator from prometheus.configuration.config import settings -def initialize_services() -> ServiceCoordinator: +def initialize_services() -> dict[str, BaseService]: """Initializes and configures the complete prometheus service stack. This function creates and configures all required services for prometheus @@ -60,15 +58,10 @@ def initialize_services() -> ServiceCoordinator: settings.MAX_TOKEN_PER_NEO4J_RESULT, ) - service_coordinator = ServiceCoordinator( - issue_service, - knowledge_graph_service, - llm_service, - neo4j_service, - repository_service, - settings.MAX_TOKEN_PER_NEO4J_RESULT, - settings.GITHUB_ACCESS_TOKEN, - Path(settings.WORKING_DIRECTORY).absolute(), - ) - - return service_coordinator + return { + "neo4j_service": neo4j_service, + "llm_service": llm_service, + "knowledge_graph_service": knowledge_graph_service, + "repository_service": repository_service, + "issue_service": issue_service, + } diff --git a/prometheus/app/main.py b/prometheus/app/main.py index 2eeca317..8947c7b9 100644 --- a/prometheus/app/main.py +++ b/prometheus/app/main.py @@ -35,12 +35,13 @@ @asynccontextmanager async def lifespan(app: FastAPI): # Initialization on startup - app.state.service_coordinator = dependencies.initialize_services() + app.state.service = dependencies.initialize_services() # Initialization Completed yield # Cleanup on shutdown - app.state.service_coordinator.clear() - app.state.service_coordinator.close() + logger.info("Shutting down services...") + for service in app.state.service.values(): + service.close() app = FastAPI(lifespan=lifespan) From 5ecf4a13f3afc9482d54007690ca00d93b3d7686 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 01:07:45 +0800 Subject: [PATCH 08/55] Add GITHUB_ACCESS_TOKEN to configuration settings --- prometheus/configuration/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/prometheus/configuration/config.py b/prometheus/configuration/config.py index 11b7432c..ede7c576 100644 --- a/prometheus/configuration/config.py +++ b/prometheus/configuration/config.py @@ -45,6 +45,9 @@ class Settings(BaseSettings): TEMPERATURE: float MAX_OUTPUT_TOKENS: int + # GitHub Token + GITHUB_ACCESS_TOKEN: str + # Database DATABASE_URL: str From 385a9bbe96bd32325132f10504d098afa30a506a Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 01:21:57 +0800 Subject: [PATCH 09/55] Refactor GitHub repository upload functions to use service dictionary and improve knowledge graph handling --- prometheus/app/api/issue.py | 2 +- prometheus/app/api/repository.py | 43 +++- .../app/services/service_coordinator.py | 213 ------------------ 3 files changed, 38 insertions(+), 220 deletions(-) delete mode 100644 prometheus/app/services/service_coordinator.py diff --git a/prometheus/app/api/issue.py b/prometheus/app/api/issue.py index 00fd8b05..9a183dab 100644 --- a/prometheus/app/api/issue.py +++ b/prometheus/app/api/issue.py @@ -12,7 +12,7 @@ response_description="Returns the patch, test results, and issue response", ) def answer_issue(issue: IssueRequest, request: Request): - if not request.app.state.service_coordinator.exists_knowledge_graph(): + if not request.app.state.service["knowledge_graph_service"].exists(): raise HTTPException( status_code=404, detail="A repository is not uploaded, use /repository/ endpoint to upload one", diff --git a/prometheus/app/api/repository.py b/prometheus/app/api/repository.py index 9f4af412..3c70fae0 100644 --- a/prometheus/app/api/repository.py +++ b/prometheus/app/api/repository.py @@ -10,9 +10,24 @@ Upload a GitHub repository to Prometheus, default to the latest commit in the main branch. """, ) -def upload_github_repository(https_url: str, request: Request): +def upload_github_repository(github_token: str, https_url: str, request: Request): try: - request.app.state.service_coordinator.upload_github_repository(https_url) + # Get the repository and knowledge graph services + repository_service = request.app.state.service["repository_service"] + knowledge_graph_service = request.app.state.service["knowledge_graph_service"] + + # Clean the services to ensure no previous data is present + repository_service.clean() + knowledge_graph_service.clean() + + # Clone the repository + saved_path = repository_service.clone_github_repo( + github_token, https_url + ) + # Build and save the knowledge graph from the cloned repository + knowledge_graph_service.build_and_save_knowledge_graph( + saved_path, https_url + ) except git.exc.GitCommandError: raise HTTPException(status_code=400, detail=f"Unable to clone {https_url}") @@ -23,9 +38,25 @@ def upload_github_repository(https_url: str, request: Request): Upload a GitHub repository at a specific commit to Prometheus. """, ) -def upload_github_repository_at_commit(https_url: str, commit_id: str, request: Request): +def upload_github_repository_at_commit(github_token, https_url: str, commit_id: str, request: Request): try: - request.app.state.service_coordinator.upload_github_repository(https_url, commit_id) + # Get the repository and knowledge graph services + repository_service = request.app.state.service["repository_service"] + knowledge_graph_service = request.app.state.service["knowledge_graph_service"] + + # Clean the services to ensure no previous data is present + repository_service.clean() + knowledge_graph_service.clean() + + # Clone the repository + saved_path = repository_service.clone_github_repo( + github_token, https_url, commit_id + ) + + # Build and save the knowledge graph from the cloned repository + knowledge_graph_service.build_and_save_knowledge_graph( + saved_path, https_url, commit_id + ) except git.exc.GitCommandError: raise HTTPException( status_code=400, detail=f"Unable to clone {https_url} with commit {commit_id}" @@ -39,7 +70,7 @@ def upload_github_repository_at_commit(https_url: str, commit_id: str, request: """, ) def delete(request: Request): - if not request.app.state.service_coordinator.exists_knowledge_graph(): + if not request.app.state.service["knowledge_graph_service"].exists(): return {"message": "No knowledge graph to delete"} request.app.state.service_coordinator.clear() @@ -54,4 +85,4 @@ def delete(request: Request): response_model=bool, ) def knowledge_graph_exists(request: Request) -> bool: - return request.app.state.service_coordinator.exists_knowledge_graph() + return request.app.state.service["knowledge_graph_service"].exists() diff --git a/prometheus/app/services/service_coordinator.py b/prometheus/app/services/service_coordinator.py deleted file mode 100644 index 05955eb2..00000000 --- a/prometheus/app/services/service_coordinator.py +++ /dev/null @@ -1,213 +0,0 @@ -"""Central coordinator for managing interactions between various prometheus services. - -This coordinator orchestrates the interactions between multiple specialized services -including knowledge graph management, LLM operations, database connections, and -repository management. It provides a unified interface for codebase analysis, -issue handling, and conversation management. -""" - -import logging -import traceback -from datetime import datetime -from pathlib import Path -from typing import Mapping, Optional, Sequence - -from prometheus.app.services.issue_service import IssueService -from prometheus.app.services.knowledge_graph_service import KnowledgeGraphService -from prometheus.app.services.llm_service import LLMService -from prometheus.app.services.neo4j_service import Neo4jService -from prometheus.app.services.repository_service import RepositoryService -from prometheus.lang_graph.graphs.issue_state import IssueType - - -class ServiceCoordinator: - """Coordinates operations between various prometheus services. - - This class serves as the central orchestrator for all service interactions, - managing the lifecycle of various operations including codebase analysis, - issue handling, and conversation management. It ensures proper initialization, - coordination, and cleanup of all dependent services. - """ - - def __init__( - self, - issue_service: IssueService, - knowledge_graph_service: KnowledgeGraphService, - llm_service: LLMService, - neo4j_service: Neo4jService, - repository_service: RepositoryService, - max_token_per_neo4j_result: int, - github_token: str, - working_directory: Path, - ): - """Initializes the service coordinator with required services. - - Args: - issue_service: Service for issue handling. - knowledge_graph_service: Service for knowledge graph operations. - llm_service: Service for language model operations. - neo4j_service: Service for Neo4j database operations. - repository_service: Service for repository management. - max_token_per_neo4j_result: Maximum number of tokens per Neo4j result. - github_token: GitHub access token for repository operations. - working_directory: Working directory for all Prometheus related files. - """ - self.issue_service = issue_service - self.knowledge_graph_service = knowledge_graph_service - self.llm_service = llm_service - self.neo4j_service = neo4j_service - self.repository_service = repository_service - self.max_token_per_neo4j_result = max_token_per_neo4j_result - self.github_token = github_token - self.working_directory = working_directory - self.answer_issue_log_dir = self.working_directory / "answer_issue_logs" - self.answer_issue_log_dir.mkdir(parents=True, exist_ok=True) - self._logger = logging.getLogger("prometheus.app.services.service_coordinator") - - if ( - self.knowledge_graph_service.get_local_path() - != self.repository_service.get_working_dir() - ): - self._logger.critical( - f"Knowledge graph and repository working directories do not match: {self.knowledge_graph_service.get_local_path()} vs {self.repository_service.get_working_dir()}. Resetting all services." - ) - self.clear() - - def answer_issue( - self, - issue_number: int, - issue_title: str, - issue_body: str, - issue_comments: Sequence[Mapping[str, str]], - issue_type: IssueType, - run_build: bool, - run_existing_test: bool, - number_of_candidate_patch: int, - dockerfile_content: Optional[str] = None, - image_name: Optional[str] = None, - workdir: Optional[str] = None, - build_commands: Optional[Sequence[str]] = None, - test_commands: Optional[Sequence[str]] = None, - push_to_remote: Optional[bool] = None, - ): - """ - Processes an issue, generates patches if needed, runs optional builds and tests, - and can push changes to a remote branch. - - Args: - issue_number: The issue number to process. - issue_title: Title of the issue. - issue_body: Body of the issue. - issue_comments: Comments on the issue. - issue_type: Type of the issue (e.g., bug, feature). - run_build: Whether to run a build after applying the patch. - run_existing_test: Whether to run existing tests after applying the patch. - number_of_candidate_patch: Number of candidate patches to generate. - dockerfile_content: Optional Dockerfile content for user-defined environment. - image_name: Optional name for the Docker image. - workdir: Working directory for the container. - build_commands: Commands to build the project. - test_commands: Commands to test the project. - push_to_remote: Whether to push changes to a remote branch. - Returns: - A tuple containing: - - remote_branch_name: Name of the remote branch if changes were pushed. - - patch: The generated patch for the issue. - - passed_reproducing_test: Whether the reproducing test passed. - - passed_build: Whether the build passed. - - passed_existing_test: Whether existing tests passed. - - issue_response: Response from the issue service after processing. - """ - logger = logging.getLogger("prometheus") - formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - log_file = self.answer_issue_log_dir / f"{timestamp}.log" - file_handler = logging.FileHandler(log_file) - file_handler.setFormatter(formatter) - logger.addHandler(file_handler) - - try: - # fix issue - patch, passed_reproducing_test, passed_build, passed_existing_test, issue_response = ( - self.issue_service.answer_issue( - issue_title=issue_title, - issue_body=issue_body, - issue_comments=issue_comments, - issue_type=issue_type, - run_build=run_build, - run_existing_test=run_existing_test, - number_of_candidate_patch=number_of_candidate_patch, - dockerfile_content=dockerfile_content, - image_name=image_name, - workdir=workdir, - build_commands=build_commands, - test_commands=test_commands, - ) - ) - # push to remote if requested - remote_branch_name = None - if patch and push_to_remote: - remote_branch_name = self.repository_service.push_change_to_remote( - f"Fixes #{issue_number}", patch - ) - return ( - remote_branch_name, - patch, - passed_reproducing_test, - passed_build, - passed_existing_test, - issue_response, - ) - except Exception as e: - logger.error(f"Error in answer_issue: {str(e)}\n{traceback.format_exc()}") - return None, None, False, False, False, None - finally: - logger.removeHandler(file_handler) - file_handler.close() - - def exists_knowledge_graph(self) -> bool: - return self.knowledge_graph_service.exists() - - def upload_local_repository(self, path: Path): - """Uploads a local repository. - - Args: - path: Path to the local repository directory. - """ - self.clear() - self.knowledge_graph_service.build_and_save_knowledge_graph(path) - - def upload_github_repository(self, https_url: str, commit_id: Optional[str] = None): - """Uploads a GitHub repository. - - Args: - https_url: HTTPS URL of the GitHub repository. - commit_id: Optional specific commit to analyze. - """ - # CLean the existing knowledge graph and repository state - self.clear() - # Clone the repository - saved_path = self.repository_service.clone_github_repo( - self.github_token, https_url, commit_id - ) - # Build and save the knowledge graph from the cloned repository - self.knowledge_graph_service.build_and_save_knowledge_graph( - saved_path, https_url, commit_id - ) - - def clear(self): - """Clears all service state and working directories. - - Resets the knowledge graph, cleans repository working directory, - and reinitializes subgraph services. - """ - self.knowledge_graph_service.clear() - self.repository_service.clean() - - def close(self): - """Closes all database connections and releases resources. - - This method should be called when the coordinator is no longer needed - to ensure proper cleanup of database connections and resources. - """ - self.neo4j_service.close() From eba7732ec8bc2a76d47b0c451d0fc8137f6878fd Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 01:34:46 +0800 Subject: [PATCH 10/55] Update working_directory type to string and ensure proper Path handling for answer_issue_log_dir --- prometheus/app/dependencies.py | 1 + prometheus/app/services/issue_service.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/prometheus/app/dependencies.py b/prometheus/app/dependencies.py index 684dfe04..cacb6112 100644 --- a/prometheus/app/dependencies.py +++ b/prometheus/app/dependencies.py @@ -56,6 +56,7 @@ def initialize_services() -> dict[str, BaseService]: neo4j_service, llm_service, settings.MAX_TOKEN_PER_NEO4J_RESULT, + settings.WORKING_DIRECTORY, ) return { diff --git a/prometheus/app/services/issue_service.py b/prometheus/app/services/issue_service.py index 924f223c..0d7800be 100644 --- a/prometheus/app/services/issue_service.py +++ b/prometheus/app/services/issue_service.py @@ -23,7 +23,7 @@ def __init__( neo4j_service: Neo4jService, llm_service: LLMService, max_token_per_neo4j_result: int, - working_directory: Path, + working_directory: str, ): self.kg_service = kg_service self.repository_service = repository_service @@ -31,7 +31,7 @@ def __init__( self.llm_service = llm_service self.max_token_per_neo4j_result = max_token_per_neo4j_result self.working_directory = working_directory - self.answer_issue_log_dir = self.working_directory / "answer_issue_logs" + self.answer_issue_log_dir = Path(self.working_directory) / "answer_issue_logs" self.answer_issue_log_dir.mkdir(parents=True, exist_ok=True) def answer_issue( From f51660c585ca6f8fa520306ef193bf8958d49ac7 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 01:35:32 +0800 Subject: [PATCH 11/55] Rename OPENAI_API_KEY to OPENAI_FORMAT_API_KEY and update example.env to include new general settings --- example.env | 7 ++++--- prometheus/configuration/config.py | 5 +---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/example.env b/example.env index d2b2c063..6dd48b14 100644 --- a/example.env +++ b/example.env @@ -1,6 +1,10 @@ # Logging PROMETHEUS_LOGGING_LEVEL=DEBUG +# General settings +PROMETHEUS_ENVIRONMENT=local +PROMETHEUS_BACKEND_CORS_ORIGINS=["http://localhost:9002"] + # Neo4j settings PROMETHEUS_NEO4J_URI=bolt://neo4j:7687 PROMETHEUS_NEO4J_USERNAME=neo4j @@ -29,8 +33,5 @@ PROMETHEUS_MAX_INPUT_TOKENS=64000 PROMETHEUS_TEMPERATURE=0.3 PROMETHEUS_MAX_OUTPUT_TOKENS=15000 -# GitHub settings -PROMETHEUS_GITHUB_ACCESS_TOKEN=github_access_token - # Database settings PROMETHEUS_DATABASE_URL=postgresql://postgres:password@localhost:5432/postgres?sslmode=disable diff --git a/prometheus/configuration/config.py b/prometheus/configuration/config.py index ede7c576..e43b8484 100644 --- a/prometheus/configuration/config.py +++ b/prometheus/configuration/config.py @@ -38,16 +38,13 @@ class Settings(BaseSettings): ANTHROPIC_API_KEY: str GEMINI_API_KEY: str OPENAI_FORMAT_BASE_URL: str - OPENAI_API_KEY: str + OPENAI_FORMAT_API_KEY: str # Model parameters MAX_INPUT_TOKENS: int TEMPERATURE: float MAX_OUTPUT_TOKENS: int - # GitHub Token - GITHUB_ACCESS_TOKEN: str - # Database DATABASE_URL: str From a1ae76638e25ecc2479bc28a00fc8f2158352446 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 01:35:37 +0800 Subject: [PATCH 12/55] Refactor repository cloning functions for improved readability and consistency --- prometheus/app/api/repository.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/prometheus/app/api/repository.py b/prometheus/app/api/repository.py index 3c70fae0..052aa3b5 100644 --- a/prometheus/app/api/repository.py +++ b/prometheus/app/api/repository.py @@ -21,13 +21,9 @@ def upload_github_repository(github_token: str, https_url: str, request: Request knowledge_graph_service.clean() # Clone the repository - saved_path = repository_service.clone_github_repo( - github_token, https_url - ) + saved_path = repository_service.clone_github_repo(github_token, https_url) # Build and save the knowledge graph from the cloned repository - knowledge_graph_service.build_and_save_knowledge_graph( - saved_path, https_url - ) + knowledge_graph_service.build_and_save_knowledge_graph(saved_path, https_url) except git.exc.GitCommandError: raise HTTPException(status_code=400, detail=f"Unable to clone {https_url}") @@ -38,7 +34,9 @@ def upload_github_repository(github_token: str, https_url: str, request: Request Upload a GitHub repository at a specific commit to Prometheus. """, ) -def upload_github_repository_at_commit(github_token, https_url: str, commit_id: str, request: Request): +def upload_github_repository_at_commit( + github_token, https_url: str, commit_id: str, request: Request +): try: # Get the repository and knowledge graph services repository_service = request.app.state.service["repository_service"] @@ -49,14 +47,10 @@ def upload_github_repository_at_commit(github_token, https_url: str, commit_id: knowledge_graph_service.clean() # Clone the repository - saved_path = repository_service.clone_github_repo( - github_token, https_url, commit_id - ) + saved_path = repository_service.clone_github_repo(github_token, https_url, commit_id) # Build and save the knowledge graph from the cloned repository - knowledge_graph_service.build_and_save_knowledge_graph( - saved_path, https_url, commit_id - ) + knowledge_graph_service.build_and_save_knowledge_graph(saved_path, https_url, commit_id) except git.exc.GitCommandError: raise HTTPException( status_code=400, detail=f"Unable to clone {https_url} with commit {commit_id}" From ea618a614a4945bf417ec6b387e2b457a87c3717 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 01:52:43 +0800 Subject: [PATCH 13/55] Add general settings to Docker Compose files and remove GitHub access token --- docker-compose.win_mac.yml | 7 ++++--- docker-compose.yml | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docker-compose.win_mac.yml b/docker-compose.win_mac.yml index 8eeec133..bdeb6cd2 100644 --- a/docker-compose.win_mac.yml +++ b/docker-compose.win_mac.yml @@ -36,6 +36,10 @@ services: # Logging - PROMETHEUS_LOGGING_LEVEL=${PROMETHEUS_LOGGING_LEVEL} + # General settings + - PROMETHEUS_ENVIRONMENT=${PROMETHEUS_ENVIRONMENT} + - PROMETHEUS_BACKEND_CORS_ORIGINS=${PROMETHEUS_BACKEND_CORS_ORIGINS} + # Neo4j settings - PROMETHEUS_NEO4J_URI=${PROMETHEUS_NEO4J_URI} - PROMETHEUS_NEO4J_USERNAME=${PROMETHEUS_NEO4J_USERNAME} @@ -64,9 +68,6 @@ services: - PROMETHEUS_MAX_OUTPUT_TOKENS=${PROMETHEUS_MAX_OUTPUT_TOKENS} - PROMETHEUS_TEMPERATURE=${PROMETHEUS_TEMPERATURE} - # GitHub settings - - PROMETHEUS_GITHUB_ACCESS_TOKEN=${PROMETHEUS_GITHUB_ACCESS_TOKEN} - # Database settings - PROMETHEUS_DATABASE_URL=${PROMETHEUS_DATABASE_URL} networks: diff --git a/docker-compose.yml b/docker-compose.yml index 03fb66f3..72f3a56b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,6 +57,10 @@ services: # Logging - PROMETHEUS_LOGGING_LEVEL=${PROMETHEUS_LOGGING_LEVEL} + # General settings + - PROMETHEUS_ENVIRONMENT=${PROMETHEUS_ENVIRONMENT} + - PROMETHEUS_BACKEND_CORS_ORIGINS=${PROMETHEUS_BACKEND_CORS_ORIGINS} + # Neo4j settings - PROMETHEUS_NEO4J_URI=${PROMETHEUS_NEO4J_URI} - PROMETHEUS_NEO4J_USERNAME=${PROMETHEUS_NEO4J_USERNAME} @@ -85,9 +89,6 @@ services: - PROMETHEUS_MAX_OUTPUT_TOKENS=${PROMETHEUS_MAX_OUTPUT_TOKENS} - PROMETHEUS_TEMPERATURE=${PROMETHEUS_TEMPERATURE} - # GitHub settings - - PROMETHEUS_GITHUB_ACCESS_TOKEN=${PROMETHEUS_GITHUB_ACCESS_TOKEN} - # Database settings - PROMETHEUS_DATABASE_URL=${PROMETHEUS_DATABASE_URL} volumes: From 41ef1d9ad91584c29b463a73f34eb9fcd72690d2 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 01:56:42 +0800 Subject: [PATCH 14/55] Refactor GitHub repository upload functions to use type hints and improve service cleanup --- prometheus/app/api/repository.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/prometheus/app/api/repository.py b/prometheus/app/api/repository.py index 052aa3b5..f78e5fa3 100644 --- a/prometheus/app/api/repository.py +++ b/prometheus/app/api/repository.py @@ -1,6 +1,9 @@ import git from fastapi import APIRouter, HTTPException, Request +from prometheus.app.services.knowledge_graph_service import KnowledgeGraphService +from prometheus.app.services.repository_service import RepositoryService + router = APIRouter() @@ -13,12 +16,14 @@ def upload_github_repository(github_token: str, https_url: str, request: Request): try: # Get the repository and knowledge graph services - repository_service = request.app.state.service["repository_service"] - knowledge_graph_service = request.app.state.service["knowledge_graph_service"] + repository_service: RepositoryService = request.app.state.service["repository_service"] + knowledge_graph_service: KnowledgeGraphService = request.app.state.service[ + "knowledge_graph_service" + ] # Clean the services to ensure no previous data is present repository_service.clean() - knowledge_graph_service.clean() + knowledge_graph_service.clear() # Clone the repository saved_path = repository_service.clone_github_repo(github_token, https_url) @@ -39,12 +44,14 @@ def upload_github_repository_at_commit( ): try: # Get the repository and knowledge graph services - repository_service = request.app.state.service["repository_service"] - knowledge_graph_service = request.app.state.service["knowledge_graph_service"] + repository_service: RepositoryService = request.app.state.service["repository_service"] + knowledge_graph_service: KnowledgeGraphService = request.app.state.service[ + "knowledge_graph_service" + ] # Clean the services to ensure no previous data is present repository_service.clean() - knowledge_graph_service.clean() + knowledge_graph_service.clear() # Clone the repository saved_path = repository_service.clone_github_repo(github_token, https_url, commit_id) From a8376ea0180f25bccf1582c3e8ba6a8e463faf41 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 02:08:23 +0800 Subject: [PATCH 15/55] Refactor issue handling and repository deletion logic for improved clarity and service access --- prometheus/app/api/issue.py | 2 +- prometheus/app/api/repository.py | 11 +++++++++-- prometheus/app/services/issue_service.py | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/prometheus/app/api/issue.py b/prometheus/app/api/issue.py index 9a183dab..10bd5753 100644 --- a/prometheus/app/api/issue.py +++ b/prometheus/app/api/issue.py @@ -32,7 +32,7 @@ def answer_issue(issue: IssueRequest, request: Request): passed_build, passed_existing_test, issue_response, - ) = request.app.state.service_coordinator.answer_issue( + ) = request.app.state.service["issue_service"].answer_issue( issue_number=issue.issue_number, issue_title=issue.issue_title, issue_body=issue.issue_body, diff --git a/prometheus/app/api/repository.py b/prometheus/app/api/repository.py index f78e5fa3..cb177555 100644 --- a/prometheus/app/api/repository.py +++ b/prometheus/app/api/repository.py @@ -71,10 +71,17 @@ def upload_github_repository_at_commit( """, ) def delete(request: Request): - if not request.app.state.service["knowledge_graph_service"].exists(): + knowledge_graph_service: KnowledgeGraphService = request.app.state.service[ + "knowledge_graph_service" + ] + if not knowledge_graph_service.exists(): return {"message": "No knowledge graph to delete"} + # Get the repository service to clean up the repository data + repository_service: RepositoryService = request.app.state.service["repository_service"] - request.app.state.service_coordinator.clear() + # Clear the knowledge graph and repository data + knowledge_graph_service.clear() + repository_service.clean() return {"message": "Successfully deleted knowledge graph"} diff --git a/prometheus/app/services/issue_service.py b/prometheus/app/services/issue_service.py index 0d7800be..a66898bc 100644 --- a/prometheus/app/services/issue_service.py +++ b/prometheus/app/services/issue_service.py @@ -139,6 +139,7 @@ def answer_issue( ) elif output_state["issue_type"] == IssueType.QUESTION: return ( + None, None, False, False, From 870d1f86ce2b80f76ccf08c6a74efeb02be3620c Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 02:10:47 +0800 Subject: [PATCH 16/55] Add logging for environment and CORS origins in main.py --- prometheus/app/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prometheus/app/main.py b/prometheus/app/main.py index 8947c7b9..59f0e5f2 100644 --- a/prometheus/app/main.py +++ b/prometheus/app/main.py @@ -19,6 +19,8 @@ # Log the configuration settings logger.info(f"LOGGING_LEVEL={settings.LOGGING_LEVEL}") +logger.info(f"ENVIRONMENT={settings.ENVIRONMENT}") +logger.info(f"BACKEND_CORS_ORIGINS={settings.BACKEND_CORS_ORIGINS}") logger.info(f"ADVANCED_MODEL={settings.ADVANCED_MODEL}") logger.info(f"BASE_MODEL={settings.BASE_MODEL}") logger.info(f"NEO4J_BATCH_SIZE={settings.NEO4J_BATCH_SIZE}") From c21d744f808046652e59cc534d4fa9cc6342d22e Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 02:20:26 +0800 Subject: [PATCH 17/55] Add custom unique ID generation for API routes in FastAPI application --- prometheus/app/main.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/prometheus/app/main.py b/prometheus/app/main.py index 59f0e5f2..13e72e37 100644 --- a/prometheus/app/main.py +++ b/prometheus/app/main.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone from fastapi import FastAPI +from fastapi.routing import APIRoute from prometheus.app import dependencies from prometheus.app.api import issue, repository @@ -46,7 +47,21 @@ async def lifespan(app: FastAPI): service.close() -app = FastAPI(lifespan=lifespan) +def custom_generate_unique_id(route: APIRoute) -> str: + """ + Custom function to generate unique IDs for API routes based on their tags and names. + """ + return f"{route.tags[0]}-{route.name}" + + +app = FastAPI( + lifespan=lifespan, + title=settings.PROJECT_NAME, # Title on generated documentation + openapi_url=f"{settings.BASE_URL}/openapi.json", # Path to generated OpenAPI documentation + generate_unique_id_function=custom_generate_unique_id, # Custom function for generating unique route IDs + version=settings.version, # Version of the API + debug=True if settings.ENVIRONMENT == "local" else False, +) app.include_router(repository.router, prefix="/repository", tags=["repository"]) app.include_router(issue.router, prefix="/issue", tags=["issue"]) From 0fef3b899a7b25a34a532a510b98fe100affa001 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 02:28:07 +0800 Subject: [PATCH 18/55] Update API documentation to correct endpoint URL for issue answering --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0044bed3..f2ccaba8 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ governed by a state machine to ensure code quality through automated reviews, bu You can ask Prometheus to analyze and answer a specific issue in your codebase using the `/issue/answer/` API endpoint. - **Endpoint:** `POST /issue/answer/` - - **Request Body:** JSON object matching the `IssueRequest` schema (see [API Documents](http://localhost:9002/docs#/issue/answer_issue_issue_answer__post)) + - **Request Body:** JSON object matching the `IssueRequest` schema (see [API Documents](http://127.0.0.1:9002/docs#/issue/issue-answer_issue)) - **Response:** Returns the generated patch, test/build results, and a summary response. --- From 6df68b96527eb2060d9ee2efb2b88d159b3764ae Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:05:07 +0800 Subject: [PATCH 19/55] Add DatabaseService for database management and integrate into dependencies --- prometheus/app/db.py | 57 --------------------- prometheus/app/dependencies.py | 3 ++ prometheus/app/services/database_service.py | 22 ++++++++ 3 files changed, 25 insertions(+), 57 deletions(-) delete mode 100644 prometheus/app/db.py create mode 100644 prometheus/app/services/database_service.py diff --git a/prometheus/app/db.py b/prometheus/app/db.py deleted file mode 100644 index a95fa8f1..00000000 --- a/prometheus/app/db.py +++ /dev/null @@ -1,57 +0,0 @@ -import logging -from typing import Optional - -from passlib.hash import bcrypt -from sqlmodel import Session, SQLModel, create_engine - -from prometheus.app.entity.user import User -from prometheus.configuration.config import settings - -engine = create_engine(settings.DATABASE_URL, echo=True) -_logger = logging.getLogger("prometheus.app.db") - - -# Create the database and tables -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -# Create a superuser and commit it to the database -def create_superuser( - username: str, - email: str, - password: str, - github_token: Optional[str] = None, -) -> None: - """ - Create a new superuser and commit it to the database. - - Args: - username (str): Desired username. - email (str): Email address. - password (str): Plaintext password (will be hashed). - github_token (Optional[str]): Optional GitHub token. - - Returns: - User: The created superuser instance. - """ - with Session(engine) as session: - if session.query(User).filter(User.username == username).first(): - raise ValueError(f"Username '{username}' already exists") - if session.query(User).filter(User.email == email).first(): - raise ValueError(f"Email '{email}' already exists") - - hashed_password = bcrypt.hash(password) - - user = User( - username=username, - email=email, - password_hash=hashed_password, - github_token=github_token, - issue_credit=999999, - is_superuser=True, - ) - session.add(user) - session.commit() - session.refresh(user) - _logger.info(f"Superuser '{username}' created successfully.") diff --git a/prometheus/app/dependencies.py b/prometheus/app/dependencies.py index cacb6112..6b5a02bb 100644 --- a/prometheus/app/dependencies.py +++ b/prometheus/app/dependencies.py @@ -1,6 +1,7 @@ """Initializes and configures all prometheus services.""" from prometheus.app.services.base_service import BaseService +from prometheus.app.services.database_service import DatabaseService from prometheus.app.services.issue_service import IssueService from prometheus.app.services.knowledge_graph_service import KnowledgeGraphService from prometheus.app.services.llm_service import LLMService @@ -58,6 +59,7 @@ def initialize_services() -> dict[str, BaseService]: settings.MAX_TOKEN_PER_NEO4J_RESULT, settings.WORKING_DIRECTORY, ) + database_service = DatabaseService(settings.DATABASE_URL) return { "neo4j_service": neo4j_service, @@ -65,4 +67,5 @@ def initialize_services() -> dict[str, BaseService]: "knowledge_graph_service": knowledge_graph_service, "repository_service": repository_service, "issue_service": issue_service, + "database_service": database_service, } diff --git a/prometheus/app/services/database_service.py b/prometheus/app/services/database_service.py new file mode 100644 index 00000000..42b684d6 --- /dev/null +++ b/prometheus/app/services/database_service.py @@ -0,0 +1,22 @@ +import logging + +from sqlmodel import SQLModel, create_engine + +from prometheus.app.services.base_service import BaseService + + +class DatabaseService(BaseService): + def __init__(self, DATABASE_URL: str): + self.engine = create_engine(DATABASE_URL, echo=True) + self._logger = logging.getLogger("prometheus.app.services.database_service") + + # Create the database and tables + def create_db_and_tables(self): + SQLModel.metadata.create_all(self.engine) + + def close(self): + """ + Close the database connection and release any resources. + """ + self.engine.dispose() + self._logger.info("Database connection closed.") From e512457069c4147985b09f049640beea3e9e3ff8 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:05:20 +0800 Subject: [PATCH 20/55] Add logging for Neo4j driver connection closure in Neo4jService --- prometheus/app/services/neo4j_service.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/prometheus/app/services/neo4j_service.py b/prometheus/app/services/neo4j_service.py index bdb1cfd7..da27fea3 100644 --- a/prometheus/app/services/neo4j_service.py +++ b/prometheus/app/services/neo4j_service.py @@ -1,5 +1,7 @@ """Service for managing Neo4j database driver.""" +import logging + from neo4j import GraphDatabase from prometheus.app.services.base_service import BaseService @@ -7,6 +9,7 @@ class Neo4jService(BaseService): def __init__(self, neo4j_uri: str, neo4j_username: str, neo4j_password: str): + self._logger = logging.getLogger("prometheus.app.services.neo4j_service") self.neo4j_driver = GraphDatabase.driver( neo4j_uri, auth=(neo4j_username, neo4j_password), @@ -17,3 +20,4 @@ def __init__(self, neo4j_uri: str, neo4j_username: str, neo4j_password: str): def close(self): self.neo4j_driver.close() + self._logger.info("Neo4j driver connection closed.") From b1f42a673baf4b10262cb09fec1c25c5a424781a Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:05:28 +0800 Subject: [PATCH 21/55] Update user model field constraints for username, email, and github_token --- prometheus/app/entity/user.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/prometheus/app/entity/user.py b/prometheus/app/entity/user.py index 6ed3776c..e6bdf13f 100644 --- a/prometheus/app/entity/user.py +++ b/prometheus/app/entity/user.py @@ -5,15 +5,18 @@ class User(SQLModel, table=True): id: int = Field(primary_key=True, description="User ID") username: str = Field( - index=True, unique=True, max_length=50, description="Username of the user" + index=True, unique=True, max_length=20, description="Username of the user" ) email: str = Field( - index=True, unique=True, max_length=100, description="Email address of the user" + index=True, unique=True, max_length=30, description="Email address of the user" ) password_hash: str = Field(max_length=128, description="Hashed password of the user") github_token: str = Field( - default=None, nullable=True, description="Optional GitHub token for integrations" + default=None, + nullable=True, + description="Optional GitHub token for integrations", + max_length=100, ) issue_credit: int = Field(default=0, ge=0, description="Number of issue credits the user has") From f18839ea3ef9ac51d57f3624270c4a6e3c9dbef4 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:15:17 +0800 Subject: [PATCH 22/55] Add UserService and user model for user management --- prometheus/app/dependencies.py | 3 + prometheus/app/models/requests/user.py | 27 +++++++++ prometheus/app/services/user_service.py | 78 +++++++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 prometheus/app/models/requests/user.py create mode 100644 prometheus/app/services/user_service.py diff --git a/prometheus/app/dependencies.py b/prometheus/app/dependencies.py index 6b5a02bb..c66b7470 100644 --- a/prometheus/app/dependencies.py +++ b/prometheus/app/dependencies.py @@ -7,6 +7,7 @@ from prometheus.app.services.llm_service import LLMService from prometheus.app.services.neo4j_service import Neo4jService from prometheus.app.services.repository_service import RepositoryService +from prometheus.app.services.user_service import UserService from prometheus.configuration.config import settings @@ -60,6 +61,7 @@ def initialize_services() -> dict[str, BaseService]: settings.WORKING_DIRECTORY, ) database_service = DatabaseService(settings.DATABASE_URL) + user_service = UserService(database_service) return { "neo4j_service": neo4j_service, @@ -68,4 +70,5 @@ def initialize_services() -> dict[str, BaseService]: "repository_service": repository_service, "issue_service": issue_service, "database_service": database_service, + "user_service": user_service, } diff --git a/prometheus/app/models/requests/user.py b/prometheus/app/models/requests/user.py new file mode 100644 index 00000000..c3f4dbfd --- /dev/null +++ b/prometheus/app/models/requests/user.py @@ -0,0 +1,27 @@ +import re + +from pydantic import BaseModel, Field, field_validator + + +class CreateUserRequest(BaseModel): + username: str = Field(description="username of the user", max_length=20) + email: str = Field( + description="email of the user", + examples=["your_email@gmail.com"], + max_length=30, + ) + password: str = Field( + description="password of the user", + examples=["P@ssw0rd!"], + min_length=12, + max_length=30, + ) + github_token: str = Field(description="github token of the user", max_length=100) + + @field_validator("email", mode="after") + @classmethod + def validate_email_format(cls, v: str) -> str: + pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$" + if not re.match(pattern, v): + raise ValueError("Invalid email format") + return v diff --git a/prometheus/app/services/user_service.py b/prometheus/app/services/user_service.py new file mode 100644 index 00000000..a4669a74 --- /dev/null +++ b/prometheus/app/services/user_service.py @@ -0,0 +1,78 @@ +import logging +from typing import Optional + +from passlib.hash import bcrypt +from sqlmodel import Session + +from prometheus.app.entity.user import User +from prometheus.app.services.base_service import BaseService +from prometheus.app.services.database_service import DatabaseService + + +class UserService(BaseService): + def __init__(self, database_service: DatabaseService): + self.database_service = database_service + self.engine = database_service.engine + self._logger = logging.getLogger("prometheus.app.services.user_service") + + def create_user( + self, + username: str, + email: str, + password: str, + github_token: Optional[str] = None, + issue_credit: int = 0, + is_superuser: bool = False, + ) -> None: + """ + Create a new user in the database. + + This method is a placeholder and should be implemented with actual user creation logic. + """ + """ + Create a new superuser and commit it to the database. + + Args: + username (str): Desired username. + email (str): Email address. + password (str): Plaintext password (will be hashed). + github_token (Optional[str]): Optional GitHub token. + + Returns: + User: The created superuser instance. + """ + with Session(self.engine) as session: + if session.query(User).filter(User.username == username).first(): + raise ValueError(f"Username '{username}' already exists") + if session.query(User).filter(User.email == email).first(): + raise ValueError(f"Email '{email}' already exists") + + hashed_password = bcrypt.hash(password) + + user = User( + username=username, + email=email, + password_hash=hashed_password, + github_token=github_token, + issue_credit=issue_credit, + is_superuser=is_superuser, + ) + session.add(user) + session.commit() + session.refresh(user) + + # Create a superuser and commit it to the database + def create_superuser( + self, + username: str, + email: str, + password: str, + github_token: Optional[str] = None, + ) -> None: + """ + Create a new superuser in the database. + + This method creates a superuser with the provided credentials and commits it to the database. + """ + self.create_user(username, email, password, github_token, is_superuser=True) + self._logger.info(f"Superuser '{username}' created successfully.") From 971008f73f0d794e4068cedb1bfd36c8dae30e16 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:19:13 +0800 Subject: [PATCH 23/55] Add start method to BaseService and DatabaseService for service initialization --- prometheus/app/main.py | 3 +++ prometheus/app/services/base_service.py | 7 +++++++ prometheus/app/services/database_service.py | 8 ++++++++ 3 files changed, 18 insertions(+) diff --git a/prometheus/app/main.py b/prometheus/app/main.py index 13e72e37..037bbda0 100644 --- a/prometheus/app/main.py +++ b/prometheus/app/main.py @@ -39,6 +39,9 @@ async def lifespan(app: FastAPI): # Initialization on startup app.state.service = dependencies.initialize_services() + logger.info("Starting services...") + for service in app.state.service.values(): + service.start() # Initialization Completed yield # Cleanup on shutdown diff --git a/prometheus/app/services/base_service.py b/prometheus/app/services/base_service.py index 9b55a597..4bb9ff0d 100644 --- a/prometheus/app/services/base_service.py +++ b/prometheus/app/services/base_service.py @@ -3,6 +3,13 @@ class BaseService: Base class for all services in the Prometheus application. """ + def start(self): + """ + Start the service. + This method should be overridden by subclasses to implement specific startup logic. + """ + pass + def close(self): """ Close the service and release any resources. diff --git a/prometheus/app/services/database_service.py b/prometheus/app/services/database_service.py index 42b684d6..ae4cfc7b 100644 --- a/prometheus/app/services/database_service.py +++ b/prometheus/app/services/database_service.py @@ -14,6 +14,14 @@ def __init__(self, DATABASE_URL: str): def create_db_and_tables(self): SQLModel.metadata.create_all(self.engine) + def start(self): + """ + Start the database service by creating the database and tables. + This method is called when the service is initialized. + """ + self.create_db_and_tables() + self._logger.info("Database and tables created successfully.") + def close(self): """ Close the database connection and release any resources. From 0f4117251215f86c7a471ad4aa5da525338beadf Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:22:34 +0800 Subject: [PATCH 24/55] delete create_db_and_tables.py --- prometheus/script/create_db_and_tables.py | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 prometheus/script/create_db_and_tables.py diff --git a/prometheus/script/create_db_and_tables.py b/prometheus/script/create_db_and_tables.py deleted file mode 100644 index 3147c90e..00000000 --- a/prometheus/script/create_db_and_tables.py +++ /dev/null @@ -1,4 +0,0 @@ -from prometheus.app.db import create_db_and_tables - -if __name__ == "__main__": - create_db_and_tables() From b6a66a4a936a001d16e7648e40c4542c4021ad01 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:38:26 +0800 Subject: [PATCH 25/55] Add superuser creation functionality with email validation --- prometheus/app/services/user_service.py | 4 +++- prometheus/script/create_superuser.py | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/prometheus/app/services/user_service.py b/prometheus/app/services/user_service.py index a4669a74..3328e821 100644 --- a/prometheus/app/services/user_service.py +++ b/prometheus/app/services/user_service.py @@ -74,5 +74,7 @@ def create_superuser( This method creates a superuser with the provided credentials and commits it to the database. """ - self.create_user(username, email, password, github_token, is_superuser=True) + self.create_user( + username, email, password, github_token, is_superuser=True, issue_credit=999999 + ) self._logger.info(f"Superuser '{username}' created successfully.") diff --git a/prometheus/script/create_superuser.py b/prometheus/script/create_superuser.py index bf6bf5f9..20829308 100644 --- a/prometheus/script/create_superuser.py +++ b/prometheus/script/create_superuser.py @@ -1,6 +1,12 @@ import argparse +import re -from prometheus.app.db import create_superuser +from prometheus.app.services.database_service import DatabaseService +from prometheus.app.services.user_service import UserService +from prometheus.configuration.config import settings + +database_service: DatabaseService = DatabaseService(settings.DATABASE_URL) +user_service: UserService = UserService(database_service) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Create a superuser account.") @@ -11,7 +17,11 @@ args = parser.parse_args() - create_superuser( + pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$" + if not re.match(pattern, args.email): + raise ValueError("Invalid email format") + + user_service.create_superuser( username=args.username, email=args.email, password=args.password, From 23403939d6b682302fa682d223580a352583dd4f Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:41:11 +0800 Subject: [PATCH 26/55] Refactor user creation method documentation to include issue_credit parameter and clarify superuser creation --- prometheus/app/services/user_service.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/prometheus/app/services/user_service.py b/prometheus/app/services/user_service.py index 3328e821..3412d0c1 100644 --- a/prometheus/app/services/user_service.py +++ b/prometheus/app/services/user_service.py @@ -25,22 +25,18 @@ def create_user( is_superuser: bool = False, ) -> None: """ - Create a new user in the database. + Create a new superuser and commit it to the database. - This method is a placeholder and should be implemented with actual user creation logic. + Args: + username (str): Desired username. + email (str): Email address. + password (str): Plaintext password (will be hashed). + github_token (Optional[str]): Optional GitHub token. + issue_credit (int): Optional issue credit. + is_superuser (bool): Whether the user is a superuser. + Returns: + User: The created superuser instance. """ - """ - Create a new superuser and commit it to the database. - - Args: - username (str): Desired username. - email (str): Email address. - password (str): Plaintext password (will be hashed). - github_token (Optional[str]): Optional GitHub token. - - Returns: - User: The created superuser instance. - """ with Session(self.engine) as session: if session.query(User).filter(User.username == username).first(): raise ValueError(f"Username '{username}' already exists") From 0e662b9a26cb7d25c8d62b2a563cb5de95a0a6a5 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:52:51 +0800 Subject: [PATCH 27/55] Replace bcrypt with argon2 for password hashing in user creation --- prometheus/app/services/user_service.py | 5 +++-- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/prometheus/app/services/user_service.py b/prometheus/app/services/user_service.py index 3412d0c1..29d1d1d4 100644 --- a/prometheus/app/services/user_service.py +++ b/prometheus/app/services/user_service.py @@ -1,7 +1,7 @@ import logging from typing import Optional -from passlib.hash import bcrypt +from argon2 import PasswordHasher from sqlmodel import Session from prometheus.app.entity.user import User @@ -14,6 +14,7 @@ def __init__(self, database_service: DatabaseService): self.database_service = database_service self.engine = database_service.engine self._logger = logging.getLogger("prometheus.app.services.user_service") + self.ph = PasswordHasher() def create_user( self, @@ -43,7 +44,7 @@ def create_user( if session.query(User).filter(User.email == email).first(): raise ValueError(f"Email '{email}' already exists") - hashed_password = bcrypt.hash(password) + hashed_password = self.ph.hash(password) user = User( username=username, diff --git a/pyproject.toml b/pyproject.toml index 43acfb9b..9d02d232 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "dynaconf>=3.2.6", "docker>=7.1.0", "unidiff>=0.7.5", - "passlib[bcrypt]>=1.7.4", + "argon2-cffi>=23.1.0", "sqlmodel==0.0.24", "psycopg2-binary" ] From 4e1a11785f6c48f4809db48faba64fe7a7da162e Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:59:40 +0800 Subject: [PATCH 28/55] Fix import path for FileOperationException and rename exception file --- ...file_operation_exceptions.py => file_operation_exception.py} | 0 prometheus/lang_graph/nodes/context_extraction_node.py | 2 +- prometheus/utils/file_utils.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename prometheus/exceptions/{file_operation_exceptions.py => file_operation_exception.py} (100%) diff --git a/prometheus/exceptions/file_operation_exceptions.py b/prometheus/exceptions/file_operation_exception.py similarity index 100% rename from prometheus/exceptions/file_operation_exceptions.py rename to prometheus/exceptions/file_operation_exception.py diff --git a/prometheus/lang_graph/nodes/context_extraction_node.py b/prometheus/lang_graph/nodes/context_extraction_node.py index e1f8db73..cf331b34 100644 --- a/prometheus/lang_graph/nodes/context_extraction_node.py +++ b/prometheus/lang_graph/nodes/context_extraction_node.py @@ -5,7 +5,7 @@ from langchain_core.messages import SystemMessage from pydantic import BaseModel, Field -from prometheus.exceptions.file_operation_exceptions import FileOperationException +from prometheus.exceptions.file_operation_exception import FileOperationException from prometheus.lang_graph.subgraphs.context_retrieval_state import ContextRetrievalState from prometheus.models.context import Context from prometheus.utils.file_utils import read_file_with_line_numbers diff --git a/prometheus/utils/file_utils.py b/prometheus/utils/file_utils.py index fb3252af..c63f79dd 100644 --- a/prometheus/utils/file_utils.py +++ b/prometheus/utils/file_utils.py @@ -1,7 +1,7 @@ import os from pathlib import Path -from prometheus.exceptions.file_operation_exceptions import FileOperationException +from prometheus.exceptions.file_operation_exception import FileOperationException def read_file_with_line_numbers( From 03b2f4f7bd9834fc131a743b9098f1e213b71cf6 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:02:20 +0800 Subject: [PATCH 29/55] Add JWT configuration settings to config and example.env --- example.env | 3 +++ prometheus/configuration/config.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/example.env b/example.env index 6dd48b14..3283dffb 100644 --- a/example.env +++ b/example.env @@ -35,3 +35,6 @@ PROMETHEUS_MAX_OUTPUT_TOKENS=15000 # Database settings PROMETHEUS_DATABASE_URL=postgresql://postgres:password@localhost:5432/postgres?sslmode=disable + +# JWT settings +PROMETHEUS_JWT_SECRET_KEY=your_jwt_secret_key diff --git a/prometheus/configuration/config.py b/prometheus/configuration/config.py index e43b8484..f9320acb 100644 --- a/prometheus/configuration/config.py +++ b/prometheus/configuration/config.py @@ -48,5 +48,9 @@ class Settings(BaseSettings): # Database DATABASE_URL: str + # JWT Configuration + JWT_SECRET_KEY: str + ACCESS_TOKEN_EXPIRE_TIME: int = 7 # days + settings = Settings() From e14e7e520390ce003fadd45c0aa2c421a806c036 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:02:54 +0800 Subject: [PATCH 30/55] Add JWTException and ServerException classes for error handling --- prometheus/exceptions/jwt_exception.py | 12 ++++++++++++ prometheus/exceptions/server_exception.py | 10 ++++++++++ 2 files changed, 22 insertions(+) create mode 100644 prometheus/exceptions/jwt_exception.py create mode 100644 prometheus/exceptions/server_exception.py diff --git a/prometheus/exceptions/jwt_exception.py b/prometheus/exceptions/jwt_exception.py new file mode 100644 index 00000000..d5589110 --- /dev/null +++ b/prometheus/exceptions/jwt_exception.py @@ -0,0 +1,12 @@ +from prometheus.exceptions.server_exception import ServerException + + +class JWTException(ServerException): + """ + class for JWT exceptions. + This exception is raised when there is an issue with JWT operations, + such as token generation or validation. + """ + + def __init__(self, code: int = 401, message: str = "An error occurred with the JWT operation."): + super().__init__(code, message) diff --git a/prometheus/exceptions/server_exception.py b/prometheus/exceptions/server_exception.py new file mode 100644 index 00000000..5bf81cc7 --- /dev/null +++ b/prometheus/exceptions/server_exception.py @@ -0,0 +1,10 @@ +class ServerException(Exception): + """ + Base class for server exceptions. + This exception is raised when there is an issue with server operations. + """ + + def __init__(self, code: int, message: str): + super().__init__(message) + self.code = code + self.message = message From c5d5a588c398d0c08619f6f60bb535bd447dced3 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:06:35 +0800 Subject: [PATCH 31/55] Add JWT utility class for token generation and validation --- prometheus/utils/jwt_utils.py | 32 ++++++++++++++++++++++++++++++++ pyproject.toml | 3 ++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 prometheus/utils/jwt_utils.py diff --git a/prometheus/utils/jwt_utils.py b/prometheus/utils/jwt_utils.py new file mode 100644 index 00000000..db3971e8 --- /dev/null +++ b/prometheus/utils/jwt_utils.py @@ -0,0 +1,32 @@ +import datetime +import jwt +from prometheus.configuration.config import settings + +from prometheus.exceptions.jwt_exception import JWTException + + +class JWTUtils: + """JWT Utility for token generation and validation""" + + def __init__(self, algorithm='HS256'): + """Initialize JWT utils with configuration""" + self.secret_key = settings.JWT_SECRET_KEY + self.algorithm = algorithm + self.expire_time = int(settings.ACCESS_TOKEN_EXPIRE_TIME) + + def generate_token(self, payload): + """Generate JWT token""" + payload_copy = payload.copy() + payload_copy['exp'] = datetime.datetime.now() + datetime.timedelta(days=self.expire_time) + token = jwt.encode(payload_copy, self.secret_key, algorithm=self.algorithm) + return token + + def decode_token(self, token): + """Decode and parse JWT token""" + try: + decoded = jwt.decode(token, self.secret_key, algorithms=[self.algorithm]) + return decoded + except jwt.ExpiredSignatureError: + raise JWTException(message="Token expired") + except jwt.InvalidTokenError: + raise JWTException(message="Invalid token") diff --git a/pyproject.toml b/pyproject.toml index 9d02d232..1d163fcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,8 @@ dependencies = [ "unidiff>=0.7.5", "argon2-cffi>=23.1.0", "sqlmodel==0.0.24", - "psycopg2-binary" + "psycopg2-binary", + "pyjwt==2.6.0", ] requires-python = ">= 3.11" From 0ceb27d55a063b04a9a031864570572d48265702 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:13:12 +0800 Subject: [PATCH 32/55] Add script to generate secure JWT secret token and update README --- README.md | 10 ++++++++++ prometheus/script/generate_jwt_token.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 prometheus/script/generate_jwt_token.py diff --git a/README.md b/README.md index f2ccaba8..780984ac 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,20 @@ governed by a state machine to ensure code quality through automated reviews, bu ``` 2. #### Copy the `example.env` file to `.env` and update it with your API keys and other required configurations: + ```bash mv example.env .env ``` + > You need to provide a secure `JWT_SECRET_KEY` in the `.env` file. + > You can generate a strong key by running the following command: + + ```bash + python -m prometheus.script.generate_jwt_token + ``` + + This will print a secure token you can copy and paste into your `.env` file + 3. #### Create the working directory to store logs and cloned repositories: ```bash diff --git a/prometheus/script/generate_jwt_token.py b/prometheus/script/generate_jwt_token.py new file mode 100644 index 00000000..5e500c8f --- /dev/null +++ b/prometheus/script/generate_jwt_token.py @@ -0,0 +1,15 @@ +import base64 +import secrets + + +def generate_jwt_secret_token(length: int = 64) -> str: + """ + Generate a secure JWT secret token. + """ + token_bytes = secrets.token_bytes(length) + return base64.urlsafe_b64encode(token_bytes).decode("utf-8") + + +if __name__ == "__main__": + secret = generate_jwt_secret_token() + print(f"JWT_SECRET_TOKEN={secret}") From b8214ae17e85fa19b6ada832e7fcd285a4445e9e Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:13:27 +0800 Subject: [PATCH 33/55] Refactor JWTUtils to use double quotes for string literals and improve code formatting --- prometheus/utils/jwt_utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/prometheus/utils/jwt_utils.py b/prometheus/utils/jwt_utils.py index db3971e8..684652f5 100644 --- a/prometheus/utils/jwt_utils.py +++ b/prometheus/utils/jwt_utils.py @@ -1,14 +1,15 @@ import datetime + import jwt -from prometheus.configuration.config import settings +from prometheus.configuration.config import settings from prometheus.exceptions.jwt_exception import JWTException class JWTUtils: """JWT Utility for token generation and validation""" - def __init__(self, algorithm='HS256'): + def __init__(self, algorithm="HS256"): """Initialize JWT utils with configuration""" self.secret_key = settings.JWT_SECRET_KEY self.algorithm = algorithm @@ -17,7 +18,7 @@ def __init__(self, algorithm='HS256'): def generate_token(self, payload): """Generate JWT token""" payload_copy = payload.copy() - payload_copy['exp'] = datetime.datetime.now() + datetime.timedelta(days=self.expire_time) + payload_copy["exp"] = datetime.datetime.now() + datetime.timedelta(days=self.expire_time) token = jwt.encode(payload_copy, self.secret_key, algorithm=self.algorithm) return token From 77f3418f0595d514e8e019273cc844d5eb5e955c Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:28:08 +0800 Subject: [PATCH 34/55] Add authentication module with login request and response models --- prometheus/app/api/auth.py | 29 +++++++++++++++++++++++ prometheus/app/models/requests/auth.py | 31 +++++++++++++++++++++++++ prometheus/app/models/requests/user.py | 3 +-- prometheus/app/models/response/auth.py | 9 +++++++ prometheus/app/services/user_service.py | 28 ++++++++++++++++++++++ 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 prometheus/app/api/auth.py create mode 100644 prometheus/app/models/requests/auth.py create mode 100644 prometheus/app/models/response/auth.py diff --git a/prometheus/app/api/auth.py b/prometheus/app/api/auth.py new file mode 100644 index 00000000..6feb6e17 --- /dev/null +++ b/prometheus/app/api/auth.py @@ -0,0 +1,29 @@ +from fastapi import APIRouter, Request + +from prometheus.app.models.requests.auth import LoginRequest +from prometheus.app.models.response.auth import LoginResponse +from prometheus.app.services.user_service import UserService + +router = APIRouter() + + +@router.post( + "/login/", + summary="Login to the system", + description="Login to the system using username, email, and password. Returns an access token.", + response_description="Returns an access token for authenticated requests", + response_model=LoginResponse, +) +def login(login_request: LoginRequest, request: Request) -> LoginResponse: + """ + Login to the system using username, email, and password. + Returns an access token for authenticated requests. + """ + + user_service: UserService = request.app.state.service["user_service"] + access_token = user_service.login( + username=login_request.username, + email=login_request.email, + password=login_request.password, + ) + return LoginResponse(access_token=access_token) diff --git a/prometheus/app/models/requests/auth.py b/prometheus/app/models/requests/auth.py new file mode 100644 index 00000000..96ef3fea --- /dev/null +++ b/prometheus/app/models/requests/auth.py @@ -0,0 +1,31 @@ +import re + +from pydantic import BaseModel, Field, field_validator, model_validator + + +class LoginRequest(BaseModel): + username: str = Field(description="username of the user", max_length=20) + email: str = Field( + description="email of the user", + examples=["your_email@gmail.com"], + max_length=30, + ) + password: str = Field( + description="password of the user", + examples=["P@ssw0rd!"], + min_length=12, + max_length=30, + ) + + @field_validator("email", mode="after") + def validate_email_format(self, v: str) -> str: + pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$" + if not re.match(pattern, v): + raise ValueError("Invalid email format") + return v + + @model_validator(mode="after") + def check_username_or_email(self) -> "LoginRequest": + if not self.username and not self.email: + raise ValueError("At least one of 'username' or 'email' must be provided.") + return self diff --git a/prometheus/app/models/requests/user.py b/prometheus/app/models/requests/user.py index c3f4dbfd..6a0e3144 100644 --- a/prometheus/app/models/requests/user.py +++ b/prometheus/app/models/requests/user.py @@ -19,8 +19,7 @@ class CreateUserRequest(BaseModel): github_token: str = Field(description="github token of the user", max_length=100) @field_validator("email", mode="after") - @classmethod - def validate_email_format(cls, v: str) -> str: + def validate_email_format(self, v: str) -> str: pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$" if not re.match(pattern, v): raise ValueError("Invalid email format") diff --git a/prometheus/app/models/response/auth.py b/prometheus/app/models/response/auth.py new file mode 100644 index 00000000..56efc9f6 --- /dev/null +++ b/prometheus/app/models/response/auth.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +class LoginResponse(BaseModel): + """ + Response model for user login. + """ + + access_token: str diff --git a/prometheus/app/services/user_service.py b/prometheus/app/services/user_service.py index 29d1d1d4..e865c943 100644 --- a/prometheus/app/services/user_service.py +++ b/prometheus/app/services/user_service.py @@ -7,6 +7,7 @@ from prometheus.app.entity.user import User from prometheus.app.services.base_service import BaseService from prometheus.app.services.database_service import DatabaseService +from prometheus.utils.jwt_utils import JWTUtils class UserService(BaseService): @@ -15,6 +16,7 @@ def __init__(self, database_service: DatabaseService): self.engine = database_service.engine self._logger = logging.getLogger("prometheus.app.services.user_service") self.ph = PasswordHasher() + self.jwt_utils = JWTUtils() def create_user( self, @@ -58,6 +60,32 @@ def create_user( session.commit() session.refresh(user) + def login(self, username: str, email: str, password: str) -> str: + """ + Log in a user by verifying their credentials and return an access token. + + Args: + username (str): Username of the user. + email (str): Email address of the user. + password (str): Plaintext password. + """ + with Session(self.engine) as session: + user = ( + session.query(User) + .filter((User.username == username) | (User.email == email)) + .first() + ) + + if not user: + raise ValueError("Invalid username or email") + + if not self.ph.verify(user.password_hash, password): + raise ValueError("Invalid password") + + # Generate and return a JWT token for the user + token = self.jwt_utils.generate_token({"user_id": user.id}) + return token + # Create a superuser and commit it to the database def create_superuser( self, From cbd70829844f3c26d41bd9a7d3300b777cf24f19 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:38:17 +0800 Subject: [PATCH 35/55] Add IssueResponse model and update answer_issue function to return structured response --- prometheus/app/api/issue.py | 20 +++++++++++--------- prometheus/app/models/response/issue.py | 10 ++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 prometheus/app/models/response/issue.py diff --git a/prometheus/app/api/issue.py b/prometheus/app/api/issue.py index 10bd5753..e155b564 100644 --- a/prometheus/app/api/issue.py +++ b/prometheus/app/api/issue.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, HTTPException, Request from prometheus.app.models.requests.issue import IssueRequest +from prometheus.app.models.response.issue import IssueResponse router = APIRouter() @@ -10,8 +11,9 @@ summary="Process and generate a response for an issue", description="Analyzes an issue, generates patches if needed, runs optional builds and tests, and can push changes to a remote branch.", response_description="Returns the patch, test results, and issue response", + response_model=IssueResponse, ) -def answer_issue(issue: IssueRequest, request: Request): +def answer_issue(issue: IssueRequest, request: Request) -> IssueResponse: if not request.app.state.service["knowledge_graph_service"].exists(): raise HTTPException( status_code=404, @@ -48,11 +50,11 @@ def answer_issue(issue: IssueRequest, request: Request): test_commands=issue.test_commands, push_to_remote=issue.push_to_remote, ) - return { - "patch": patch, - "passed_reproducing_test": passed_reproducing_test, - "passed_build": passed_build, - "passed_existing_test": passed_existing_test, - "issue_response": issue_response, - "remote_branch_name": remote_branch_name, - } + return IssueResponse( + patch=patch, + passed_reproducing_test=passed_reproducing_test, + passed_build=passed_build, + passed_existing_test=passed_existing_test, + issue_response=issue_response, + remote_branch_name=remote_branch_name, + ) diff --git a/prometheus/app/models/response/issue.py b/prometheus/app/models/response/issue.py new file mode 100644 index 00000000..d06f46a7 --- /dev/null +++ b/prometheus/app/models/response/issue.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class IssueResponse(BaseModel): + patch: str + passed_reproducing_test: bool + passed_build: bool + passed_existing_test: bool + issue_response: str + remote_branch_name: str | None = None From b36286ce3f03189a5fa1471a2ff321544c9fa13a Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:48:46 +0800 Subject: [PATCH 36/55] Refactor upload_github_repository functions to raise ServerException on clone errors --- prometheus/app/api/repository.py | 55 +++++++++++++++----------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/prometheus/app/api/repository.py b/prometheus/app/api/repository.py index cb177555..78a717f7 100644 --- a/prometheus/app/api/repository.py +++ b/prometheus/app/api/repository.py @@ -1,8 +1,9 @@ import git -from fastapi import APIRouter, HTTPException, Request +from fastapi import APIRouter, Request from prometheus.app.services.knowledge_graph_service import KnowledgeGraphService from prometheus.app.services.repository_service import RepositoryService +from prometheus.exceptions.server_exception import ServerException router = APIRouter() @@ -14,23 +15,22 @@ """, ) def upload_github_repository(github_token: str, https_url: str, request: Request): - try: - # Get the repository and knowledge graph services - repository_service: RepositoryService = request.app.state.service["repository_service"] - knowledge_graph_service: KnowledgeGraphService = request.app.state.service[ - "knowledge_graph_service" - ] - - # Clean the services to ensure no previous data is present - repository_service.clean() - knowledge_graph_service.clear() + # Get the repository and knowledge graph services + repository_service: RepositoryService = request.app.state.service["repository_service"] + knowledge_graph_service: KnowledgeGraphService = request.app.state.service[ + "knowledge_graph_service" + ] + # Clean the services to ensure no previous data is present + repository_service.clean() + knowledge_graph_service.clear() + try: # Clone the repository saved_path = repository_service.clone_github_repo(github_token, https_url) - # Build and save the knowledge graph from the cloned repository - knowledge_graph_service.build_and_save_knowledge_graph(saved_path, https_url) except git.exc.GitCommandError: - raise HTTPException(status_code=400, detail=f"Unable to clone {https_url}") + raise ServerException(code=400, message=f"Unable to clone {https_url}") + # Build and save the knowledge graph from the cloned repository + knowledge_graph_service.build_and_save_knowledge_graph(saved_path, https_url) @router.get( @@ -42,26 +42,23 @@ def upload_github_repository(github_token: str, https_url: str, request: Request def upload_github_repository_at_commit( github_token, https_url: str, commit_id: str, request: Request ): - try: - # Get the repository and knowledge graph services - repository_service: RepositoryService = request.app.state.service["repository_service"] - knowledge_graph_service: KnowledgeGraphService = request.app.state.service[ - "knowledge_graph_service" - ] + # Get the repository and knowledge graph services + repository_service: RepositoryService = request.app.state.service["repository_service"] + knowledge_graph_service: KnowledgeGraphService = request.app.state.service[ + "knowledge_graph_service" + ] - # Clean the services to ensure no previous data is present - repository_service.clean() - knowledge_graph_service.clear() + # Clean the services to ensure no previous data is present + repository_service.clean() + knowledge_graph_service.clear() + try: # Clone the repository saved_path = repository_service.clone_github_repo(github_token, https_url, commit_id) - - # Build and save the knowledge graph from the cloned repository - knowledge_graph_service.build_and_save_knowledge_graph(saved_path, https_url, commit_id) except git.exc.GitCommandError: - raise HTTPException( - status_code=400, detail=f"Unable to clone {https_url} with commit {commit_id}" - ) + raise ServerException(code=400, message=f"Unable to clone {https_url}") + # Build and save the knowledge graph from the cloned repository + knowledge_graph_service.build_and_save_knowledge_graph(saved_path, https_url, commit_id) @router.get( From 8f87edaeb0ae8ebecf2b995f81d6e06623bf0078 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:50:13 +0800 Subject: [PATCH 37/55] Add global exception handlers for FastAPI application --- prometheus/app/exception_handler.py | 26 ++++++++++++++++++++++++++ prometheus/app/main.py | 4 ++++ 2 files changed, 30 insertions(+) create mode 100644 prometheus/app/exception_handler.py diff --git a/prometheus/app/exception_handler.py b/prometheus/app/exception_handler.py new file mode 100644 index 00000000..2a9a56f0 --- /dev/null +++ b/prometheus/app/exception_handler.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse + +from prometheus.exceptions.server_exception import ServerException + + +def register_exception_handlers(app: FastAPI): + """Global exception handlers for the FastAPI application.""" + + @app.exception_handler(ServerException) + async def custom_exception_handler(_request: Request, exc: ServerException): + """ + Custom exception handler for ServerException. + """ + return JSONResponse( + status_code=exc.code, content={"code": exc.code, "message": exc.message, "data": None} + ) + + @app.exception_handler(Exception) + async def global_exception_handler(_request: Request, _exc: Exception): + """ + Global exception handler for all uncaught exceptions. + """ + return JSONResponse( + status_code=500, content={"code": 500, "message": "Internal Server Error", "data": None} + ) diff --git a/prometheus/app/main.py b/prometheus/app/main.py index 037bbda0..fe7bc600 100644 --- a/prometheus/app/main.py +++ b/prometheus/app/main.py @@ -7,6 +7,7 @@ from prometheus.app import dependencies from prometheus.app.api import issue, repository +from prometheus.app.exception_handler import register_exception_handlers from prometheus.configuration.config import settings # Create a logger for the application's namespace @@ -66,6 +67,9 @@ def custom_generate_unique_id(route: APIRoute) -> str: debug=True if settings.ENVIRONMENT == "local" else False, ) +# Register the exception handlers +register_exception_handlers(app) + app.include_router(repository.router, prefix="/repository", tags=["repository"]) app.include_router(issue.router, prefix="/issue", tags=["issue"]) From 2151208ff2355990825176672cf99415dbab02e0 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:00:20 +0800 Subject: [PATCH 38/55] add unified return json format --- prometheus/app/main.py | 4 +++ prometheus/app/middlewares/__init__.py | 0 .../response_wrapper_middleware.py | 25 +++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 prometheus/app/middlewares/__init__.py create mode 100644 prometheus/app/middlewares/response_wrapper_middleware.py diff --git a/prometheus/app/main.py b/prometheus/app/main.py index fe7bc600..b04db85b 100644 --- a/prometheus/app/main.py +++ b/prometheus/app/main.py @@ -8,6 +8,7 @@ from prometheus.app import dependencies from prometheus.app.api import issue, repository from prometheus.app.exception_handler import register_exception_handlers +from prometheus.app.middlewares.response_wrapper_middleware import ResponseWrapperMiddleware from prometheus.configuration.config import settings # Create a logger for the application's namespace @@ -67,6 +68,9 @@ def custom_generate_unique_id(route: APIRoute) -> str: debug=True if settings.ENVIRONMENT == "local" else False, ) +# Register middlewares +app.add_middleware(ResponseWrapperMiddleware) + # Register the exception handlers register_exception_handlers(app) diff --git a/prometheus/app/middlewares/__init__.py b/prometheus/app/middlewares/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prometheus/app/middlewares/response_wrapper_middleware.py b/prometheus/app/middlewares/response_wrapper_middleware.py new file mode 100644 index 00000000..fea9b792 --- /dev/null +++ b/prometheus/app/middlewares/response_wrapper_middleware.py @@ -0,0 +1,25 @@ +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware +from fastapi import Request +import json + + +class ResponseWrapperMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + response = await call_next(request) + # Check if the response is a JSON response + if response.headers.get("content-type") == "application/json": + # Decode the JSON response body + original_data = json.loads(response.body.decode("utf-8")) + + # Check if the original data is already in the expected format + if isinstance(original_data, dict) and set(original_data.keys()) == {"code", "message", "data"}: + new_data = original_data + else: + new_data = { + "code": response.status_code, + "message": "Success", + "data": original_data, + } + return JSONResponse(content=new_data, status_code=response.status_code) + return response From 60e5787f5f80f8e707580377826ecee954b0fbee Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:14:38 +0800 Subject: [PATCH 39/55] Refactor authentication and repository endpoints to use a unified response model --- prometheus/app/api/auth.py | 7 +++--- prometheus/app/api/repository.py | 16 ++++++++---- prometheus/app/main.py | 4 --- .../response_wrapper_middleware.py | 25 ------------------- prometheus/app/models/response/response.py | 14 +++++++++++ 5 files changed, 29 insertions(+), 37 deletions(-) delete mode 100644 prometheus/app/middlewares/response_wrapper_middleware.py create mode 100644 prometheus/app/models/response/response.py diff --git a/prometheus/app/api/auth.py b/prometheus/app/api/auth.py index 6feb6e17..cb55a352 100644 --- a/prometheus/app/api/auth.py +++ b/prometheus/app/api/auth.py @@ -2,6 +2,7 @@ from prometheus.app.models.requests.auth import LoginRequest from prometheus.app.models.response.auth import LoginResponse +from prometheus.app.models.response.response import Response from prometheus.app.services.user_service import UserService router = APIRouter() @@ -12,9 +13,9 @@ summary="Login to the system", description="Login to the system using username, email, and password. Returns an access token.", response_description="Returns an access token for authenticated requests", - response_model=LoginResponse, + response_model=Response[LoginResponse], ) -def login(login_request: LoginRequest, request: Request) -> LoginResponse: +def login(login_request: LoginRequest, request: Request) -> Response[LoginResponse]: """ Login to the system using username, email, and password. Returns an access token for authenticated requests. @@ -26,4 +27,4 @@ def login(login_request: LoginRequest, request: Request) -> LoginResponse: email=login_request.email, password=login_request.password, ) - return LoginResponse(access_token=access_token) + return Response(data=LoginResponse(access_token=access_token)) diff --git a/prometheus/app/api/repository.py b/prometheus/app/api/repository.py index 78a717f7..49784a32 100644 --- a/prometheus/app/api/repository.py +++ b/prometheus/app/api/repository.py @@ -1,6 +1,7 @@ import git from fastapi import APIRouter, Request +from prometheus.app.models.response.response import Response from prometheus.app.services.knowledge_graph_service import KnowledgeGraphService from prometheus.app.services.repository_service import RepositoryService from prometheus.exceptions.server_exception import ServerException @@ -13,6 +14,7 @@ description=""" Upload a GitHub repository to Prometheus, default to the latest commit in the main branch. """, + response_model=Response, ) def upload_github_repository(github_token: str, https_url: str, request: Request): # Get the repository and knowledge graph services @@ -31,6 +33,7 @@ def upload_github_repository(github_token: str, https_url: str, request: Request raise ServerException(code=400, message=f"Unable to clone {https_url}") # Build and save the knowledge graph from the cloned repository knowledge_graph_service.build_and_save_knowledge_graph(saved_path, https_url) + return Response() @router.get( @@ -38,6 +41,7 @@ def upload_github_repository(github_token: str, https_url: str, request: Request description=""" Upload a GitHub repository at a specific commit to Prometheus. """, + response_model=Response, ) def upload_github_repository_at_commit( github_token, https_url: str, commit_id: str, request: Request @@ -59,6 +63,7 @@ def upload_github_repository_at_commit( raise ServerException(code=400, message=f"Unable to clone {https_url}") # Build and save the knowledge graph from the cloned repository knowledge_graph_service.build_and_save_knowledge_graph(saved_path, https_url, commit_id) + return Response() @router.get( @@ -66,20 +71,21 @@ def upload_github_repository_at_commit( description=""" Delete the repository uploaded to Prometheus, along with other information. """, + response_model=Response, ) def delete(request: Request): knowledge_graph_service: KnowledgeGraphService = request.app.state.service[ "knowledge_graph_service" ] if not knowledge_graph_service.exists(): - return {"message": "No knowledge graph to delete"} + return Response(message="No knowledge graph to delete") # Get the repository service to clean up the repository data repository_service: RepositoryService = request.app.state.service["repository_service"] # Clear the knowledge graph and repository data knowledge_graph_service.clear() repository_service.clean() - return {"message": "Successfully deleted knowledge graph"} + return Response() @router.get( @@ -87,7 +93,7 @@ def delete(request: Request): description=""" If there is a codebase uploaded to Promtheus. """, - response_model=bool, + response_model=Response[bool], ) -def knowledge_graph_exists(request: Request) -> bool: - return request.app.state.service["knowledge_graph_service"].exists() +def knowledge_graph_exists(request: Request) -> Response[bool]: + return Response(data=request.app.state.service["knowledge_graph_service"].exists()) diff --git a/prometheus/app/main.py b/prometheus/app/main.py index b04db85b..fe7bc600 100644 --- a/prometheus/app/main.py +++ b/prometheus/app/main.py @@ -8,7 +8,6 @@ from prometheus.app import dependencies from prometheus.app.api import issue, repository from prometheus.app.exception_handler import register_exception_handlers -from prometheus.app.middlewares.response_wrapper_middleware import ResponseWrapperMiddleware from prometheus.configuration.config import settings # Create a logger for the application's namespace @@ -68,9 +67,6 @@ def custom_generate_unique_id(route: APIRoute) -> str: debug=True if settings.ENVIRONMENT == "local" else False, ) -# Register middlewares -app.add_middleware(ResponseWrapperMiddleware) - # Register the exception handlers register_exception_handlers(app) diff --git a/prometheus/app/middlewares/response_wrapper_middleware.py b/prometheus/app/middlewares/response_wrapper_middleware.py deleted file mode 100644 index fea9b792..00000000 --- a/prometheus/app/middlewares/response_wrapper_middleware.py +++ /dev/null @@ -1,25 +0,0 @@ -from fastapi.responses import JSONResponse -from starlette.middleware.base import BaseHTTPMiddleware -from fastapi import Request -import json - - -class ResponseWrapperMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request: Request, call_next): - response = await call_next(request) - # Check if the response is a JSON response - if response.headers.get("content-type") == "application/json": - # Decode the JSON response body - original_data = json.loads(response.body.decode("utf-8")) - - # Check if the original data is already in the expected format - if isinstance(original_data, dict) and set(original_data.keys()) == {"code", "message", "data"}: - new_data = original_data - else: - new_data = { - "code": response.status_code, - "message": "Success", - "data": original_data, - } - return JSONResponse(content=new_data, status_code=response.status_code) - return response diff --git a/prometheus/app/models/response/response.py b/prometheus/app/models/response/response.py new file mode 100644 index 00000000..11a53180 --- /dev/null +++ b/prometheus/app/models/response/response.py @@ -0,0 +1,14 @@ +from typing import TypeVar, Generic + +from pydantic import BaseModel + +T = TypeVar('T') + + +class Response(BaseModel, Generic[T]): + """ + Generic response model for API responses. + """ + code: int = 200 + message: str = "Success" + data: T | None = None From c4b8c38bfea7cb0a997c7a9d4c11809e787eff9a Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:20:50 +0800 Subject: [PATCH 40/55] Refactor answer_issue function to use a generic Response model and raise ServerException for error handling --- prometheus/app/api/issue.py | 39 ++++++++++++---------- prometheus/app/models/response/response.py | 5 +-- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/prometheus/app/api/issue.py b/prometheus/app/api/issue.py index e155b564..803ebb90 100644 --- a/prometheus/app/api/issue.py +++ b/prometheus/app/api/issue.py @@ -1,7 +1,9 @@ -from fastapi import APIRouter, HTTPException, Request +from fastapi import APIRouter, Request from prometheus.app.models.requests.issue import IssueRequest from prometheus.app.models.response.issue import IssueResponse +from prometheus.app.models.response.response import Response +from prometheus.exceptions.server_exception import ServerException router = APIRouter() @@ -9,22 +11,23 @@ @router.post( "/answer/", summary="Process and generate a response for an issue", - description="Analyzes an issue, generates patches if needed, runs optional builds and tests, and can push changes to a remote branch.", + description="Analyzes an issue, generates patches if needed, runs optional builds and tests, and can push changes " + "to a remote branch.", response_description="Returns the patch, test results, and issue response", - response_model=IssueResponse, + response_model=Response[IssueResponse], ) -def answer_issue(issue: IssueRequest, request: Request) -> IssueResponse: +def answer_issue(issue: IssueRequest, request: Request) -> Response[IssueResponse]: if not request.app.state.service["knowledge_graph_service"].exists(): - raise HTTPException( - status_code=404, - detail="A repository is not uploaded, use /repository/ endpoint to upload one", + raise ServerException( + code=404, + message="A repository is not uploaded, use /repository/ endpoint to upload one", ) if issue.dockerfile_content or issue.image_name: if issue.workdir is None: - raise HTTPException( - status_code=400, - detail="workdir must be provided for user defined environment", + raise ServerException( + code=400, + message="workdir must be provided for user defined environment", ) ( @@ -50,11 +53,13 @@ def answer_issue(issue: IssueRequest, request: Request) -> IssueResponse: test_commands=issue.test_commands, push_to_remote=issue.push_to_remote, ) - return IssueResponse( - patch=patch, - passed_reproducing_test=passed_reproducing_test, - passed_build=passed_build, - passed_existing_test=passed_existing_test, - issue_response=issue_response, - remote_branch_name=remote_branch_name, + return Response( + data=IssueResponse( + patch=patch, + passed_reproducing_test=passed_reproducing_test, + passed_build=passed_build, + passed_existing_test=passed_existing_test, + issue_response=issue_response, + remote_branch_name=remote_branch_name, + ) ) diff --git a/prometheus/app/models/response/response.py b/prometheus/app/models/response/response.py index 11a53180..a483c2a2 100644 --- a/prometheus/app/models/response/response.py +++ b/prometheus/app/models/response/response.py @@ -1,14 +1,15 @@ -from typing import TypeVar, Generic +from typing import Generic, TypeVar from pydantic import BaseModel -T = TypeVar('T') +T = TypeVar("T") class Response(BaseModel, Generic[T]): """ Generic response model for API responses. """ + code: int = 200 message: str = "Success" data: T | None = None From 0b09e9d1bfffdae9dac9044d1967c83d1115de71 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 20:54:43 +0800 Subject: [PATCH 41/55] Update test fixtures to use a consistent working directory path --- tests/app/services/test_issue_service.py | 1 + tests/app/services/test_repository_service.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/app/services/test_issue_service.py b/tests/app/services/test_issue_service.py index 861b608f..003bb978 100644 --- a/tests/app/services/test_issue_service.py +++ b/tests/app/services/test_issue_service.py @@ -49,6 +49,7 @@ def issue_service(mock_kg_service, mock_repository_service, mock_neo4j_service, mock_neo4j_service, mock_llm_service, max_token_per_neo4j_result=1000, + working_directory="/test/working/dir", ) diff --git a/tests/app/services/test_repository_service.py b/tests/app/services/test_repository_service.py index 99d1af27..6ce2ac65 100644 --- a/tests/app/services/test_repository_service.py +++ b/tests/app/services/test_repository_service.py @@ -32,7 +32,7 @@ def mock_knowledge_graph(): @pytest.fixture def service(mock_kg_service): - working_dir = Path("/test/working/dir") + working_dir = "/test/working/dir" # Don't mock GitRepository in the fixture anymore with patch("pathlib.Path.mkdir"): return RepositoryService( From 6179b2793380291847f1740122748dfaf5a78c75 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 20:55:38 +0800 Subject: [PATCH 42/55] Remove unused test functions for local repository uploads --- tests/app/api/test_repository.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/app/api/test_repository.py b/tests/app/api/test_repository.py index 3f983899..f4facc09 100644 --- a/tests/app/api/test_repository.py +++ b/tests/app/api/test_repository.py @@ -5,7 +5,6 @@ from fastapi.testclient import TestClient from prometheus.app.api import repository -from tests.test_utils import test_project_paths app = FastAPI() app.include_router(repository.router, prefix="/repository", tags=["repository"]) @@ -19,23 +18,6 @@ def mock_service_coordinator(): yield service_coordinator -def test_upload_local_repository(mock_service_coordinator): - mock_service_coordinator.upload_local_repository.return_value = None - response = client.get( - "/repository/local", - params={"local_repository": test_project_paths.TEST_PROJECT_PATH.absolute().as_posix()}, - ) - assert response.status_code == 200 - - -def test_upload_fake_local_repository(): - response = client.get( - "/repository/local/", - params={"local_repository": "/foo/bar/"}, - ) - assert response.status_code == 404 - - def test_delete(mock_service_coordinator): mock_service_coordinator.exists_knowledge_graph.return_value = True mock_service_coordinator.clear.return_value = None From 0d79e8c52b42ad406719e3378feec52bfcd0cb98 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 21:49:28 +0800 Subject: [PATCH 43/55] Normalize success message to lowercase in response model --- prometheus/app/models/response/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prometheus/app/models/response/response.py b/prometheus/app/models/response/response.py index a483c2a2..09c7e595 100644 --- a/prometheus/app/models/response/response.py +++ b/prometheus/app/models/response/response.py @@ -11,5 +11,5 @@ class Response(BaseModel, Generic[T]): """ code: int = 200 - message: str = "Success" + message: str = "success" data: T | None = None From 9351c635dcb79f3e600662bff687ccfc674a8e89 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:00:21 +0800 Subject: [PATCH 44/55] Refactor test files to use a unified mock service structure and register exception handlers --- tests/app/api/test_issue.py | 48 ++++---- tests/app/api/test_repository.py | 16 +-- tests/app/services/test_db_engine.py | 12 -- .../app/services/test_service_coordinator.py | 115 ------------------ tests/app/test_db.py | 49 -------- tests/app/test_main.py | 12 +- 6 files changed, 42 insertions(+), 210 deletions(-) delete mode 100644 tests/app/services/test_db_engine.py delete mode 100644 tests/app/services/test_service_coordinator.py delete mode 100644 tests/app/test_db.py diff --git a/tests/app/api/test_issue.py b/tests/app/api/test_issue.py index ae5e7c1a..475b4607 100644 --- a/tests/app/api/test_issue.py +++ b/tests/app/api/test_issue.py @@ -5,23 +5,25 @@ from fastapi.testclient import TestClient from prometheus.app.api import issue +from prometheus.app.exception_handler import register_exception_handlers from prometheus.lang_graph.graphs.issue_state import IssueType app = FastAPI() +register_exception_handlers(app) app.include_router(issue.router, prefix="/issue", tags=["issue"]) client = TestClient(app) @pytest.fixture -def mock_service_coordinator(): - service_coordinator = mock.MagicMock() - app.state.service_coordinator = service_coordinator - yield service_coordinator +def mock_service(): + service = mock.MagicMock() + app.state.service = service + yield service -def test_answer_issue(mock_service_coordinator): - mock_service_coordinator.exists_knowledge_graph.return_value = True - mock_service_coordinator.answer_issue.return_value = ( +def test_answer_issue(mock_service): + mock_service["knowledge_graph_service"].exists_knowledge_graph.return_value = True + mock_service["issue_service"].answer_issue.return_value = ( "feature/fix-42", # remote_branch_name "test patch", # patch True, # passed_reproducing_test @@ -41,17 +43,21 @@ def test_answer_issue(mock_service_coordinator): assert response.status_code == 200 assert response.json() == { - "remote_branch_name": "feature/fix-42", - "patch": "test patch", - "passed_reproducing_test": True, - "passed_build": True, - "passed_existing_test": True, - "issue_response": "Issue fixed", + "code": 200, + "message": "success", + "data": { + "remote_branch_name": "feature/fix-42", + "patch": "test patch", + "passed_reproducing_test": True, + "passed_build": True, + "passed_existing_test": True, + "issue_response": "Issue fixed", + } } -def test_answer_issue_no_repository(mock_service_coordinator): - mock_service_coordinator.exists_knowledge_graph.return_value = False +def test_answer_issue_no_repository(mock_service): + mock_service["knowledge_graph_service"].exists.return_value = False response = client.post( "/issue/answer", @@ -61,8 +67,8 @@ def test_answer_issue_no_repository(mock_service_coordinator): assert response.status_code == 404 -def test_answer_issue_invalid_container_config(mock_service_coordinator): - mock_service_coordinator.exists_knowledge_graph.return_value = True +def test_answer_issue_invalid_container_config(mock_service): + mock_service["knowledge_graph_service"].exists.return_value = True response = client.post( "/issue/answer", @@ -78,9 +84,9 @@ def test_answer_issue_invalid_container_config(mock_service_coordinator): assert response.status_code == 400 -def test_answer_issue_with_container(mock_service_coordinator): - mock_service_coordinator.exists_knowledge_graph.return_value = True - mock_service_coordinator.answer_issue.return_value = ( +def test_answer_issue_with_container(mock_service): + mock_service["knowledge_graph_service"].exists.return_value = True + mock_service["issue_service"].answer_issue.return_value = ( "feature/fix-42", "test patch", True, @@ -102,7 +108,7 @@ def test_answer_issue_with_container(mock_service_coordinator): response = client.post("/issue/answer", json=test_payload) assert response.status_code == 200 - mock_service_coordinator.answer_issue.assert_called_once_with( + mock_service["issue_service"].answer_issue.assert_called_once_with( issue_number=42, issue_title="Test Issue", issue_body="Test description", diff --git a/tests/app/api/test_repository.py b/tests/app/api/test_repository.py index f4facc09..73f5bcf4 100644 --- a/tests/app/api/test_repository.py +++ b/tests/app/api/test_repository.py @@ -5,21 +5,23 @@ from fastapi.testclient import TestClient from prometheus.app.api import repository +from prometheus.app.exception_handler import register_exception_handlers app = FastAPI() +register_exception_handlers(app) app.include_router(repository.router, prefix="/repository", tags=["repository"]) client = TestClient(app) @pytest.fixture -def mock_service_coordinator(): - service_coordinator = mock.MagicMock() - app.state.service_coordinator = service_coordinator - yield service_coordinator +def mock_service(): + service = mock.MagicMock() + app.state.service = service + yield service -def test_delete(mock_service_coordinator): - mock_service_coordinator.exists_knowledge_graph.return_value = True - mock_service_coordinator.clear.return_value = None +def test_delete(mock_service): + mock_service["knowledge_graph_service"].exists.return_value = True + mock_service["knowledge_graph_service"].clear.return_value = None response = client.get("repository/delete") assert response.status_code == 200 diff --git a/tests/app/services/test_db_engine.py b/tests/app/services/test_db_engine.py deleted file mode 100644 index 86f3c949..00000000 --- a/tests/app/services/test_db_engine.py +++ /dev/null @@ -1,12 +0,0 @@ -import pytest -from sqlmodel import create_engine - -from prometheus.app.db import create_db_and_tables - - -@pytest.fixture(scope="session") -def test_engine(postgres_container_fixture): - url = postgres_container_fixture.get_connection_url() - engine = create_engine(url, echo=True) - create_db_and_tables() - yield engine diff --git a/tests/app/services/test_service_coordinator.py b/tests/app/services/test_service_coordinator.py deleted file mode 100644 index 71652628..00000000 --- a/tests/app/services/test_service_coordinator.py +++ /dev/null @@ -1,115 +0,0 @@ -import tempfile -from pathlib import Path -from unittest.mock import Mock - -import pytest - -from prometheus.app.services.issue_service import IssueService -from prometheus.app.services.knowledge_graph_service import KnowledgeGraphService -from prometheus.app.services.llm_service import LLMService -from prometheus.app.services.neo4j_service import Neo4jService -from prometheus.app.services.repository_service import RepositoryService -from prometheus.app.services.service_coordinator import ServiceCoordinator - - -@pytest.fixture -def mock_services(): - llm_service = Mock(spec=LLMService) - llm_service.model = Mock() - - issue_service = Mock(sepc=IssueService) - knowledge_graph_service = Mock(spec=KnowledgeGraphService) - knowledge_graph_service.kg = None - knowledge_graph_service.get_local_path.return_value = None - neo4j_service = Mock(spec=Neo4jService) - repository_service = Mock(spec=RepositoryService) - repository_service.get_working_dir.return_value = None - - yield { - "issue_service": issue_service, - "knowledge_graph_service": knowledge_graph_service, - "llm_service": llm_service, - "neo4j_service": neo4j_service, - "max_token_per_neo4j_result": 1000, - "repository_service": repository_service, - "github_token": "test_token", - "working_directory": Path(tempfile.TemporaryDirectory().name), - } - - -@pytest.fixture -def service_coordinator(mock_services): - coordinator = ServiceCoordinator( - mock_services["issue_service"], - mock_services["knowledge_graph_service"], - mock_services["llm_service"], - mock_services["neo4j_service"], - mock_services["repository_service"], - mock_services["max_token_per_neo4j_result"], - mock_services["github_token"], - mock_services["working_directory"], - ) - return coordinator - - -def test_initialization(service_coordinator, mock_services): - """Test that services are properly initialized""" - assert service_coordinator.knowledge_graph_service == mock_services["knowledge_graph_service"] - assert service_coordinator.llm_service == mock_services["llm_service"] - assert service_coordinator.neo4j_service == mock_services["neo4j_service"] - assert service_coordinator.repository_service == mock_services["repository_service"] - - -def test_upload_local_repository(service_coordinator, mock_services): - """Test local repository upload process""" - test_path = Path("/test/path") - - service_coordinator.upload_local_repository(test_path) - - # Verify the sequence of operations - mock_services["knowledge_graph_service"].build_and_save_knowledge_graph.assert_called_once_with( - test_path - ) - - -def test_upload_github_repository(service_coordinator, mock_services): - """Test GitHub repository upload process""" - test_url = "https://github.com/test/repo" - test_commit = "abc123" - saved_path = Path("/saved/path") - mock_services["repository_service"].clone_github_repo.return_value = saved_path - - service_coordinator.upload_github_repository(test_url, test_commit) - - # Verify the sequence of operations - mock_services["repository_service"].clone_github_repo.assert_called_once_with( - mock_services["github_token"], test_url, test_commit - ) - mock_services["knowledge_graph_service"].build_and_save_knowledge_graph.assert_called_once_with( - saved_path, test_url, test_commit - ) - - -def test_clear(service_coordinator, mock_services): - """Test clearing of services""" - service_coordinator.clear() - - mock_services["knowledge_graph_service"].clear.assert_called_once() - mock_services["repository_service"].clean.assert_called_once() - - -def test_close(service_coordinator, mock_services): - """Test proper closing of services""" - service_coordinator.close() - - mock_services["neo4j_service"].close.assert_called_once() - - -def test_exists_knowledge_graph(service_coordinator, mock_services): - """Test knowledge graph existence check""" - mock_services["knowledge_graph_service"].exists.return_value = True - - result = service_coordinator.exists_knowledge_graph() - - mock_services["knowledge_graph_service"].exists.assert_called_once() - assert result is True diff --git a/tests/app/test_db.py b/tests/app/test_db.py deleted file mode 100644 index 589fca1b..00000000 --- a/tests/app/test_db.py +++ /dev/null @@ -1,49 +0,0 @@ -import pytest -from sqlmodel import Session, select - -from prometheus.app.db import create_superuser -from prometheus.app.entity.user import User -from tests.app.services.test_db_engine import test_engine # noqa: F401 -from tests.test_utils.fixtures import postgres_container_fixture # noqa: F401 - - -@pytest.mark.slow -def test_create_superuser(postgres_container_fixture, test_engine): # noqa: F811 - username = "admin" - email = "admin@example.com" - password = "strongpassword" - github_token = "ghp_123456" - - # Create superuser - create_superuser( - username=username, - email=email, - password=password, - github_token=github_token, - ) - - with Session(test_engine) as session: - user = session.exec(select(User).where(User.username == username)).first() - - assert user is not None - assert user.email == email - assert user.is_superuser is True - assert user.issue_credit == 999999 - assert user.github_token == github_token - assert user.password_hash != password # Password must be hashed - - -@pytest.mark.slow -def test_duplicate_superuser_raises(postgres_container_fixture, test_engine): # noqa: F811 - username = "admin2" - email = "admin2@example.com" - password = "password" - - create_superuser(username, email, password) - - # Trying again with same username or email - with pytest.raises(ValueError, match="Username 'admin2' already exists"): - create_superuser(username, "different@example.com", "pass") - - with pytest.raises(ValueError, match="Email 'admin2@example.com' already exists"): - create_superuser("different_user", email, "pass") diff --git a/tests/app/test_main.py b/tests/app/test_main.py index 8ad0c0c1..2a45d5f5 100644 --- a/tests/app/test_main.py +++ b/tests/app/test_main.py @@ -6,10 +6,10 @@ @pytest.fixture def mock_dependencies(): - """Mock the service coordinator dependencies""" - mock_coordinator = MagicMock() - with patch("prometheus.app.dependencies.initialize_services", return_value=mock_coordinator): - yield mock_coordinator + """Mock the service dependencies""" + mock_service = MagicMock() + with patch("prometheus.app.dependencies.initialize_services", return_value=mock_service): + yield mock_service @pytest.fixture @@ -24,5 +24,5 @@ def test_client(mock_dependencies): def test_app_initialization(test_client, mock_dependencies): """Test that the app initializes correctly with mocked dependencies""" - assert test_client.app.state.service_coordinator is not None - assert test_client.app.state.service_coordinator == mock_dependencies + assert test_client.app.state.service is not None + assert test_client.app.state.service == mock_dependencies From e7f9396825ad93a8b7da019b71b6ec15323f73c9 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:00:39 +0800 Subject: [PATCH 45/55] Fix formatting issue in test_issue.py by correcting indentation --- tests/app/api/test_issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/app/api/test_issue.py b/tests/app/api/test_issue.py index 475b4607..5367c872 100644 --- a/tests/app/api/test_issue.py +++ b/tests/app/api/test_issue.py @@ -52,7 +52,7 @@ def test_answer_issue(mock_service): "passed_build": True, "passed_existing_test": True, "issue_response": "Issue fixed", - } + }, } From d2eed5892d027bfec36cff6c2f8a98b6618cff4e Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:08:01 +0800 Subject: [PATCH 46/55] Add database service tests and remove redundant port binding in fixtures --- tests/app/services/test_database_service.py | 17 +++++++++++++++++ tests/test_utils/fixtures.py | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 tests/app/services/test_database_service.py diff --git a/tests/app/services/test_database_service.py b/tests/app/services/test_database_service.py new file mode 100644 index 00000000..5c7930b0 --- /dev/null +++ b/tests/app/services/test_database_service.py @@ -0,0 +1,17 @@ +import pytest + +from prometheus.app.services.database_service import DatabaseService +from tests.test_utils.fixtures import postgres_container_fixture # noqa: F401 + + +@pytest.mark.slow +def test_database_service(postgres_container_fixture): # noqa: F811 + url = postgres_container_fixture.get_connection_url() + database_service = DatabaseService(url) + assert database_service.engine is not None + + try: + database_service.engine.connect() + database_service.engine.dispose() # Ensure connection is valid + except Exception as e: + pytest.fail(f"Connection verification failed: {e}") diff --git a/tests/test_utils/fixtures.py b/tests/test_utils/fixtures.py index 4073a844..b2b5cdd7 100644 --- a/tests/test_utils/fixtures.py +++ b/tests/test_utils/fixtures.py @@ -60,7 +60,6 @@ def postgres_container_fixture(): port=5432, ) .with_name(f"postgres_container_{uuid.uuid4().hex[:12]}") - .with_bind_ports(5432, 5432) # Bind to a specific port for testing ) with container as postgres_container: yield postgres_container From 3b4a38a205e4a91c8132b7b796e9d1b5dd5d0f08 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:08:16 +0800 Subject: [PATCH 47/55] Refactor postgres container fixture for improved readability --- tests/test_utils/fixtures.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/test_utils/fixtures.py b/tests/test_utils/fixtures.py index b2b5cdd7..a0e7940a 100644 --- a/tests/test_utils/fixtures.py +++ b/tests/test_utils/fixtures.py @@ -51,16 +51,13 @@ def empty_neo4j_container_fixture(): @pytest.fixture(scope="session") def postgres_container_fixture(): - container = ( - PostgresContainer( - image=POSTGRES_IMAGE, - username=POSTGRES_USERNAME, - password=POSTGRES_PASSWORD, - dbname=POSTGRES_DB, - port=5432, - ) - .with_name(f"postgres_container_{uuid.uuid4().hex[:12]}") - ) + container = PostgresContainer( + image=POSTGRES_IMAGE, + username=POSTGRES_USERNAME, + password=POSTGRES_PASSWORD, + dbname=POSTGRES_DB, + port=5432, + ).with_name(f"postgres_container_{uuid.uuid4().hex[:12]}") with container as postgres_container: yield postgres_container From 04a0269aa3125f5347ae41c95d2df3d6de304e9e Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:25:30 +0800 Subject: [PATCH 48/55] Update test_issue_service.py to modify working directory path and adjust assertion results --- tests/app/services/test_issue_service.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/app/services/test_issue_service.py b/tests/app/services/test_issue_service.py index 003bb978..6e8f2224 100644 --- a/tests/app/services/test_issue_service.py +++ b/tests/app/services/test_issue_service.py @@ -49,7 +49,7 @@ def issue_service(mock_kg_service, mock_repository_service, mock_neo4j_service, mock_neo4j_service, mock_llm_service, max_token_per_neo4j_result=1000, - working_directory="/test/working/dir", + working_directory="/tmp/working_dir/", ) @@ -78,6 +78,7 @@ def test_answer_issue_with_general_container(issue_service, monkeypatch): # Exercise result = issue_service.answer_issue( + issue_number=-1, issue_title="Test Issue", issue_body="Test Body", issue_comments=[], @@ -102,7 +103,7 @@ def test_answer_issue_with_general_container(issue_service, monkeypatch): build_commands=None, test_commands=None, ) - assert result == ("test_patch", True, True, True, "test_response") + assert result == (None, "test_patch", True, True, True, "test_response") def test_answer_issue_with_user_defined_container(issue_service, monkeypatch): @@ -123,6 +124,7 @@ def test_answer_issue_with_user_defined_container(issue_service, monkeypatch): # Exercise result = issue_service.answer_issue( + issue_number=-1, issue_title="Test Issue", issue_body="Test Body", issue_comments=[], @@ -146,4 +148,4 @@ def test_answer_issue_with_user_defined_container(issue_service, monkeypatch): "FROM python:3.8", "test-image", ) - assert result == (None, False, False, False, "test_response") + assert result == (None, None, False, False, False, "test_response") From a26660f025de077f8f7b0b5f9abb89cd318a6ca3 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:30:43 +0800 Subject: [PATCH 49/55] Update environment configuration for CORS and add JWT secret key --- .github/workflows/pytest_and_coverage.yml | 7 +++++++ example.env | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest_and_coverage.yml b/.github/workflows/pytest_and_coverage.yml index 15aa9547..c80effdc 100644 --- a/.github/workflows/pytest_and_coverage.yml +++ b/.github/workflows/pytest_and_coverage.yml @@ -14,6 +14,10 @@ jobs: # Logging PROMETHEUS_LOGGING_LEVEL: DEBUG + # General settings + PROMETHEUS_ENVIRONMENT: local + PROMETHEUS_BACKEND_CORS_ORIGINS: "[\"*\"]" + # Neo4j settings PROMETHEUS_NEO4J_URI: bolt://localhost:7687 PROMETHEUS_NEO4J_USERNAME: neo4j @@ -48,6 +52,9 @@ jobs: # DATABASE settings PROMETHEUS_DATABASE_URL: postgresql://postgres:password@localhost:5432/postgres?sslmode=disable + # JWT settings + PROMETHEUS_JWT_SECRET_KEY: your_jwt_secret_key + steps: - name: Check out code uses: actions/checkout@v4 diff --git a/example.env b/example.env index 3283dffb..8699b7d4 100644 --- a/example.env +++ b/example.env @@ -3,7 +3,7 @@ PROMETHEUS_LOGGING_LEVEL=DEBUG # General settings PROMETHEUS_ENVIRONMENT=local -PROMETHEUS_BACKEND_CORS_ORIGINS=["http://localhost:9002"] +PROMETHEUS_BACKEND_CORS_ORIGINS=["*] # Neo4j settings PROMETHEUS_NEO4J_URI=bolt://neo4j:7687 From ce00bd865e5df4e2d53f77cca7e2af40cf344569 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:53:15 +0800 Subject: [PATCH 50/55] Add user service tests for superuser creation --- tests/app/services/test_user_service.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/app/services/test_user_service.py diff --git a/tests/app/services/test_user_service.py b/tests/app/services/test_user_service.py new file mode 100644 index 00000000..80fb56bd --- /dev/null +++ b/tests/app/services/test_user_service.py @@ -0,0 +1,25 @@ +import pytest + +from prometheus.app.services.database_service import DatabaseService +from prometheus.app.services.user_service import UserService +from tests.test_utils.fixtures import postgres_container_fixture + + +@pytest.fixture +def mock_database_service(postgres_container_fixture): + service = DatabaseService(postgres_container_fixture.get_connection_url()) + service.start() + return service + + +def test_create_superuser(mock_database_service): + # Exercise + service = UserService(mock_database_service) + service.create_superuser( + "testuser", + "test@gmail.com", + "password123", + github_token="gh_token" + ) + +# TODO test login From 1726289674174af92a8301ea4a0627f6d3a27aa2 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:54:12 +0800 Subject: [PATCH 51/55] Refactor superuser creation test for improved readability --- tests/app/services/test_user_service.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/app/services/test_user_service.py b/tests/app/services/test_user_service.py index 80fb56bd..42b74039 100644 --- a/tests/app/services/test_user_service.py +++ b/tests/app/services/test_user_service.py @@ -2,7 +2,6 @@ from prometheus.app.services.database_service import DatabaseService from prometheus.app.services.user_service import UserService -from tests.test_utils.fixtures import postgres_container_fixture @pytest.fixture @@ -15,11 +14,7 @@ def mock_database_service(postgres_container_fixture): def test_create_superuser(mock_database_service): # Exercise service = UserService(mock_database_service) - service.create_superuser( - "testuser", - "test@gmail.com", - "password123", - github_token="gh_token" - ) + service.create_superuser("testuser", "test@gmail.com", "password123", github_token="gh_token") + # TODO test login From a773c2c2d68f46a305563d8eabadff33e62a9111 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 23:01:15 +0800 Subject: [PATCH 52/55] Refactor mock_database_service fixture to use yield for resource management --- tests/app/services/test_user_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/app/services/test_user_service.py b/tests/app/services/test_user_service.py index 42b74039..fbc24d78 100644 --- a/tests/app/services/test_user_service.py +++ b/tests/app/services/test_user_service.py @@ -2,13 +2,15 @@ from prometheus.app.services.database_service import DatabaseService from prometheus.app.services.user_service import UserService +from tests.test_utils.fixtures import postgres_container_fixture # noqa: F401 @pytest.fixture -def mock_database_service(postgres_container_fixture): +def mock_database_service(postgres_container_fixture): # noqa: F811 service = DatabaseService(postgres_container_fixture.get_connection_url()) service.start() - return service + yield service + service.close() def test_create_superuser(mock_database_service): From 63e2ce1f5c36fadfef57b1bdd594796c2b7d46c0 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Mon, 4 Aug 2025 23:58:37 +0800 Subject: [PATCH 53/55] Add login test and refactor user retrieval queries in UserService --- prometheus/app/services/user_service.py | 15 +++++++-------- tests/app/services/test_user_service.py | 7 ++++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/prometheus/app/services/user_service.py b/prometheus/app/services/user_service.py index e865c943..32cc9124 100644 --- a/prometheus/app/services/user_service.py +++ b/prometheus/app/services/user_service.py @@ -2,7 +2,7 @@ from typing import Optional from argon2 import PasswordHasher -from sqlmodel import Session +from sqlmodel import Session, or_, select from prometheus.app.entity.user import User from prometheus.app.services.base_service import BaseService @@ -41,9 +41,11 @@ def create_user( User: The created superuser instance. """ with Session(self.engine) as session: - if session.query(User).filter(User.username == username).first(): + statement = select(User).where(User.username == username) + if session.exec(statement).first(): raise ValueError(f"Username '{username}' already exists") - if session.query(User).filter(User.email == email).first(): + statement = select(User).where(User.email == email) + if session.exec(statement).first(): raise ValueError(f"Email '{email}' already exists") hashed_password = self.ph.hash(password) @@ -70,11 +72,8 @@ def login(self, username: str, email: str, password: str) -> str: password (str): Plaintext password. """ with Session(self.engine) as session: - user = ( - session.query(User) - .filter((User.username == username) | (User.email == email)) - .first() - ) + statement = select(User).where(or_(User.username == username, User.email == email)) + user = session.exec(statement).first() if not user: raise ValueError("Invalid username or email") diff --git a/tests/app/services/test_user_service.py b/tests/app/services/test_user_service.py index fbc24d78..3136972f 100644 --- a/tests/app/services/test_user_service.py +++ b/tests/app/services/test_user_service.py @@ -19,4 +19,9 @@ def test_create_superuser(mock_database_service): service.create_superuser("testuser", "test@gmail.com", "password123", github_token="gh_token") -# TODO test login +def test_login(mock_database_service): + # Exercise + service = UserService(mock_database_service) + access_token = service.login("testuser", "test@gmail.com", "password123") + # Verify + assert access_token is not None From 9bdf3a6ecdf98e4725ad08a30115bf85a4ae0844 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:03:35 +0800 Subject: [PATCH 54/55] Refactor email validation method to use class method syntax --- prometheus/app/models/requests/auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prometheus/app/models/requests/auth.py b/prometheus/app/models/requests/auth.py index 96ef3fea..f0e8b8fa 100644 --- a/prometheus/app/models/requests/auth.py +++ b/prometheus/app/models/requests/auth.py @@ -17,8 +17,9 @@ class LoginRequest(BaseModel): max_length=30, ) + @classmethod @field_validator("email", mode="after") - def validate_email_format(self, v: str) -> str: + def validate_email_format(cls, v: str) -> str: pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$" if not re.match(pattern, v): raise ValueError("Invalid email format") From 439a9e87440623c3fab6ec9b89e7c27043d793c8 Mon Sep 17 00:00:00 2001 From: Yue Pan <79363355+dcloud347@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:09:04 +0800 Subject: [PATCH 55/55] Add unit tests for authentication login endpoint --- tests/app/api/test_auth.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/app/api/test_auth.py diff --git a/tests/app/api/test_auth.py b/tests/app/api/test_auth.py new file mode 100644 index 00000000..240cfe9e --- /dev/null +++ b/tests/app/api/test_auth.py @@ -0,0 +1,38 @@ +from unittest import mock + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from prometheus.app.api import auth +from prometheus.app.exception_handler import register_exception_handlers + +app = FastAPI() +register_exception_handlers(app) +app.include_router(auth.router, prefix="/auth", tags=["auth"]) +client = TestClient(app) + + +@pytest.fixture +def mock_service(): + service = mock.MagicMock() + app.state.service = service + yield service + + +def test_login(mock_service): + mock_service["user_service"].login.return_value = "your_access_token" + response = client.post( + "/auth/login", + json={ + "username": "testuser", + "email": "test@gmail.com", + "password": "passwordpassword", + }, + ) + assert response.status_code == 200 + assert response.json() == { + "code": 200, + "message": "success", + "data": {"access_token": "your_access_token"}, + }