Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
name: coverage-reports

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
Expand Down
3 changes: 3 additions & 0 deletions backend/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ BACKLOG__AI_AGENT__TOKEN=

BACKLOG__SUPERUSER__EMAIL=admin@site.com
BACKLOG__SUPERUSER__PASSWORD=admin

BACKLOG__AI_AGENT__BASE_URL=
BACKLOG__AI_AGENT__TOKEN=
2 changes: 1 addition & 1 deletion backend/backlog_app/api/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async def create_movie(
await db.commit()
await db.refresh(movie)

logger.info("Movie <%s> has been created.", movie.id)
logger.info("Movie <%s> has been created.", movie.slug)

return MovieRead.model_validate(movie)

Expand Down
3 changes: 1 addition & 2 deletions backend/backlog_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,8 @@ def url(self) -> str:

class AIAgentConfig(BaseModel):
base_url: str
access_id: str
token: str
model: str = "DeepSeek v3.2"
model: str = "deepseek-ai/DeepSeek-V3"
timeout: int = 10


Expand Down
44 changes: 22 additions & 22 deletions backend/backlog_app/servicies/ai_agent/client.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
import httpx
from openai import OpenAI

from backlog_app.config import settings


class AIClient:
def __init__(self, model: str):
self.model = model
self.client = httpx.AsyncClient(timeout=settings.ai_agent.timeout)
self.client = OpenAI(
base_url=settings.ai_agent.base_url, api_key=settings.ai_agent.token
)

async def translate(self, text: str) -> str:
payload = {
"model": self.model,
"messages": [
response = self.client.chat.completions.create(
model=self.model,
max_tokens=2500,
temperature=0.5,
presence_penalty=0,
top_p=0.95,
messages=[
{
"role": "user",
"role": "system",
"content": (
"Translate the following text from English into Russian, "
"preserve the meaning and naturalness of the language:\n\n"
f"{text}"
"You are a translation engine. "
"Return ONLY the translated text in Russian. "
"Do NOT add explanations, comments, notes, or formatting. "
"Output must contain only the translation."
),
}
},
{
"role": "user",
"content": f"{text}",
},
],
"temperature": 0.3,
}

response = await self.client.post(
f"{settings.ai_agent.base_url}/api/v1/cloud-ai/agents/{settings.ai_agent.access_id}/v1/chat/completions",
json=payload,
headers={
"Authorization": f"Bearer {settings.ai_agent.token}",
},
)

response.raise_for_status()
data = response.json()
return data["choices"][0]["message"]["content"]
return response.choices[0].message.content
26 changes: 14 additions & 12 deletions backend/backlog_app/tasks/movie_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,72 +17,74 @@


async def update_movie_rating(movie: MovieRead, db: AsyncSession, user: User):
movie_db = await crud.get_movie_by_id(db, movie.id)
movie_db = await crud.get_movie_by_slug(db, movie.slug)
year = getattr(movie, "year", None)

if year is None:
logger.info("Movie <%s> has no year, skipping rating update", movie.id)
logger.info("Movie <%s> has no year, skipping rating update", movie.slug)
return

try:
imdb_rating, metacritic_score = await provider.get_title_rating(
movie_db.title, movie_db.year
)
except Exception as e:
logger.exception("Failed to fetch rating for movie <%s>: %s", movie.id, e)
logger.exception("Failed to fetch rating for movie <%s>: %s", movie.slug, e)
return

await partial_update_movie(
db,
movie.id,
movie.slug,
MovieUpdate(
imdb_rating=imdb_rating,
metacritic_score=metacritic_score,
),
user,
)

logger.info("Movie <%s> ratings updated in background", movie.id)
logger.info("Movie <%s> ratings updated in background", movie.slug)


async def update_movie_description(
movie: MovieRead, db: AsyncSession, user: User
) -> None:

movie_db = await crud.get_movie_by_id(db, movie.id)
movie_db = await crud.get_movie_by_slug(db, movie.slug)

description = getattr(movie_db, "description", None)
year = getattr(movie_db, "year", None)

if description and description.strip():
logger.info("Movie <%s> already has description, skipping", movie.id)
logger.info("Movie <%s> already has description, skipping", movie.slug)
return

if not year:
logger.info("Movie <%s> has no year, skipping description update", movie.id)
logger.info("Movie <%s> has no year, skipping description update", movie.slug)
return

try:
en_description = await provider.get_title_description(
movie_db.title, movie_db.year
)
except Exception as e:
logger.exception("Failed to fetch description for movie <%s>: %s", movie.id, e)
logger.exception(
"Failed to fetch description for movie <%s>: %s", movie.slug, e
)
return

try:
ru_description = await translator.translate(en_description)
except Exception as e:
logger.exception(
"Failed to translate description for movie <%s>: %s", movie.id, e
"Failed to translate description for movie <%s>: %s", movie.slug, e
)
return

await partial_update_movie(
db,
movie.id,
movie.slug,
MovieUpdate(description=ru_description),
user,
)

logger.info("Movie <%s> description updated in background", movie.id)
logger.info("Movie <%s> description updated in background", movie.slug)
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies = [
"fastapi-users[sqlalchemy]>=15.0.3",
"fastapi[standard]>=0.128.0",
"jinja2>=3.1.6",
"openai>=2.41.0",
"pydantic-settings[yaml]>=2.12.0",
"python-slugify>=8.0.4",
"sqlalchemy[asyncio]>=2.0.46",
Expand Down
44 changes: 25 additions & 19 deletions backend/tests/test_servicies/test_ai_agent/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,38 @@
async def test_translate_success():
client = AIClient(model="gpt-test")

mock_response = AsyncMock()
mock_response.raise_for_status = Mock()
mock_response.json = Mock(
return_value={"choices": [{"message": {"content": "Привет мир"}}]}
)
mock_response = Mock()
mock_response.choices = [
Mock(
message=Mock(
content="Привет мир",
)
)
]

with patch.object(client.client, "post", return_value=mock_response) as mock_post:
with patch.object(
client.client.chat.completions,
"create",
return_value=mock_response,
) as mock_create:
result = await client.translate("Hello world")

assert result == "Привет мир"

called_payload = mock_post.call_args[1]["json"]
assert (
"Translate the following text from English into Russian"
in called_payload["messages"][0]["content"]
)
assert "Hello world" in called_payload["messages"][0]["content"]
kwargs = mock_create.call_args.kwargs

assert kwargs["model"] == "gpt-test"
assert kwargs["messages"][1]["content"] == "Hello world"


@pytest.mark.asyncio
async def test_translate_http_error():
async def test_translate_error():
client = AIClient(model="gpt-test")

mock_response = AsyncMock()
mock_response.raise_for_status = Mock(side_effect=Exception("HTTP Error"))
mock_response.json = AsyncMock(return_value={})

with patch.object(client.client, "post", return_value=mock_response):
with pytest.raises(Exception, match="HTTP Error"):
with patch.object(
client.client.chat.completions,
"create",
side_effect=Exception("API Error"),
):
with pytest.raises(Exception, match="API Error"):
await client.translate("Hello world")
Loading
Loading