From b3198a774eeb28394e2ce12bb6d5655dba7ed4aa Mon Sep 17 00:00:00 2001 From: pc in honor of Ross Date: Fri, 7 Nov 2025 19:34:35 +0300 Subject: [PATCH 1/5] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D1=87=D0=B0=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/main.py | 30 +++-- simple_backend/src/task_tracker/models.py | 8 ++ .../src/task_tracker/requirements.txt | Bin 25 -> 914 bytes simple_backend/src/task_tracker/storage.py | 105 ++++++++++++++++++ 4 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 simple_backend/src/task_tracker/models.py create mode 100644 simple_backend/src/task_tracker/storage.py diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 3db98d0d..88ae8fbd 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,19 +1,35 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Body +from storage import JSONStorage, CloudJSONStorage +from models import Task, TaskCreate +import os +from dotenv import load_dotenv app = FastAPI() +load_dotenv() +BIN_ID = os.getenv('BIN_ID') +MASTER_KEY = os.getenv('MASTER_KEY') + +storage = CloudJSONStorage(bin_id=BIN_ID, master_key=MASTER_KEY) @app.get("/tasks") def get_tasks(): - pass + return storage.get_task() @app.post("/tasks") -def create_task(task): - pass +def create_task(task_data: TaskCreate): + task = storage.create_task(task_data.model_dump()) + return task @app.put("/tasks/{task_id}") -def update_task(task_id: int): - pass +def update_task(task_id: int, task_data: TaskCreate = Body(...)): + is_update = storage.update_task(task_id, task_data.model_dump()) + if is_update: + return is_update + return None @app.delete("/tasks/{task_id}") def delete_task(task_id: int): - pass + is_delete = storage.delete_task(task_id) + if is_delete: + return True + return None diff --git a/simple_backend/src/task_tracker/models.py b/simple_backend/src/task_tracker/models.py new file mode 100644 index 00000000..8398bc65 --- /dev/null +++ b/simple_backend/src/task_tracker/models.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + +class TaskCreate(BaseModel): + title: str + status: bool + +class Task(TaskCreate): + id: int \ No newline at end of file diff --git a/simple_backend/src/task_tracker/requirements.txt b/simple_backend/src/task_tracker/requirements.txt index 8e0578a0c2cb636e21d871d5ae4fdbb81887bee2..136952186363f93581c4f8341dbbf9b55b6092d4 100644 GIT binary patch literal 914 zcmZWo%TmHX5bU#6ehQX(;N#%IyHGc3QS@WK>T>MdnOBrwY5O)^kb%X`T5RJ zb2S){aTi!%!rdUp1J^aKY$v$H1rh(T1NLai>ks%_;{^O371gAb@;u^_qN+Jr9X%^7 zxrK;Xi7_>toTnqMq;p4yg6N5L+u@yOYx2HKMeKwOd88vt)f_8RzT>&-!MP!~p=M?} zVN$VgjJ;%~w@lixH}w;-yJP>QO%u2gnL)oKb6_e1J>>K^o5&n0rd-n{bgpESO3OSK zJg>=C|EkBMcbM%QGb2vZ6bE!_eR_UE4=9PK$Gg=%*Vr0b^SHj;Ux Date: Sat, 8 Nov 2025 17:07:05 +0300 Subject: [PATCH 2/5] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D1=87=D0=B0=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/clients.py | 31 ++++++++++++++++++++++ simple_backend/src/task_tracker/main.py | 18 +++++++++---- 2 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 simple_backend/src/task_tracker/clients.py diff --git a/simple_backend/src/task_tracker/clients.py b/simple_backend/src/task_tracker/clients.py new file mode 100644 index 00000000..3862dbce --- /dev/null +++ b/simple_backend/src/task_tracker/clients.py @@ -0,0 +1,31 @@ +import requests + + +class CloudFlareClient: + def __init__(self, api_token, account_id): + self.api_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/" + self.headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json" + } + self.inputs = [ + {"role": "system", "content": + "Отвечай по-русски, кратко (1–2 предложения)," + "как лучше выполнить задачу из списка дел. Без приветствий и воды." + "Отвечай естественно, без вводных фраз вроде «Для выполнения задачи…», «Чтобы сделать…», «Следует…»." + "Сразу переходи к сути," + "Если в задаче есть действие, пиши как" + " будто даёшь понятную инструкцию или совет, а не академическое объяснение"} + ] + + def generate_answer(self,task): + data = {"messages": self.inputs + [{"role": "user", "content": task}]} + response = requests.post(f"{self.api_url}@cf/meta/llama-3-8b-instruct", headers=self.headers, json=data) + + + if response.status_code != 200: + print("Ошибка запроса:", response.status_code, response.text) + return None + + result = response.json() + return result.get("result", {}).get("response", "⚠️ Нет ответа в JSON") \ No newline at end of file diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 88ae8fbd..23dfdd7a 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -3,33 +3,41 @@ from models import Task, TaskCreate import os from dotenv import load_dotenv +from clients import CloudFlareClient app = FastAPI() + load_dotenv() BIN_ID = os.getenv('BIN_ID') MASTER_KEY = os.getenv('MASTER_KEY') - storage = CloudJSONStorage(bin_id=BIN_ID, master_key=MASTER_KEY) +API_TOKEN_AI = os.getenv('API_TOKEN_AI') +ACCOUNT_ID_AI = os.getenv('ACCOUNT_ID_AI') +client_ai = CloudFlareClient(API_TOKEN_AI, ACCOUNT_ID_AI) + + @app.get("/tasks") def get_tasks(): return storage.get_task() @app.post("/tasks") def create_task(task_data: TaskCreate): + ai_reply = client_ai.generate_answer(task_data.title) + task_data.title = f"{task_data.title} — {ai_reply}" task = storage.create_task(task_data.model_dump()) return task @app.put("/tasks/{task_id}") def update_task(task_id: int, task_data: TaskCreate = Body(...)): + ai_reply = client_ai.generate_answer(task_data.title) + task_data.title = f"{task_data.title} — {ai_reply}" is_update = storage.update_task(task_id, task_data.model_dump()) - if is_update: - return is_update + if is_update: return is_update return None @app.delete("/tasks/{task_id}") def delete_task(task_id: int): is_delete = storage.delete_task(task_id) - if is_delete: - return True + if is_delete: return True return None From 5acd6ab13bc2b7084201bd5f6363c466898f2d0e Mon Sep 17 00:00:00 2001 From: pc in honor of Ross Date: Mon, 10 Nov 2025 13:32:20 +0300 Subject: [PATCH 3/5] fixes --- simple_backend/src/task_tracker/clients.py | 45 ++++++---- simple_backend/src/task_tracker/config.py | 16 ++++ simple_backend/src/task_tracker/main.py | 53 +++++++----- simple_backend/src/task_tracker/models.py | 7 +- .../src/task_tracker/requirements.txt | Bin 914 -> 1044 bytes simple_backend/src/task_tracker/storage.py | 79 ++++++++++-------- 6 files changed, 123 insertions(+), 77 deletions(-) create mode 100644 simple_backend/src/task_tracker/config.py diff --git a/simple_backend/src/task_tracker/clients.py b/simple_backend/src/task_tracker/clients.py index 3862dbce..69c01cae 100644 --- a/simple_backend/src/task_tracker/clients.py +++ b/simple_backend/src/task_tracker/clients.py @@ -1,31 +1,40 @@ import requests - +from loguru import logger class CloudFlareClient: + + inputs = [ + {"role": "system", "content": + "Отвечай по-русски, кратко (1–2 предложения)," + "как лучше выполнить задачу из списка дел. Без приветствий и воды." + "Отвечай естественно, без вводных фраз вроде «Для выполнения задачи…», «Чтобы сделать…», «Следует…»." + "Сразу переходи к сути," + "Если в задаче есть действие, пиши как" + " будто даёшь понятную инструкцию или совет, а не академическое объяснение"} ] + def __init__(self, api_token, account_id): self.api_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/" self.headers = { "Authorization": f"Bearer {api_token}", "Content-Type": "application/json" } - self.inputs = [ - {"role": "system", "content": - "Отвечай по-русски, кратко (1–2 предложения)," - "как лучше выполнить задачу из списка дел. Без приветствий и воды." - "Отвечай естественно, без вводных фраз вроде «Для выполнения задачи…», «Чтобы сделать…», «Следует…»." - "Сразу переходи к сути," - "Если в задаче есть действие, пиши как" - " будто даёшь понятную инструкцию или совет, а не академическое объяснение"} - ] - def generate_answer(self,task): - data = {"messages": self.inputs + [{"role": "user", "content": task}]} - response = requests.post(f"{self.api_url}@cf/meta/llama-3-8b-instruct", headers=self.headers, json=data) + def generate_answer(self,task): + data = {"messages": CloudFlareClient.inputs + [{"role": "user", "content": task}]} + try: + response = requests.post(f"{self.api_url}@cf/meta/llama-3-8b-instruct", headers=self.headers, json=data) + response.raise_for_status() + result = response.json() + response_text = result["result"]["response"] - if response.status_code != 200: - print("Ошибка запроса:", response.status_code, response.text) - return None + if not response_text: + raise ValueError("Пустой или некорректный ответ от CloudFlare AI") + return response_text - result = response.json() - return result.get("result", {}).get("response", "⚠️ Нет ответа в JSON") \ No newline at end of file + except requests.exceptions.HTTPError as e: + logger.error(f'Ошибка при запросе к Cloudflare AI: {e}') + raise + except requests.exceptions.ConnectionError as e: + logger.error(f'Ошибка соединения Cloudflare AI: {e}') + raise \ No newline at end of file diff --git a/simple_backend/src/task_tracker/config.py b/simple_backend/src/task_tracker/config.py new file mode 100644 index 00000000..cd021141 --- /dev/null +++ b/simple_backend/src/task_tracker/config.py @@ -0,0 +1,16 @@ +from pydantic_settings import BaseSettings +from starlette.config import Config + +config = Config(".env") + +class Settings(BaseSettings): + BIN_ID: str = config("BIN_ID", cast=str) + MASTER_KEY: str = config("MASTER_KEY", cast=str) + API_TOKEN_AI: str = config("API_TOKEN_AI", cast=str) + ACCOUNT_ID_AI: str = config("ACCOUNT_ID_AI", cast=str) + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + +settings = Settings() diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 23dfdd7a..939cf0cf 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,43 +1,50 @@ -from fastapi import FastAPI, Body +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from pydantic import ValidationError from storage import JSONStorage, CloudJSONStorage from models import Task, TaskCreate -import os -from dotenv import load_dotenv +from config import settings from clients import CloudFlareClient +from typing import List app = FastAPI() +storage = CloudJSONStorage(bin_id=settings.BIN_ID, master_key=settings.MASTER_KEY) +client_ai = CloudFlareClient(settings.API_TOKEN_AI, settings.ACCOUNT_ID_AI) -load_dotenv() -BIN_ID = os.getenv('BIN_ID') -MASTER_KEY = os.getenv('MASTER_KEY') -storage = CloudJSONStorage(bin_id=BIN_ID, master_key=MASTER_KEY) -API_TOKEN_AI = os.getenv('API_TOKEN_AI') -ACCOUNT_ID_AI = os.getenv('ACCOUNT_ID_AI') -client_ai = CloudFlareClient(API_TOKEN_AI, ACCOUNT_ID_AI) +@app.exception_handler(ValueError) +async def value_error_handler(request, exc: ValueError): + return JSONResponse(status_code=404, content={"detail": str(exc)}) +@app.exception_handler(ValidationError) +async def validation_error_handler(request, exc: ValidationError): + return JSONResponse(status_code=422, content={"detail": exc.errors()}) -@app.get("/tasks") +@app.exception_handler(Exception) +async def global_exception_handler(request, exc: Exception): + return JSONResponse(status_code=500, content={"detail": "Internal Server Error"}) + + + +@app.get("/tasks", response_model=List[Task]) def get_tasks(): - return storage.get_task() + return storage.list_tasks() -@app.post("/tasks") +@app.post("/tasks", response_model=Task) def create_task(task_data: TaskCreate): ai_reply = client_ai.generate_answer(task_data.title) task_data.title = f"{task_data.title} — {ai_reply}" - task = storage.create_task(task_data.model_dump()) + task = storage.create_task(task_data) return task -@app.put("/tasks/{task_id}") -def update_task(task_id: int, task_data: TaskCreate = Body(...)): +@app.put("/tasks/{task_id}", response_model=Task) +def update_task(task_id: int, task_data: TaskCreate): ai_reply = client_ai.generate_answer(task_data.title) task_data.title = f"{task_data.title} — {ai_reply}" - is_update = storage.update_task(task_id, task_data.model_dump()) - if is_update: return is_update - return None + task = storage.update_task(task_id, task_data) + return task -@app.delete("/tasks/{task_id}") +@app.delete("/tasks/{task_id}", response_model=Task) def delete_task(task_id: int): - is_delete = storage.delete_task(task_id) - if is_delete: return True - return None + task = storage.delete_task(task_id) + return task \ No newline at end of file diff --git a/simple_backend/src/task_tracker/models.py b/simple_backend/src/task_tracker/models.py index 8398bc65..bd32e822 100644 --- a/simple_backend/src/task_tracker/models.py +++ b/simple_backend/src/task_tracker/models.py @@ -1,8 +1,9 @@ -from pydantic import BaseModel +from pydantic import BaseModel, Field class TaskCreate(BaseModel): - title: str - status: bool + title: str = Field(..., min_length=5, max_length=300, description="Название задачи") + + status: bool = False class Task(TaskCreate): id: int \ No newline at end of file diff --git a/simple_backend/src/task_tracker/requirements.txt b/simple_backend/src/task_tracker/requirements.txt index 136952186363f93581c4f8341dbbf9b55b6092d4..4611c7f9249514755d63149d157db393f3e6a46b 100644 GIT binary patch delta 127 zcmbQlK80h$Bu2>`hJ1!}hEj$iAhrcU0|q??b09XJypd6K@*+kRMO}tshE#?UAk1XQ z1F9_st1|+sF$6+A27}ET86Pt0m4l^?8H^a>p&F9GT5^G!K?Z}Af;1U0@G@{Q006M0 B7!m*g delta 20 ccmbQjF^PS{B*w{C7zHLDU{u*G#dL-d08TXr1poj5 diff --git a/simple_backend/src/task_tracker/storage.py b/simple_backend/src/task_tracker/storage.py index 3f046cfd..bdf2eee4 100644 --- a/simple_backend/src/task_tracker/storage.py +++ b/simple_backend/src/task_tracker/storage.py @@ -1,6 +1,7 @@ import json from pathlib import Path import requests +from models import Task, TaskCreate class IsMemoryStorage: @@ -9,17 +10,17 @@ def __init__(self): self.next_task = 1 - def list_tasks(self): + def list_tasks(self) -> list: return self.tasks - def create_task(self, task_data): - task = {'id': self.next_task, **task_data} + def create_task(self, task_data: dict) -> Task: + task = Task(id=self.next_task, **task_data) self.tasks.append(task) self.next_task += 1 return task - def get_task(self, task_id): + def get_task(self, task_id)->Task: for task in self.tasks: if task['id'] == task_id: return task @@ -46,42 +47,50 @@ def __init__(self, filepath: str | None = "tasks.json"): if not self.file.exists(): self.file.write_text("[]", encoding="utf-8") - def _load(self): - with self.file.open("r", encoding="utf-8") as f: - return json.load(f) + def _load(self) -> list[Task]: + data = json.loads(self.file.read_text(encoding="utf-8")) + return [Task(**t) for t in data] - def _save(self, data): - with self.file.open("w", encoding="utf-8") as f: - json.dump(data, f, indent=4, ensure_ascii=False) + def _save(self, tasks: list[Task]): + data = [t.model_dump() for t in tasks] + self.file.write_text(json.dumps(data, indent=4, ensure_ascii=False), encoding="utf-8") - def get_task(self): + def list_tasks(self) -> list[Task]: return self._load() - def create_task(self, task_data: dict): + def get_task(self, task_id: int) -> Task: + for task in self._load(): + if task.id == task_id: + return task + raise ValueError(f"Task with id={task_id} not found") + + + def create_task(self, task_data: TaskCreate) -> Task: tasks = self._load() - new_id = max([t["id"] for t in tasks], default=0) + 1 - task = {"id": new_id, **task_data} + new_id = max([t.id for t in tasks], default=0) + 1 + task = Task(id=new_id, **task_data.model_dump()) tasks.append(task) self._save(tasks) return task - def update_task(self, task_id: int, new_data: dict): + def update_task(self, task_id: int, task_data: TaskCreate) -> Task: tasks = self._load() - for task in tasks: - if task["id"] == task_id: - task.update(new_data) - self._save(tasks) - return task - return None + task = next((t for t in tasks if t.id == task_id), None) + if not task: + raise ValueError(f"Task with id={task_id} not found") + updated_task = task.model_copy(update=task_data.model_dump()) + tasks[tasks.index(task)] = updated_task + self._save(tasks) + return updated_task - def delete_task(self, task_id: int): + def delete_task(self, task_id: int) -> Task: tasks = self._load() - for task in tasks: - if task["id"] == task_id: - tasks.remove(task) - self._save(tasks) - return task - return None + task = next((t for t in tasks if t.id == task_id), None) + if not task: + raise ValueError(f"Task with id={task_id} not found") + tasks.remove(task) + self._save(tasks) + return task class CloudJSONStorage(JSONStorage): def __init__(self, bin_id: str, master_key: str): @@ -92,14 +101,18 @@ def __init__(self, bin_id: str, master_key: str): "Content-Type": "application/json", } - def _load(self): + def _load(self) -> list[Task]: res = requests.get(f"{self.base_url}/latest", headers=self.headers) res.raise_for_status() - record = res.json()["record"] - return record.get("tasks", []) - def _save(self, tasks): - payload = {"tasks": tasks} + record = res.json().get("record", {}) + tasks_data = record.get("tasks", []) + + tasks = [Task(**task) for task in tasks_data] + return tasks + + def _save(self, tasks: list[Task]) -> dict: + payload = {"tasks": [task.dict() for task in tasks]} res = requests.put(self.base_url, headers=self.headers, json=payload) res.raise_for_status() return res.json() From 5ec61a6707d81865d6ecbaa6dd8f05cdce5d2e89 Mon Sep 17 00:00:00 2001 From: pc in honor of Ross Date: Mon, 10 Nov 2025 13:47:25 +0300 Subject: [PATCH 4/5] add README.md --- simple_backend/src/task_tracker/README.md | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 simple_backend/src/task_tracker/README.md diff --git a/simple_backend/src/task_tracker/README.md b/simple_backend/src/task_tracker/README.md new file mode 100644 index 00000000..264f02b7 --- /dev/null +++ b/simple_backend/src/task_tracker/README.md @@ -0,0 +1,39 @@ +## Минусы хранить данные в ОЗУ +1. При перезапуске всё теряется +2. Отсутствие масштабируемости +3. Сложность интеграции с внешними сервисами +4. Ограничение объёма данных + +## что улучшилось после того, как список из оперативной памяти изменился на файл проекта? +* файл не теряется при перезапуске +* легкая интеграция +* можно создать резервные копии + +## избавились ли мы таким способом от хранения состояния или нет? +* Нет, мы не избавились от хранения состояния, мы просто перенесли его из оперативной памяти на файл. + +## где еще можно хранить задачи и какие есть преимущества и недостатки этих подходов? +### реляционные бд +**Плюсы:** +- Высокая масштабируемость и доступность. +- Простая интеграция с фронтендом и API. +- Автоматическое резервное копирование в облаке. +- Можно работать с несколькими клиентами одновременно. + +**Минусы:** +- Может быть дороже (облачные сервисы). +- Требует сетевого подключения. +- Сложнее отлаживать локально. +### nosql +**Плюсы:** +- Высокая масштабируемость и доступность. +- Простая интеграция с фронтендом и API. +- Автоматическое резервное копирование в облаке. +- Можно работать с несколькими клиентами одновременно. + +**Минусы:** +- Может быть дороже (облачные сервисы). +- Требует сетевого подключения. +- Сложнее отлаживать локально. + +### Проблема гонки пока есть, её надо решать, если приложение будет использоваться несколькими клиентами одновременно. \ No newline at end of file From e9e128e74484d6d618c0a6c2a4cb803fdf0f3d97 Mon Sep 17 00:00:00 2001 From: pc in honor of Ross Date: Mon, 10 Nov 2025 16:57:01 +0300 Subject: [PATCH 5/5] =?UTF-8?q?fixes=20and=203=20=D0=B7=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/basehttp.py | 26 +++++++++ simple_backend/src/task_tracker/clients.py | 59 +++++++++------------ simple_backend/src/task_tracker/main.py | 8 +-- simple_backend/src/task_tracker/models.py | 5 +- simple_backend/src/task_tracker/storage.py | 42 +++++++-------- 5 files changed, 75 insertions(+), 65 deletions(-) create mode 100644 simple_backend/src/task_tracker/basehttp.py diff --git a/simple_backend/src/task_tracker/basehttp.py b/simple_backend/src/task_tracker/basehttp.py new file mode 100644 index 00000000..7ad24725 --- /dev/null +++ b/simple_backend/src/task_tracker/basehttp.py @@ -0,0 +1,26 @@ +import requests + +class BaseHTTPClient: + def __init__(self, base_url: str, headers: dict | None = None): + self.base_url = base_url.rstrip("/") + self.headers = headers or {} + + def get(self, endpoint: str = "", **kwargs): + url = f"{self.base_url}/{endpoint.lstrip('/')}" + res = requests.get(url, headers=self.headers, **kwargs) + res.raise_for_status() + return res.json() + + def post(self, endpoint: str = "", json: dict | None = None, **kwargs): + url = f"{self.base_url}/{endpoint.lstrip('/')}" + res = requests.post(url, headers=self.headers, json=json, **kwargs) + res.raise_for_status() + return res.json() + + def put(self, endpoint: str = "", json: dict | None = None, **kwargs): + url = f"{self.base_url}/{endpoint.lstrip('/')}" + res = requests.put(url, headers=self.headers, json=json, **kwargs) + res.raise_for_status() + return res.json() + + diff --git a/simple_backend/src/task_tracker/clients.py b/simple_backend/src/task_tracker/clients.py index 69c01cae..d52683c1 100644 --- a/simple_backend/src/task_tracker/clients.py +++ b/simple_backend/src/task_tracker/clients.py @@ -1,40 +1,31 @@ -import requests -from loguru import logger - -class CloudFlareClient: +from basehttp import BaseHTTPClient +class CloudFlareClient(BaseHTTPClient): inputs = [ - {"role": "system", "content": - "Отвечай по-русски, кратко (1–2 предложения)," - "как лучше выполнить задачу из списка дел. Без приветствий и воды." - "Отвечай естественно, без вводных фраз вроде «Для выполнения задачи…», «Чтобы сделать…», «Следует…»." - "Сразу переходи к сути," - "Если в задаче есть действие, пиши как" - " будто даёшь понятную инструкцию или совет, а не академическое объяснение"} ] - - def __init__(self, api_token, account_id): - self.api_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/" - self.headers = { - "Authorization": f"Bearer {api_token}", - "Content-Type": "application/json" - } + {"role": "system", "content": + "Отвечай по-русски, кратко (1–2 предложения)," + "как лучше выполнить задачу из списка дел. Без приветствий и воды." + "Отвечай естественно, без вводных фраз вроде «Для выполнения задачи…», «Чтобы сделать…», «Следует…»." + "Сразу переходи к сути," + "Если в задаче есть действие, пиши как" + " будто даёшь понятную инструкцию или совет, а не академическое объяснение"} + ] + def __init__(self, api_token: str, account_id: str): + super().__init__( + base_url=f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run", + headers={ + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json" + } + ) - def generate_answer(self,task): - data = {"messages": CloudFlareClient.inputs + [{"role": "user", "content": task}]} - try: - response = requests.post(f"{self.api_url}@cf/meta/llama-3-8b-instruct", headers=self.headers, json=data) - response.raise_for_status() - result = response.json() - response_text = result["result"]["response"] + def generate_answer(self, task: str) -> str: + data = {"messages": self.inputs + [{"role": "user", "content": task}]} + result = self.post("@cf/meta/llama-3-8b-instruct", json=data) + response_text = result["result"]["response"] - if not response_text: - raise ValueError("Пустой или некорректный ответ от CloudFlare AI") - return response_text + if not response_text: + raise ValueError("Пустой или некорректный ответ от CloudFlare AI") - except requests.exceptions.HTTPError as e: - logger.error(f'Ошибка при запросе к Cloudflare AI: {e}') - raise - except requests.exceptions.ConnectionError as e: - logger.error(f'Ошибка соединения Cloudflare AI: {e}') - raise \ No newline at end of file + return response_text diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 939cf0cf..1fe4c10f 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,8 +1,8 @@ -from fastapi import FastAPI, Request +from fastapi import FastAPI from fastapi.responses import JSONResponse from pydantic import ValidationError from storage import JSONStorage, CloudJSONStorage -from models import Task, TaskCreate +from models import Task from config import settings from clients import CloudFlareClient from typing import List @@ -31,14 +31,14 @@ def get_tasks(): return storage.list_tasks() @app.post("/tasks", response_model=Task) -def create_task(task_data: TaskCreate): +def create_task(task_data: Task): ai_reply = client_ai.generate_answer(task_data.title) task_data.title = f"{task_data.title} — {ai_reply}" task = storage.create_task(task_data) return task @app.put("/tasks/{task_id}", response_model=Task) -def update_task(task_id: int, task_data: TaskCreate): +def update_task(task_id: int, task_data: Task): ai_reply = client_ai.generate_answer(task_data.title) task_data.title = f"{task_data.title} — {ai_reply}" task = storage.update_task(task_id, task_data) diff --git a/simple_backend/src/task_tracker/models.py b/simple_backend/src/task_tracker/models.py index bd32e822..a545664f 100644 --- a/simple_backend/src/task_tracker/models.py +++ b/simple_backend/src/task_tracker/models.py @@ -1,9 +1,8 @@ from pydantic import BaseModel, Field -class TaskCreate(BaseModel): +class Task(BaseModel): title: str = Field(..., min_length=5, max_length=300, description="Название задачи") status: bool = False + id: int -class Task(TaskCreate): - id: int \ No newline at end of file diff --git a/simple_backend/src/task_tracker/storage.py b/simple_backend/src/task_tracker/storage.py index bdf2eee4..c7ed54f8 100644 --- a/simple_backend/src/task_tracker/storage.py +++ b/simple_backend/src/task_tracker/storage.py @@ -1,7 +1,7 @@ import json from pathlib import Path -import requests -from models import Task, TaskCreate +from models import Task +from basehttp import BaseHTTPClient class IsMemoryStorage: @@ -65,20 +65,20 @@ def get_task(self, task_id: int) -> Task: raise ValueError(f"Task with id={task_id} not found") - def create_task(self, task_data: TaskCreate) -> Task: + def create_task(self, task_data: Task) -> Task: tasks = self._load() new_id = max([t.id for t in tasks], default=0) + 1 - task = Task(id=new_id, **task_data.model_dump()) + task = Task(id=new_id, **task_data.model_dump(exclude={'id'})) tasks.append(task) self._save(tasks) return task - def update_task(self, task_id: int, task_data: TaskCreate) -> Task: + def update_task(self, task_id: int, task_data: Task) -> Task: tasks = self._load() task = next((t for t in tasks if t.id == task_id), None) if not task: raise ValueError(f"Task with id={task_id} not found") - updated_task = task.model_copy(update=task_data.model_dump()) + updated_task = task.model_copy(update=task_data.model_dump(exclude={'id'})) tasks[tasks.index(task)] = updated_task self._save(tasks) return updated_task @@ -92,27 +92,21 @@ def delete_task(self, task_id: int) -> Task: self._save(tasks) return task -class CloudJSONStorage(JSONStorage): +class CloudJSONStorage(BaseHTTPClient, JSONStorage): def __init__(self, bin_id: str, master_key: str): - super().__init__(filepath="tasks.json") - self.base_url = f"https://api.jsonbin.io/v3/b/{bin_id}" - self.headers = { - "X-Master-Key": master_key, - "Content-Type": "application/json", - } + super().__init__( + base_url=f"https://api.jsonbin.io/v3/b/{bin_id}", + headers={ + "X-Master-Key": master_key, + "Content-Type": "application/json", + } + ) def _load(self) -> list[Task]: - res = requests.get(f"{self.base_url}/latest", headers=self.headers) - res.raise_for_status() - - record = res.json().get("record", {}) + record = self.get("latest").get("record", {}) tasks_data = record.get("tasks", []) - - tasks = [Task(**task) for task in tasks_data] - return tasks + return [Task(**task) for task in tasks_data] def _save(self, tasks: list[Task]) -> dict: - payload = {"tasks": [task.dict() for task in tasks]} - res = requests.put(self.base_url, headers=self.headers, json=payload) - res.raise_for_status() - return res.json() + payload = {"tasks": [task.model_dump() for task in tasks]} + return self.put("", json=payload) \ No newline at end of file