Skip to content

Commit d85ea9f

Browse files
committed
initial commit
0 parents  commit d85ea9f

23 files changed

+364
-0
lines changed

.github/workflows/mlops_workflow.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: MLOps Workflow
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v2
14+
- name: Set up Python
15+
uses: actions/setup-python@v2
16+
with:
17+
python-version: '3.9'
18+
- name: Install dependencies
19+
run: |
20+
python -m pip install --upgrade pip
21+
pip install -r requirements.txt
22+
- name: Run tests
23+
run: pytest

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.env
2+
env

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Codinsight Fast Api Server

requirements.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
fastapi
2+
pydantic[email]
3+
requests
4+
pydantic-settings
5+
pymongo
6+
pytest
7+
httpx
8+
groq
9+
pytest-asyncio

src/__init__.py

Whitespace-only changes.

src/code_explainer/__init__.py

Whitespace-only changes.

src/code_explainer/constants.py

Whitespace-only changes.

src/code_explainer/exceptions.py

Whitespace-only changes.

src/code_explainer/models.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from pydantic import BaseModel, BeforeValidator, Field, EmailStr, GetJsonSchemaHandler, ConfigDict
2+
from typing import Any, Callable, Optional, Annotated
3+
from datetime import datetime
4+
from bson import ObjectId
5+
from pydantic_core import core_schema
6+
7+
8+
9+
PyObjectId = Annotated[str, BeforeValidator(str)]
10+
11+
12+
class UserModel(BaseModel):
13+
model_config = ConfigDict(
14+
populate_by_name=True,
15+
arbitrary_types_allowed = True,
16+
json_encoders = {ObjectId: str}
17+
)
18+
19+
id: Optional[PyObjectId] = Field(alias="_id", default=None)
20+
email: EmailStr = Field(...)
21+
password: str = Field(...)
22+
23+
class CompletionModel(BaseModel):
24+
model_config = ConfigDict(
25+
populate_by_name=True,
26+
arbitrary_types_allowed = True,
27+
json_encoders = {ObjectId: str}
28+
)
29+
30+
id: Optional[PyObjectId] = Field(alias="_id", default=None)
31+
prompt: str = Field(...)
32+
completion: str = Field(...)
33+
inference_duration_in_ms: float = Field(gt=0)
34+
user_ip: str = Field(...)
35+
user_device: str = Field(...)
36+
user_id: Optional[PyObjectId] = Field(None)
37+
timestamp: datetime = Field(default_factory=lambda: datetime.now())
38+
rating: Optional[int] = Field(None, ge=1, le=5)
39+
40+
def __eq__(self, other):
41+
if isinstance(other, CompletionModel):
42+
return (
43+
self.prompt == other.prompt and
44+
self.completion == other.completion and
45+
self.user_ip == other.user_ip and
46+
self.user_device == other.user_device and
47+
self.user_id == other.user_id and
48+
self.rating == other.rating and
49+
self.inference_duration_in_ms == other.inference_duration_in_ms
50+
)
51+
return False

src/code_explainer/router.py

Whitespace-only changes.

src/code_explainer/schemas.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from pydantic import BaseModel, EmailStr, Field
2+
from typing import Optional, List
3+
from datetime import datetime
4+
5+
# Completion Schemas
6+
class CompletionCreate(BaseModel):
7+
prompt: str = Field(..., min_length=1)
8+
user_ip: Optional[str] = None
9+
user_device: Optional[str] = None
10+
11+
class CompletionResponse(BaseModel):
12+
id: str
13+
prompt: str
14+
completion: str
15+
16+
class CompletionUpdate(BaseModel):
17+
rating: int = Field(..., ge=1, le=5)
18+
19+
class CompletionList(BaseModel):
20+
total: int
21+
completions: List[CompletionResponse]
22+
23+
# Error Schemas
24+
class HTTPError(BaseModel):
25+
detail: str

src/code_explainer/service.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import time
2+
from typing import Dict, Any
3+
from src.code_explainer.models import CompletionModel
4+
from src.config import settings
5+
from groq import Groq
6+
7+
from src.database import get_db
8+
from src.exceptions import ExternalServiceError
9+
10+
class CodeExplainService:
11+
def __init__(self):
12+
self.api_key = settings.LLM_API_KEY
13+
self.model = settings.MODEL
14+
self.db = get_db()
15+
16+
17+
def save_completion(self, completion: CompletionModel):
18+
completion_dict = completion.model_dump(by_alias=True)
19+
self.db.completions.insert_one(completion_dict)
20+
21+
async def get_code_explanation(self, code: str, user_ip: str, user_device: str) -> Dict[str, Any]:
22+
start_time = time.time()
23+
24+
client = Groq(api_key= self.api_key)
25+
26+
try:
27+
chat_completion = client.chat.completions.create(
28+
messages=
29+
[
30+
{
31+
"role": "user",
32+
"content": code
33+
}
34+
],
35+
model=self.model,)
36+
except:
37+
raise ExternalServiceError(service='groq', detail='')
38+
39+
end_time = time.time()
40+
duration = end_time - start_time
41+
42+
explanation = chat_completion.choices[0].message.content
43+
44+
completion = CompletionModel(
45+
prompt=code,
46+
completion=explanation,
47+
user_ip= user_ip,
48+
user_device=user_device,
49+
inference_duration_in_ms=duration,
50+
user_id= None,
51+
rating=None
52+
)
53+
54+
self.save_completion(completion)
55+
56+
return explanation, duration
57+
58+
code_explain_service = CodeExplainService()

src/code_explainer/utils.py

Whitespace-only changes.

src/config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from pydantic_settings import BaseSettings, SettingsConfigDict
2+
from functools import lru_cache
3+
4+
class Settings(BaseSettings):
5+
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')
6+
7+
# Database settings
8+
DATABASE_URL: str = "sqlite:///./test.db"
9+
10+
# API settings
11+
API_V1_STR: str = "/api/v1"
12+
PROJECT_NAME: str = "Codinsight"
13+
14+
# Security settings
15+
SECRET_KEY: str = "YOUR_SECRET_KEY_HERE"
16+
ALGORITHM: str = "HS256"
17+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
18+
19+
# AWS settings
20+
AWS_ACCESS_KEY_ID: str = "YOUR_AWS_ACCESS_KEY_ID"
21+
AWS_SECRET_ACCESS_KEY: str = "YOUR_AWS_SECRET_ACCESS_KEY"
22+
AWS_REGION: str = "us-west-2"
23+
24+
LLM_API_KEY: str = "YOUR_LLM_API_KEY"
25+
MODEL: str = "YOUR_MODEL"
26+
27+
@lru_cache()
28+
def get_settings():
29+
return Settings()
30+
31+
# Usage
32+
settings = get_settings()
33+
print(settings.DATABASE_URL)

src/database.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from pymongo import MongoClient
2+
from pymongo.database import Database
3+
from src.config import settings
4+
5+
client = MongoClient(settings.DATABASE_URL)
6+
db = client.get_database(name='alpha')
7+
8+
def get_db() -> Database:
9+
return db
10+
11+
# Optional: Function to close the connection
12+
def close_mongo_connection():
13+
client.close()

src/exceptions.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from fastapi import HTTPException, status
2+
3+
class NotFoundError(HTTPException):
4+
def __init__(self, detail: str):
5+
super().__init__(status_code=status.HTTP_404_NOT_FOUND, detail=detail)
6+
7+
class UnauthorizedError(HTTPException):
8+
def __init__(self, detail: str):
9+
super().__init__(
10+
status_code=status.HTTP_401_UNAUTHORIZED,
11+
detail=detail,
12+
headers={"WWW-Authenticate": "Bearer"},
13+
)
14+
15+
class ForbiddenError(HTTPException):
16+
def __init__(self, detail: str):
17+
super().__init__(status_code=status.HTTP_403_FORBIDDEN, detail=detail)
18+
19+
class BadRequestError(HTTPException):
20+
def __init__(self, detail: str):
21+
super().__init__(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
22+
23+
class InternalServerError(HTTPException):
24+
def __init__(self, detail: str = "Internal server error"):
25+
super().__init__(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=detail)
26+
27+
class DatabaseError(Exception):
28+
def __init__(self, detail: str):
29+
self.detail = detail
30+
31+
class ExternalServiceError(Exception):
32+
def __init__(self, service: str, detail: str):
33+
self.service = service
34+
self.detail = detail

src/main.py

Whitespace-only changes.

src/models.py

Whitespace-only changes.

tests/__init__.py

Whitespace-only changes.

tests/code_explainer/__init__.py

Whitespace-only changes.

tests/code_explainer/test_service.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import pytest
2+
from src.code_explainer.models import CompletionModel
3+
from src.code_explainer.service import code_explain_service
4+
5+
@pytest.mark.asyncio
6+
async def test_get_code_explanation(test_db):
7+
8+
# Code example to test
9+
code = '''
10+
def add(x,y):
11+
return x + y
12+
''';
13+
14+
# Mock user data
15+
user_ip = '8.8.8.8'
16+
user_device = 'mac m1 pro'
17+
18+
explanation, duration = await code_explain_service.get_code_explanation(code=code, user_ip=user_ip, user_device=user_device);
19+
20+
completion = CompletionModel(
21+
prompt=code,
22+
completion=explanation,
23+
user_ip= user_ip,
24+
user_device=user_device,
25+
inference_duration_in_ms=duration,
26+
user_id= None,
27+
rating=None
28+
)
29+
30+
# Get the last inserted data
31+
cursor = test_db.find().sort({'_id': -1}).limit(1)
32+
33+
completion2 = [CompletionModel(**data) for data in cursor][0]
34+
35+
assert completion2 == completion
36+
37+
38+
39+
40+

tests/conftest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import pytest
2+
from src.database import get_db, close_mongo_connection
3+
4+
@pytest.fixture(scope="session", autouse=True)
5+
def setup_mongo_connection():
6+
"""
7+
Set up the MongoDB connection at the start of the test session.
8+
Close the connection after all tests have run.
9+
"""
10+
get_db() # Initialize the connection
11+
12+
yield
13+
14+
close_mongo_connection() # Close the connection after the session
15+
16+
@pytest.fixture(scope="module")
17+
def test_db():
18+
"""
19+
Use a test collection within the test database.
20+
This fixture is scoped to the module and ensures a clean environment for each test module.
21+
"""
22+
db = get_db()
23+
24+
# Create a test collection
25+
test_collection = db.completions
26+
27+
yield test_collection
28+
29+
# Clean up after all tests in the module by dropping the test collection
30+
test_collection.drop()

tests/test_database.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from src.database import get_db
2+
3+
def test_database_connection():
4+
db = get_db()
5+
assert db is not None, "Database connection failed"
6+
7+
def test_insert_and_retrieve(test_db):
8+
# Insert a test document
9+
test_document = {"name": "Test User", "email": "test@example.com"}
10+
result = test_db.insert_one(test_document)
11+
assert result.inserted_id is not None, "Failed to insert test document"
12+
13+
# Retrieve the test document
14+
retrieved_document = test_db.find_one({"_id": result.inserted_id})
15+
assert retrieved_document is not None, "Failed to retrieve test document"
16+
assert retrieved_document["name"] == "Test User", "Retrieved document data mismatch"
17+
18+
def test_update(test_db):
19+
# Insert a test document
20+
test_document = {"name": "Update Test", "status": "pending"}
21+
result = test_db.insert_one(test_document)
22+
23+
# Update the document
24+
update_result = test_db.update_one(
25+
{"_id": result.inserted_id},
26+
{"$set": {"status": "completed"}}
27+
)
28+
assert update_result.modified_count == 1, "Failed to update test document"
29+
30+
# Verify the update
31+
updated_document = test_db.find_one({"_id": result.inserted_id})
32+
assert updated_document["status"] == "completed", "Document update failed"
33+
34+
def test_delete(test_db):
35+
# Insert a test document
36+
test_document = {"name": "Delete Test"}
37+
result = test_db.insert_one(test_document)
38+
39+
# Delete the document
40+
delete_result = test_db.delete_one({"_id": result.inserted_id})
41+
assert delete_result.deleted_count == 1, "Failed to delete test document"
42+
43+
# Verify the deletion
44+
deleted_document = test_db.find_one({"_id": result.inserted_id})
45+
assert deleted_document is None, "Document was not deleted"

0 commit comments

Comments
 (0)