muryshev commited on
Commit
2ccde67
·
1 Parent(s): be938bc

Добавлен токенизатор для корректной обрезки запроса.

Browse files
Files changed (6) hide show
  1. Dockerfile +20 -8
  2. llm/common.py +2 -0
  3. llm/deepinfra_api.py +33 -2
  4. llm/vllm_api.py +1 -1
  5. main.py +61 -3
  6. prompts/gettable.py +46 -43
Dockerfile CHANGED
@@ -3,15 +3,14 @@ FROM python:3.10-slim-bullseye
3
 
4
  # Set Python to use unbuffered mode
5
  ENV PYTHONUNBUFFERED=1
6
-
7
  ENV PATH="/var/www/.local/bin:${PATH}"
8
 
9
  # Create a non-root user
10
  RUN useradd -m -u 1000 -U -s /bin/bash myuser
11
 
12
- # Install dependencies
13
- RUN apt-get update && \
14
- apt-get install -y --no-install-recommends python3-pip python3-dev && \
15
  rm -rf /var/lib/apt/lists/*
16
 
17
  # Set the working directory in the container
@@ -26,12 +25,25 @@ RUN chown -R myuser:myuser /var/www
26
  USER myuser
27
 
28
  # Copy the current directory contents into the container at /var/www
29
- COPY . /var/www
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- RUN pip install --user -r requirements.txt
 
 
32
 
33
  # Expose the port
34
- EXPOSE 7860
35
 
36
  # Run FastAPI app with Uvicorn
37
- CMD uvicorn main:app --host 0.0.0.0 --port 7860
 
3
 
4
  # Set Python to use unbuffered mode
5
  ENV PYTHONUNBUFFERED=1
 
6
  ENV PATH="/var/www/.local/bin:${PATH}"
7
 
8
  # Create a non-root user
9
  RUN useradd -m -u 1000 -U -s /bin/bash myuser
10
 
11
+ # Install system dependencies
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ python3-pip python3-dev git && \
14
  rm -rf /var/lib/apt/lists/*
15
 
16
  # Set the working directory in the container
 
25
  USER myuser
26
 
27
  # Copy the current directory contents into the container at /var/www
28
+ COPY --chown=myuser:myuser . /var/www
29
+
30
+ # Install dependencies
31
+ RUN pip install --no-cache-dir -r requirements.txt && \
32
+ pip install --no-cache-dir transformers sentencepiece
33
+
34
+ # Define tokenizer name
35
+ ARG TOKENIZER_NAME=unsloth/Llama-3.3-70B-Instruct
36
+ ENV TOKENIZER_NAME=${TOKENIZER_NAME}
37
+
38
+ ARG APP_PORT=7860
39
+ ENV APP_PORT=${APP_PORT}
40
 
41
+ # Download the tokenizer and store it in the image
42
+ RUN python -c "from transformers import AutoTokenizer; \
43
+ AutoTokenizer.from_pretrained('${TOKENIZER_NAME}')"
44
 
45
  # Expose the port
46
+ EXPOSE ${APP_PORT}
47
 
48
  # Run FastAPI app with Uvicorn
49
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", APP_PORT]
llm/common.py CHANGED
@@ -31,11 +31,13 @@ class LlmParams(BaseModel):
31
  """
32
  url: str
33
  model: Optional[str] = Field(None, description="Предполагается, что для локального API этот параметр не будет указываться, т.к. будем брать первую модель из списка потому, что модель доступна всего одна. Для deepinfra такой подход не подойдет и модель нужно задавать явно.")
 
34
  type: Optional[str] = None
35
  default: Optional[bool] = None
36
  template: Optional[str] = None
37
  predict_params: Optional[LlmPredictParams] = None
38
  api_key: Optional[str] = None
 
39
 
40
  class LlmApiProtocol(Protocol):
41
  async def tokenize(self, prompt: str) -> Optional[dict]:
 
31
  """
32
  url: str
33
  model: Optional[str] = Field(None, description="Предполагается, что для локального API этот параметр не будет указываться, т.к. будем брать первую модель из списка потому, что модель доступна всего одна. Для deepinfra такой подход не подойдет и модель нужно задавать явно.")
34
+ tokenizer: Optional[str] = Field(None, description="При использовании стороннего API, не поддерживающего токенизацию, будет использован AutoTokenizer для модели из этого поля. Используется в случае, если название модели в API не совпадает с оригинальным названием на Huggingface.")
35
  type: Optional[str] = None
36
  default: Optional[bool] = None
37
  template: Optional[str] = None
38
  predict_params: Optional[LlmPredictParams] = None
39
  api_key: Optional[str] = None
40
+ context_length: Optional[int] = None
41
 
42
  class LlmApiProtocol(Protocol):
43
  async def tokenize(self, prompt: str) -> Optional[dict]:
llm/deepinfra_api.py CHANGED
@@ -1,6 +1,7 @@
1
  import json
2
  from typing import Optional, List
3
  import httpx
 
4
  from llm.common import LlmParams, LlmApi
5
 
6
  class DeepInfraApi(LlmApi):
@@ -11,6 +12,9 @@ class DeepInfraApi(LlmApi):
11
  def __init__(self, params: LlmParams):
12
  super().__init__()
13
  super().set_params(params)
 
 
 
14
 
15
  async def get_models(self) -> List[str]:
16
  """
@@ -70,10 +74,37 @@ class DeepInfraApi(LlmApi):
70
  return actual_prompt
71
 
72
  async def tokenize(self, prompt: str) -> Optional[dict]:
73
- raise NotImplementedError("This function is not supported.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  async def detokenize(self, tokens: List[int]) -> Optional[str]:
76
- raise NotImplementedError("This function is not supported.")
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  async def create_request(self, prompt: str, system_prompt: str = None) -> dict:
79
  """
 
1
  import json
2
  from typing import Optional, List
3
  import httpx
4
+ from transformers import AutoTokenizer
5
  from llm.common import LlmParams, LlmApi
6
 
7
  class DeepInfraApi(LlmApi):
 
12
  def __init__(self, params: LlmParams):
13
  super().__init__()
14
  super().set_params(params)
15
+ print('Tokenizer initialization.')
16
+ self.tokenizer = AutoTokenizer.from_pretrained(params.tokenizer if params.tokenizer is not None else params.model)
17
+ print(f"Tokenizer initialized for model {params.model}.")
18
 
19
  async def get_models(self) -> List[str]:
20
  """
 
74
  return actual_prompt
75
 
76
  async def tokenize(self, prompt: str) -> Optional[dict]:
77
+ """
78
+ Токенизирует входной текстовый промпт.
79
+
80
+ Args:
81
+ prompt (str): Текст, который нужно токенизировать.
82
+ Returns:
83
+ dict: Словарь с токенами и их количеством или None в случае ошибки.
84
+ """
85
+ try:
86
+ tokens = self.tokenizer.encode(prompt, add_special_tokens=True)
87
+
88
+ return {"result": tokens, "num_tokens": len(tokens), "max_length": self.params.context_length}
89
+ except Exception as e:
90
+ print(f"Tokenization error: {e}")
91
+ return None
92
 
93
  async def detokenize(self, tokens: List[int]) -> Optional[str]:
94
+ """
95
+ Детокенизирует список токенов обратно в строку.
96
+
97
+ Args:
98
+ tokens (List[int]): Список токенов, который нужно преобразовать в текст.
99
+ Returns:
100
+ str: Восстановленный текст или None в случае ошибки.
101
+ """
102
+ try:
103
+ text = self.tokenizer.decode(tokens, skip_special_tokens=True)
104
+ return text
105
+ except Exception as e:
106
+ print(f"Detokenization error: {e}")
107
+ return None
108
 
109
  async def create_request(self, prompt: str, system_prompt: str = None) -> dict:
110
  """
llm/vllm_api.py CHANGED
@@ -109,7 +109,7 @@ class LlmApi(LlmApi):
109
  if response.status_code == 200:
110
  data = response.json()
111
  if "tokens" in data:
112
- return {"tokens": data["tokens"], "maxLength": data.get("max_model_len")}
113
  elif response.status_code == 404:
114
  print("Tokenization endpoint not found (404).")
115
  else:
 
109
  if response.status_code == 200:
110
  data = response.json()
111
  if "tokens" in data:
112
+ return {"tokens": data["tokens"], "max_length": data.get("max_model_len")}
113
  elif response.status_code == 404:
114
  print("Tokenization endpoint not found (404).")
115
  else:
main.py CHANGED
@@ -17,14 +17,17 @@ load_dotenv()
17
  LLM_API_URL = os.getenv("LLM_API_URL", "https://api.deepinfra.com")
18
  LLM_API_KEY = os.getenv("DEEPINFRA_API_KEY", "")
19
  LLM_NAME = os.getenv("LLM_NAME", "meta-llama/Llama-3.3-70B-Instruct-Turbo")
 
20
 
21
  default_llm_params = LlmParams(
22
  url=LLM_API_URL,
23
  api_key=LLM_API_KEY,
24
  model=LLM_NAME,
 
 
25
  predict_params=LlmPredictParams(
26
  temperature=0.15, top_p=0.95, min_p=0.05, seed=42,
27
- repetition_penalty=1.2, presence_penalty=1.1, max_tokens=6000
28
  )
29
  )
30
  llm_api = DeepInfraApi(default_llm_params)
@@ -42,10 +45,58 @@ class TextRequest(BaseModel):
42
  text: str
43
  projects: list[str] = []
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  @app.post("/extracttable")
46
  async def extracttable_route(request: TextRequest):
47
  prompt = gettable.USER_PROMPT.format(query=request.text)
48
- response = await llm_api.predict(prompt[:150000], system_prompt=gettable.SYSTEM_PROMPT)
 
 
49
 
50
  result = {"response": None, "error": None, "raw": response}
51
 
@@ -63,6 +114,12 @@ async def extracttable_route(request: TextRequest):
63
  try:
64
  result["response"] = json.loads(json_str)
65
  result["raw"] = prefix.strip()
 
 
 
 
 
 
66
  except json.JSONDecodeError as e:
67
  result["error"] = f"Ошибка декодирования JSON: {e}"
68
 
@@ -73,7 +130,8 @@ def health():
73
  return {"status": "ok"}
74
 
75
  async def generate_response(prompt):
76
- return await llm_api.predict(prompt[:150000])
 
77
 
78
  @app.post("/getsummary")
79
  async def getsummary_route(request: TextRequest):
 
17
  LLM_API_URL = os.getenv("LLM_API_URL", "https://api.deepinfra.com")
18
  LLM_API_KEY = os.getenv("DEEPINFRA_API_KEY", "")
19
  LLM_NAME = os.getenv("LLM_NAME", "meta-llama/Llama-3.3-70B-Instruct-Turbo")
20
+ TOKENIZER_NAME = os.getenv("TOKENIZER_NAME", "unsloth/Llama-3.3-70B-Instruct")
21
 
22
  default_llm_params = LlmParams(
23
  url=LLM_API_URL,
24
  api_key=LLM_API_KEY,
25
  model=LLM_NAME,
26
+ tokenizer=TOKENIZER_NAME,
27
+ context_length=130000,
28
  predict_params=LlmPredictParams(
29
  temperature=0.15, top_p=0.95, min_p=0.05, seed=42,
30
+ repetition_penalty=1.2, presence_penalty=1.1, n_predict=6000
31
  )
32
  )
33
  llm_api = DeepInfraApi(default_llm_params)
 
45
  text: str
46
  projects: list[str] = []
47
 
48
+ async def trim_prompt(prompt: str, system_prompt: str):
49
+
50
+ result = await llm_api.tokenize(prompt+system_prompt)
51
+ result_system = await llm_api.tokenize(system_prompt)
52
+
53
+ # в случае ошибки при токенизации, вернем исходную строку безопасной длины
54
+ if result["result"] is None or result_system is None:
55
+ return prompt[llm_api.params.context_length / 3]
56
+
57
+ #вероятно, часть уходит на форматирование чата, надо проверить
58
+ max_length = result["max_length"] - len(result["result"]) - len(result_system["result"]) - llm_api.params.predict_params.n_predict
59
+
60
+ detokenized_str = await llm_api.detokenize(result["result"][:max_length])
61
+
62
+ # в случае ошибки при детокенизации, вернем исходную строку безопасной длины
63
+ if detokenized_str is None:
64
+ return prompt[llm_api.params.context_length / 3]
65
+
66
+ return detokenized_str
67
+
68
+
69
+ def validate_json_format(data):
70
+ """ Проверяет, соответствует ли JSON ожидаемому формату. """
71
+ if not isinstance(data, list):
72
+ return "JSON должен быть списком объектов."
73
+
74
+ # Возможно, в дальнейшем стоит описать менее детально, пока так для отладки
75
+ for item in data:
76
+ if not isinstance(item, dict):
77
+ return "Элементы списка должны быть объектами (dict)."
78
+ if "name" not in item or "data" not in item:
79
+ return "Каждый объект должен содержать ключи 'name' и 'data'."
80
+ if not isinstance(item["name"], str):
81
+ return "'name' должен быть строкой."
82
+ if not isinstance(item["data"], dict):
83
+ return "'data' должен быть объектом (dict)."
84
+ if "columns" not in item["data"] or "rows" not in item["data"]:
85
+ return "'data' должен содержать 'columns' и 'rows'."
86
+ if not isinstance(item["data"]["columns"], list) or not all(isinstance(col, str) for col in item["data"]["columns"]):
87
+ return "'columns' должен быть списком строк."
88
+ if not isinstance(item["data"]["rows"], list) or not all(isinstance(row, list) for row in item["data"]["rows"]):
89
+ return "'rows' должен быть списком списков."
90
+
91
+ return None # Ошибок нет
92
+
93
+
94
  @app.post("/extracttable")
95
  async def extracttable_route(request: TextRequest):
96
  prompt = gettable.USER_PROMPT.format(query=request.text)
97
+ system_prompt=gettable.SYSTEM_PROMPT
98
+ prompt = await trim_prompt(prompt, system_prompt)
99
+ response = await llm_api.predict(prompt, system_prompt=system_prompt)
100
 
101
  result = {"response": None, "error": None, "raw": response}
102
 
 
114
  try:
115
  result["response"] = json.loads(json_str)
116
  result["raw"] = prefix.strip()
117
+ validation_error = validate_json_format(result["response"])
118
+ if validation_error:
119
+ result["error"] = validation_error
120
+ else:
121
+ result["response"] = result["response"]
122
+ result["raw"] = prefix.strip()
123
  except json.JSONDecodeError as e:
124
  result["error"] = f"Ошибка декодирования JSON: {e}"
125
 
 
130
  return {"status": "ok"}
131
 
132
  async def generate_response(prompt):
133
+ prompt = await trim_prompt(prompt)
134
+ return await llm_api.predict(prompt)
135
 
136
  @app.post("/getsummary")
137
  async def getsummary_route(request: TextRequest):
prompts/gettable.py CHANGED
@@ -33,7 +33,8 @@ SYSTEM_PROMPT="""
33
  (2) 'пункт 2'
34
  (3) 'пункт 3'
35
  (4) 'пункт 4'
36
- (5) JSON 'пункт 5'"
 
37
  ####
38
  Далее будет пример номер 1. Не используй данные из примера, он указывает только на логику твоей работы
39
  ####
@@ -47,25 +48,19 @@ SYSTEM_PROMPT="""
47
  (4) Заборный 6-723 - это судя по всему объект, что имеет статус 'реализация'. ДСГ 4 - это объект, что имеет длину 8 и ширину 2, которая измеряется в метрах. Штраб - что-то, что имеет напряжение 150. ЗТТ - аббревиатура, которая имеет в качестве параметров давление 1 - 150 и давление 2 - 206.4 (это с плавающей запятой значение). ЛКТР - это что-то, что находится в стадии 'в завершении'.
48
  (5) Нужно не забыть каждое числовое значение написать в числовом формате. И не забыть о показателях с точкой.
49
  (6) JSON
50
- {
51
- "Заборный 6-723":{
52
- "Статус": "реализация"
53
- },
54
- "ДСГ 4":{
55
- "Длина, в метрах":"8",
56
- "Ширина, в метрах":"2"
57
- },
58
- "Штраб":{
59
- "Напряжение":"150"
60
- },
61
- "ЗТТ":{
62
- "Давление 1":"150",
63
- "Давление 2":"206.4"
64
- },
65
- "ЛКТР":{
66
- "Статус":"в завершении"
67
- }
68
- }
69
  ####
70
  Далее будет пример номер 2. Не используй данные из примера, он указывает только на логику твоей работы
71
  ####
@@ -79,21 +74,15 @@ SYSTEM_PROMPT="""
79
  (4) В данной записи есть только объект "труба", у которого есть два экземпляра в массиве. Параметрами являются номер, длина, сечение и давление воды. Не ясно единиц измерения данных параметров. Обе трубы имеют номер по ГОСТ 3. Первая труба имеет длину 7, сечение 8 и давление 70.69 (это число с плавающей точкой). Вторая труба имеет длину 6, сечение как у первой 8, давление 106.
80
  (5) Нужно не забыть каждое числовое значение написать в числовом формате. И не забыть о показателях с точкой.
81
  (6) JSON
82
- {
83
- "Труба":[{
84
- "Номер по ГОСТ":"3",
85
- "Длина": "7",
86
- "Сечение":"8",
87
- "Давление":"70.69"
88
- },
89
- {
90
- "Номер по ГОСТ":"3",
91
- "Длина":"6",
92
- "Сечение":"8",
93
- "Давление":"106"
94
- }
95
- ]
96
- }
97
  ####
98
  Далее будет пример номер 3. Не используй данные из примера, он указывает только на логику твоей работы
99
  ####
@@ -107,17 +96,31 @@ SYSTEM_PROMPT="""
107
  (4) В данной записи есть параметр города цвет - белый. А также показатели температуры погоды, не понятна система отсчёта, но погода считается в градусах. Записано что погода -5. Количество детей 8 штук. И статус детей - они "играют в снежки".
108
  (5) Нужно не забыть каждое числовое значение написать в числовом формате.
109
  (6) JSON
110
- {
111
- "Город":{
112
- "Цвет": "белый"
 
 
 
 
113
  },
114
- "Погода":{
115
- "Температура, градус":"-5"
 
 
 
 
 
116
  },
117
- "Дети":{
118
- "Количество":"8"
119
- "Статус":"Играют в снежки"
 
 
 
 
120
  }
 
121
  ####
122
  Далее будет настоящая запись, которую требуется разобрать.
123
  ####
 
33
  (2) 'пункт 2'
34
  (3) 'пункт 3'
35
  (4) 'пункт 4'
36
+ (5) 'пункт 5'
37
+ (6) JSON 'пункт 6'"
38
  ####
39
  Далее будет пример номер 1. Не используй данные из примера, он указывает только на логику твоей работы
40
  ####
 
48
  (4) Заборный 6-723 - это судя по всему объект, что имеет статус 'реализация'. ДСГ 4 - это объект, что имеет длину 8 и ширину 2, которая измеряется в метрах. Штраб - что-то, что имеет напряжение 150. ЗТТ - аббревиатура, которая имеет в качестве параметров давление 1 - 150 и давление 2 - 206.4 (это с плавающей запятой значение). ЛКТР - это что-то, что находится в стадии 'в завершении'.
49
  (5) Нужно не забыть каждое числовое значение написать в числовом формате. И не забыть о показателях с точкой.
50
  (6) JSON
51
+ [{
52
+ name: "Датчики",
53
+ data: {
54
+ columns: ["Наименование", "Статус", "Длина, в метрах", "Ширина, в метрах", "Напряжение", "Давление 1", "Давление 2"],
55
+ rows: [
56
+ ["Заборный 6-723", "реализация", null, null, null, null, null],
57
+ ["ДСГ 4", null, 8, 2, null, null, null],
58
+ ["Штраб", null, null, null, 150, null, null],
59
+ ["ЗТТ", null, null, null, null, 150, 206.4],
60
+ ["ЛКТР", "в завершении", null, null, null, null, null]
61
+ ]
62
+ }
63
+ }]
 
 
 
 
 
 
64
  ####
65
  Далее будет пример номер 2. Не используй данные из примера, он указывает только на логику твоей работы
66
  ####
 
74
  (4) В данной записи есть только объект "труба", у которого есть два экземпляра в массиве. Параметрами являются номер, длина, сечение и давление воды. Не ясно единиц измерения данных параметров. Обе трубы имеют номер по ГОСТ 3. Первая труба имеет длину 7, сечение 8 и давление 70.69 (это число с плавающей точкой). Вторая труба имеет длину 6, сечение как у первой 8, давление 106.
75
  (5) Нужно не забыть каждое числовое значение написать в числовом формате. И не забыть о показателях с точкой.
76
  (6) JSON
77
+ [{
78
+ name: "Труба",
79
+ data: {
80
+ columns: ["Номер по ГОСТ", "Длина", "Сечение", "Давление"],
81
+ rows: [
82
+ [3, 7, 8, 70.69],
83
+ [3, 6, 8, 106],
84
+ ]
85
+ }]
 
 
 
 
 
 
86
  ####
87
  Далее будет пример номер 3. Не используй данные из примера, он указывает только на логику твоей работы
88
  ####
 
96
  (4) В данной записи есть параметр города цвет - белый. А также показатели температуры погоды, не понятна система отсчёта, но погода считается в градусах. Записано что погода -5. Количество детей 8 штук. И статус детей - они "играют в снежки".
97
  (5) Нужно не забыть каждое числовое значение написать в числовом формате.
98
  (6) JSON
99
+ [{
100
+ name: "Город",
101
+ data: {
102
+ columns: ["Цвет"],
103
+ rows: [
104
+ ["белый"]
105
+ ]
106
  },
107
+ {
108
+ name: "Погода",
109
+ data: {
110
+ columns: ["Температура, градус"],
111
+ rows: [
112
+ [-5]
113
+ ]
114
  },
115
+ {
116
+ name: "Дети",
117
+ data: {
118
+ columns: ["К��личество", "Статус"],
119
+ rows: [
120
+ [8, "Играют в снежки"]
121
+ ]
122
  }
123
+ ]
124
  ####
125
  Далее будет настоящая запись, которую требуется разобрать.
126
  ####