Skip to content

Добавил новый метод /ai/check #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
165 changes: 165 additions & 0 deletions examples/ai_check.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "# Проверка написан ли текст ИИ",
"id": "ce3ce21d4dde482a"
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-03-30T20:30:23.550263Z",
"start_time": "2025-03-30T20:30:21.095710Z"
}
},
"cell_type": "code",
"source": [
"import getpass\n",
"import os\n",
"\n",
"if \"GIGACHAT_CREDENTIALS\" not in os.environ:\n",
" os.environ[\"GIGACHAT_CREDENTIALS\"] = getpass.getpass(\"Credentials от GigaChat\")"
],
"id": "7e9f2b85999c203",
"outputs": [],
"execution_count": 1
},
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2025-03-30T20:30:44.047339Z",
"start_time": "2025-03-30T20:30:44.044387Z"
}
},
"source": [
"from gigachat import GigaChat\n",
"\n",
"client = GigaChat(\n",
" scope=\"GIGACHAT_API_CORP\",\n",
" verify_ssl_certs=False\n",
")"
],
"outputs": [],
"execution_count": 3
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-03-30T20:31:15.829824Z",
"start_time": "2025-03-30T20:31:12.929914Z"
}
},
"cell_type": "code",
"source": [
"text = \"\"\"Котики — это милые и пушистые домашние животные, которые относятся к семейству кошачьих. \n",
"Они известны своим дружелюбным характером, игривостью и мягкой шерстью. \n",
"Котики могут быть разных пород, каждая из которых имеет свои уникальные особенности. \n",
"А этот текст писала точно не нейросеть, \n",
"потому что это пишу я. У меня особо нечего сказать про котов,\n",
"кроме того, что они мягкие и пушистые, мурчащие существа.\n",
"Эти животные стали популярными благодаря своей способности поднимать настроение людям \n",
"и создавать уютную атмосферу в доме.\"\"\"\n",
"ai_result = client.check_ai(text, \"GigaCheckDetection\")\n",
"ai_result"
],
"id": "d1413eece5acabe0",
"outputs": [
{
"data": {
"text/plain": [
"AICheckResult(category='mixed', characters=533, tokens=224, ai_intervals=[[1, 262], [415, 533]])"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"execution_count": 4
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-03-30T20:32:53.737719Z",
"start_time": "2025-03-30T20:32:53.733482Z"
}
},
"cell_type": "code",
"source": [
"from IPython.display import display, HTML\n",
"\n",
"def ai_intervals_to_html(text, ai_intervals):\n",
" \"\"\"\n",
" Функция получает строку `text` и список интервалов в виде [(start, end), ...],\n",
" где части текста, попадающие в интервалы, будут выделены красным цветом, а остальные – чёрным.\n",
" Если список интервалов пустой, возвращается исходный текст.\n",
" \"\"\"\n",
" # Если интервалов нет, просто возвращаем текст, обернутый в span с чёрным цветом.\n",
" if not ai_intervals:\n",
" return f\"<span style='color: black'>{text}</span>\"\n",
" \n",
" output = \"\"\n",
" # Если до первого интервала есть текст, добавляем его чёрным цветом.\n",
" if ai_intervals[0][0] > 0:\n",
" output += f\"<span style='color: black'>{text[:ai_intervals[0][0]]}</span>\"\n",
" \n",
" for i, (start, end) in enumerate(ai_intervals):\n",
" # Если между интервалами есть промежуток > 1 символа, добавляем его чёрным цветом.\n",
" if i > 0 and (start - ai_intervals[i-1][1] > 1):\n",
" output += f\"<span style='color: black'>{text[ai_intervals[i-1][1]:start]}</span>\"\n",
" # Добавляем выделенный красным цветом фрагмент.\n",
" output += f\"<span style='color: red'>{text[start:end]}</span>\"\n",
" \n",
" # Если после последнего интервала остаётся текст, добавляем его чёрным цветом.\n",
" if ai_intervals[-1][1] < len(text):\n",
" output += f\"<span style='color: black'>{text[ai_intervals[-1][1]:]}</span>\"\n",
" \n",
" return output.replace(\"\\n\", \"<br/>\")\n",
"\n",
"html = ai_intervals_to_html(text, ai_result.ai_intervals)\n",
"display(HTML(f\"<div style='background:white'>{html}</div>\"))\n"
],
"id": "92cc36f62ee740e0",
"outputs": [
{
"data": {
"text/plain": [
"<IPython.core.display.HTML object>"
],
"text/html": [
"<div style='background:white'><span style='color: black'>К</span><span style='color: red'>отики — это милые и пушистые домашние животные, которые относятся к семейству кошачьих. <br/>Они известны своим дружелюбным характером, игривостью и мягкой шерстью. <br/>Котики могут быть разных пород, каждая из которых имеет свои уникальные особенности. <br/>А этот текст </span><span style='color: black'>писала точно не нейросеть, <br/>потому что это пишу я. У меня особо нечего сказать про котов,<br/>кроме того, что они мягкие и пушистые, мурчащие существа.<br/>Эти ж</span><span style='color: red'>ивотные стали популярными благодаря своей способности поднимать настроение людям <br/>и создавать уютную атмосферу в доме.</span></div>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"execution_count": 7
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
46 changes: 46 additions & 0 deletions src/gigachat/api/post_ai_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import Any, Dict, Optional

import httpx

from gigachat.api.utils import build_headers, build_response
from gigachat.models import AICheckResult


def _get_kwargs(
*,
input_: str,
model: str,
access_token: Optional[str] = None,
) -> Dict[str, Any]:
headers = build_headers(access_token)

return {
"method": "POST",
"url": "/ai/check",
"json": {"input": input_, "model": model},
"headers": headers,
}


def sync(
client: httpx.Client,
*,
input_: str,
model: str,
access_token: Optional[str] = None,
) -> AICheckResult:
kwargs = _get_kwargs(input_=input_, model=model, access_token=access_token)
response = client.request(**kwargs)
return build_response(response, AICheckResult)


async def asyncio(
client: httpx.AsyncClient,
*,
input_: str,
model: str,
access_token: Optional[str] = None,
) -> AICheckResult:
kwargs = _get_kwargs(input_=input_, model=model, access_token=access_token)
response = await client.request(**kwargs)
return build_response(response, AICheckResult)
16 changes: 16 additions & 0 deletions src/gigachat/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
get_image,
get_model,
get_models,
post_ai_check,
post_auth,
post_chat,
post_embeddings,
Expand All @@ -41,6 +42,7 @@
from gigachat.exceptions import AuthenticationError
from gigachat.models import (
AccessToken,
AICheckResult,
Balance,
Chat,
ChatCompletion,
Expand Down Expand Up @@ -328,6 +330,12 @@ def openapi_function_convert(self, openapi_function: str) -> OpenApiFunctions:
)
)

def check_ai(self, text: str, model: str) -> AICheckResult:
"""Проверяет переданный текст на наличие содержимого, сгенерированного с помощью нейросетевых моделей."""
return self._decorator(
lambda: post_ai_check.sync(self._client, input_=text, model=model, access_token=self.token)
)

def stream(self, payload: Union[Chat, Dict[str, Any], str]) -> Iterator[ChatCompletionChunk]:
"""Возвращает ответ модели с учетом переданных сообщений"""
chat = _parse_chat(payload, self._settings)
Expand Down Expand Up @@ -518,6 +526,14 @@ async def _acall() -> OpenApiFunctions:

return await self._adecorator(_acall)

async def acheck_ai(self, text: str, model: str) -> AICheckResult:
"""Проверяет переданный текст на наличие содержимого, сгенерированного с помощью нейросетевых моделей."""

async def _acall() -> AICheckResult:
return await post_ai_check.asyncio(self._aclient, input_=text, model=model, access_token=self.token)

return await self._adecorator(_acall)

async def astream(self, payload: Union[Chat, Dict[str, Any], str]) -> AsyncIterator[ChatCompletionChunk]:
"""Возвращает ответ модели с учетом переданных сообщений"""
chat = _parse_chat(payload, self._settings)
Expand Down
2 changes: 2 additions & 0 deletions src/gigachat/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from gigachat.models.access_token import AccessToken
from gigachat.models.ai_check_result import AICheckResult
from gigachat.models.balance import Balance
from gigachat.models.chat import Chat
from gigachat.models.chat_completion import ChatCompletion
Expand Down Expand Up @@ -29,6 +30,7 @@

__all__ = (
"AccessToken",
"AICheckResult",
"Balance",
"Chat",
"ChatCompletion",
Expand Down
19 changes: 19 additions & 0 deletions src/gigachat/models/ai_check_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import List, Literal, Optional

from gigachat.pydantic_v1 import BaseModel


class AICheckResult(BaseModel):
"""Ответ модели"""

category: Literal["ai", "human", "mixed"]
"""Результат проверки текста. Возможные значения: [ai, human, mixed]"""
characters: int
"""Количество символов в переданном тексте"""
tokens: int
"""Количество токенов в переданном тексте"""
ai_intervals: Optional[List[List[int]]]
"""
Части текста, сгенерированные моделью.
Обозначаются индексами символов, с которых начинаются и заканчиваются сгенерированные фрагменты.
"""
15 changes: 15 additions & 0 deletions tests/data/ai_check.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"category": "mixed",
"characters": 533,
"tokens": 224,
"ai_intervals": [
[
1,
262
],
[
415,
533
]
]
}
20 changes: 20 additions & 0 deletions tests/unit_tests/gigachat/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
)
from gigachat.exceptions import AuthenticationError
from gigachat.models import (
AICheckResult,
Balance,
Chat,
ChatCompletion,
Expand Down Expand Up @@ -51,6 +52,7 @@
GET_FILE_URL = f"{BASE_URL}/files/1"
GET_FILES_URL = f"{BASE_URL}/files"
FILE_DELETE_URL = f"{BASE_URL}/files/1/delete"
AI_CHECK_URL = f"{BASE_URL}/ai/check"

ACCESS_TOKEN = get_json("access_token.json")
TOKEN = get_json("token.json")
Expand All @@ -69,6 +71,7 @@
GET_FILE = get_json("get_file.json")
GET_FILES = get_json("get_files.json")
FILE_DELETE = get_json("post_files_delete.json")
AI_CHECK = get_json("ai_check.json")

FILE = get_bytes("image.jpg")

Expand Down Expand Up @@ -461,6 +464,14 @@ def test_delete_file(httpx_mock: HTTPXMock) -> None:
assert isinstance(response, DeletedFile)


def test_check_ai(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url=AI_CHECK_URL, json=AI_CHECK)

with GigaChatSyncClient(base_url=BASE_URL) as client:
response = client.check_ai(text="", model="")
assert isinstance(response, AICheckResult)


@pytest.mark.asyncio()
async def test_aget_models(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url=MODELS_URL, json=MODELS)
Expand Down Expand Up @@ -744,3 +755,12 @@ async def test_adelete_file(httpx_mock: HTTPXMock) -> None:
async with GigaChatAsyncClient(base_url=BASE_URL) as client:
response = await client.adelete_file(file="1")
assert isinstance(response, DeletedFile)


@pytest.mark.asyncio()
async def test_acheck_ai(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(url=AI_CHECK_URL, json=AI_CHECK)

async with GigaChatAsyncClient(base_url=BASE_URL) as client:
response = await client.acheck_ai(text="", model="")
assert isinstance(response, AICheckResult)