vishap-analyzer 5.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +0 -0
- src/api/__init__.py +2 -0
- src/api/auth.py +41 -0
- src/api/rate_limit.py +46 -0
- src/cli.py +79 -0
- src/config.py +39 -0
- src/llm/__init__.py +0 -0
- src/llm/async_client.py +44 -0
- src/llm/async_deepseek_client.py +54 -0
- src/llm/async_ollama_client.py +24 -0
- src/llm/client.py +44 -0
- src/llm/deepseek_client.py +58 -0
- src/llm/ollama_client.py +25 -0
- src/main.py +393 -0
- src/pipeline/__init__.py +0 -0
- src/pipeline/async_stages.py +107 -0
- src/pipeline/batch.py +59 -0
- src/pipeline/cost.py +31 -0
- src/pipeline/orchestrator.py +735 -0
- src/pipeline/prompts.py +31 -0
- src/pipeline/stages.py +226 -0
- src/storage/__init__.py +0 -0
- src/storage/history.py +64 -0
- src/storage/mode.py +38 -0
- src/tools/__init__.py +0 -0
- src/tools/analyze.py +311 -0
- src/tools/fallback_check.py +58 -0
- src/tools/patterns.py +230 -0
- src/tools/provenance.py +125 -0
- src/tools/redact.py +64 -0
- src/tools/sarif.py +153 -0
- src/tools/sast.py +146 -0
- src/tools/sca.py +68 -0
- src/tools/slice_input.py +77 -0
- src/ui/__init__.py +0 -0
- src/ui/streamlit_app.py +593 -0
- vishap_analyzer-5.8.0.dist-info/METADATA +848 -0
- vishap_analyzer-5.8.0.dist-info/RECORD +42 -0
- vishap_analyzer-5.8.0.dist-info/WHEEL +5 -0
- vishap_analyzer-5.8.0.dist-info/entry_points.txt +2 -0
- vishap_analyzer-5.8.0.dist-info/licenses/LICENSE +680 -0
- vishap_analyzer-5.8.0.dist-info/top_level.txt +1 -0
src/__init__.py
ADDED
|
File without changes
|
src/api/__init__.py
ADDED
src/api/auth.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""API-key authentication (dogfood P0 fix).
|
|
3
|
+
|
|
4
|
+
Dogfood finding: POST /analyze, /history/clear, /mode were open
|
|
5
|
+
to any client on 0.0.0.0:5001.
|
|
6
|
+
|
|
7
|
+
Behavior:
|
|
8
|
+
- If CONVEYOR_API_KEY is set in .env -> require X-API-Key header.
|
|
9
|
+
- If CONVEYOR_API_KEY is NOT set -> API works ONLY on 127.0.0.1
|
|
10
|
+
(see main.py uvicorn.run binding).
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
from fastapi import Header, HTTPException, status
|
|
14
|
+
|
|
15
|
+
API_KEY = os.getenv("CONVEYOR_API_KEY", "").strip()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def verify_api_key(x_api_key: str = Header(default="")) -> None:
|
|
19
|
+
"""FastAPI dependency: verify X-API-Key header.
|
|
20
|
+
|
|
21
|
+
If API_KEY is empty (dev mode), allow all (main.py binds to 127.0.0.1).
|
|
22
|
+
If API_KEY is set, require exact match.
|
|
23
|
+
"""
|
|
24
|
+
if not API_KEY:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
if not x_api_key:
|
|
28
|
+
raise HTTPException(
|
|
29
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
30
|
+
detail="X-API-Key header required",
|
|
31
|
+
)
|
|
32
|
+
if x_api_key != API_KEY:
|
|
33
|
+
raise HTTPException(
|
|
34
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
35
|
+
detail="Invalid API key",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_auth_enabled() -> bool:
|
|
40
|
+
"""True if CONVEYOR_API_KEY is set."""
|
|
41
|
+
return bool(API_KEY)
|
src/api/rate_limit.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Simple in-memory rate limiter (dogfood P0 fix).
|
|
3
|
+
|
|
4
|
+
Dogfood finding: no rate limiting on /analyze -> any client could
|
|
5
|
+
exhaust DeepSeek API budget.
|
|
6
|
+
|
|
7
|
+
Limits (per IP):
|
|
8
|
+
- 10 requests / minute for /analyze, /analyze/batch
|
|
9
|
+
- 30 requests / minute for /history, /mode
|
|
10
|
+
"""
|
|
11
|
+
import time
|
|
12
|
+
from collections import defaultdict, deque
|
|
13
|
+
from fastapi import Request, HTTPException, status
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_BUCKETS = defaultdict(deque)
|
|
17
|
+
|
|
18
|
+
LIMIT_ANALYZE = 10
|
|
19
|
+
LIMIT_READ = 30
|
|
20
|
+
WINDOW_SECONDS = 60
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _check(key: str, limit: int) -> None:
|
|
24
|
+
now = time.time()
|
|
25
|
+
bucket = _BUCKETS[key]
|
|
26
|
+
|
|
27
|
+
while bucket and bucket[0] < now - WINDOW_SECONDS:
|
|
28
|
+
bucket.popleft()
|
|
29
|
+
|
|
30
|
+
if len(bucket) >= limit:
|
|
31
|
+
raise HTTPException(
|
|
32
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
33
|
+
detail="Rate limit exceeded: {} req / {}s".format(limit, WINDOW_SECONDS),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
bucket.append(now)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def rate_limit_analyze(request: Request) -> None:
|
|
40
|
+
ip = request.client.host if request.client else "unknown"
|
|
41
|
+
_check("analyze:" + ip, LIMIT_ANALYZE)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def rate_limit_read(request: Request) -> None:
|
|
45
|
+
ip = request.client.host if request.client else "unknown"
|
|
46
|
+
_check("read:" + ip, LIMIT_READ)
|
src/cli.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Vishap CLI entry point.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
vishap analyze <file>
|
|
6
|
+
vishap info
|
|
7
|
+
vishap --version
|
|
8
|
+
"""
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from src.config import APP_NAME
|
|
14
|
+
from src.pipeline.orchestrator import run_pipeline
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_analyze(args):
|
|
18
|
+
"""Analyze a file."""
|
|
19
|
+
path = Path(args.input)
|
|
20
|
+
if not path.exists():
|
|
21
|
+
print(f"Error: {path} not found", file=sys.stderr)
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
material = path.read_text(encoding="utf-8", errors="replace")
|
|
25
|
+
result = run_pipeline(material, path.name, verbose=not args.quiet)
|
|
26
|
+
|
|
27
|
+
md = result.get("final_markdown", "")
|
|
28
|
+
audit = result.get("audit", {})
|
|
29
|
+
|
|
30
|
+
if args.output:
|
|
31
|
+
Path(args.output).write_text(md, encoding="utf-8")
|
|
32
|
+
print(f"Written: {args.output}")
|
|
33
|
+
else:
|
|
34
|
+
print(md)
|
|
35
|
+
print()
|
|
36
|
+
print(f"audit_id: {audit.get('audit_id')}")
|
|
37
|
+
print(f"material_hash: {audit.get('material_hash')}")
|
|
38
|
+
print(f"verdict: {audit.get('critique_verdict')} / {audit.get('risk_verdict')}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cmd_info(args):
|
|
42
|
+
"""Show info."""
|
|
43
|
+
print(f"{APP_NAME} V5.8 - 6-stage LLM pipeline")
|
|
44
|
+
print("Pipeline: Stage 0-5 (Orchestrator > Scanner > Analyst > Researcher > Synthesizer > Transmitter)")
|
|
45
|
+
print("Features: audit trail, PII redaction, compliance flags, SAST/SCA, SARIF, retry, fallback")
|
|
46
|
+
print()
|
|
47
|
+
print("Commands:")
|
|
48
|
+
print(" vishap analyze <file> - analyze a file")
|
|
49
|
+
print(" vishap info - this help")
|
|
50
|
+
print()
|
|
51
|
+
print("API: python -m src.main - FastAPI on :5001")
|
|
52
|
+
print("UI: streamlit run src/ui/streamlit_app.py - Streamlit on :8501")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
ap = argparse.ArgumentParser(prog="vishap", description=f"{APP_NAME} V5.8")
|
|
57
|
+
ap.add_argument("--version", action="version", version=f"{APP_NAME} V5.8")
|
|
58
|
+
|
|
59
|
+
sub = ap.add_subparsers(dest="command")
|
|
60
|
+
|
|
61
|
+
p_analyze = sub.add_parser("analyze", help="Analyze a file")
|
|
62
|
+
p_analyze.add_argument("input", help="Path to file")
|
|
63
|
+
p_analyze.add_argument("--quiet", action="store_true")
|
|
64
|
+
p_analyze.add_argument("--output", "-o", help="Output markdown path")
|
|
65
|
+
p_analyze.set_defaults(func=cmd_analyze)
|
|
66
|
+
|
|
67
|
+
p_info = sub.add_parser("info", help="Show info")
|
|
68
|
+
p_info.set_defaults(func=cmd_info)
|
|
69
|
+
|
|
70
|
+
args = ap.parse_args()
|
|
71
|
+
if not args.command:
|
|
72
|
+
ap.print_help()
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
args.func(args)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|
src/config.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Конфигурация Vishap.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
load_dotenv()
|
|
10
|
+
|
|
11
|
+
# === Пути ===
|
|
12
|
+
ROOT_DIR = Path(__file__).parent.parent
|
|
13
|
+
DATA_DIR = ROOT_DIR / "data"
|
|
14
|
+
PROMPTS_DIR = ROOT_DIR / "vishap"
|
|
15
|
+
TOOLS_DIR = ROOT_DIR / "src" / "tools"
|
|
16
|
+
|
|
17
|
+
# === DeepSeek API ===
|
|
18
|
+
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
|
|
19
|
+
DEEPSEEK_URL = "https://api.deepseek.com/v1/chat/completions"
|
|
20
|
+
DEEPSEEK_MODEL = "deepseek-chat"
|
|
21
|
+
|
|
22
|
+
# === Ollama (локально) ===
|
|
23
|
+
OLLAMA_MODEL = "qwen2.5:7b"
|
|
24
|
+
OLLAMA_URL = "http://localhost:11434"
|
|
25
|
+
|
|
26
|
+
# === Режим LLM ===
|
|
27
|
+
MODE = "auto"
|
|
28
|
+
|
|
29
|
+
# === Генерация ===
|
|
30
|
+
TEMPERATURE = 0.0
|
|
31
|
+
TEMPERATURE_CREATIVE = 0.3
|
|
32
|
+
MAX_TOKENS = 8000
|
|
33
|
+
|
|
34
|
+
# === Сервер ===
|
|
35
|
+
PORT = 5001
|
|
36
|
+
APP_NAME = "Vishap"
|
|
37
|
+
|
|
38
|
+
# === Данные ===
|
|
39
|
+
DATA_DIR.mkdir(exist_ok=True)
|
src/llm/__init__.py
ADDED
|
File without changes
|
src/llm/async_client.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Асинхронный клиент LLM с fallback:
|
|
4
|
+
cloud → DeepSeek API
|
|
5
|
+
local → Ollama
|
|
6
|
+
auto → облако, при ошибке → локально
|
|
7
|
+
"""
|
|
8
|
+
from src.storage.mode import get_mode
|
|
9
|
+
from src.llm import async_deepseek_client, async_ollama_client
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def async_chat(system: str, user: str,
|
|
13
|
+
temperature: float = 0.0,
|
|
14
|
+
max_tokens: int = 2000) -> dict:
|
|
15
|
+
"""Асинхронный вызов LLM. Аналог chat() с fallback."""
|
|
16
|
+
mode = get_mode()
|
|
17
|
+
|
|
18
|
+
if mode == "local":
|
|
19
|
+
result = await async_ollama_client.chat(system, user, temperature)
|
|
20
|
+
result["mode"] = "local"
|
|
21
|
+
return result
|
|
22
|
+
|
|
23
|
+
if mode == "cloud":
|
|
24
|
+
result = await async_deepseek_client.chat(system, user, temperature, max_tokens)
|
|
25
|
+
result["mode"] = "cloud"
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
# auto
|
|
29
|
+
try:
|
|
30
|
+
result = await async_deepseek_client.chat(system, user, temperature, max_tokens)
|
|
31
|
+
result["mode"] = "cloud"
|
|
32
|
+
return result
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"[Vishap] DeepSeek недоступен: {e}. Fallback → локально.")
|
|
35
|
+
try:
|
|
36
|
+
result = await async_ollama_client.chat(system, user, temperature)
|
|
37
|
+
result["mode"] = "local"
|
|
38
|
+
return result
|
|
39
|
+
except Exception as e2:
|
|
40
|
+
return {
|
|
41
|
+
"text": f"[Ошибка LLM] cloud: {e}; local: {e2}",
|
|
42
|
+
"tokens_in": 0, "tokens_out": 0,
|
|
43
|
+
"model": "none", "mode": "error",
|
|
44
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Асинхронный клиент DeepSeek API (через openai SDK)."""
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from openai import AsyncOpenAI, APIConnectionError, RateLimitError, APIStatusError
|
|
6
|
+
from tenacity import (
|
|
7
|
+
retry,
|
|
8
|
+
stop_after_attempt,
|
|
9
|
+
wait_exponential,
|
|
10
|
+
retry_if_exception_type,
|
|
11
|
+
before_sleep_log,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from src.config import DEEPSEEK_API_KEY, DEEPSEEK_MODEL
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_async_client() -> AsyncOpenAI:
|
|
20
|
+
if not DEEPSEEK_API_KEY:
|
|
21
|
+
raise ValueError("DEEPSEEK_API_KEY не задан в .env")
|
|
22
|
+
return AsyncOpenAI(
|
|
23
|
+
api_key=DEEPSEEK_API_KEY,
|
|
24
|
+
base_url="https://api.deepseek.com/v1",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@retry(
|
|
29
|
+
stop=stop_after_attempt(3),
|
|
30
|
+
wait=wait_exponential(multiplier=1, min=2, max=30),
|
|
31
|
+
retry=retry_if_exception_type((APIConnectionError, RateLimitError, APIStatusError)),
|
|
32
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
|
33
|
+
reraise=True,
|
|
34
|
+
)
|
|
35
|
+
async def chat(system: str, user: str,
|
|
36
|
+
temperature: float = 0.0,
|
|
37
|
+
max_tokens: int = 2000) -> dict:
|
|
38
|
+
"""Асинхронный вызов DeepSeek. Возвращает {text, tokens_in, tokens_out, model}."""
|
|
39
|
+
client = get_async_client()
|
|
40
|
+
response = await client.chat.completions.create(
|
|
41
|
+
model=DEEPSEEK_MODEL,
|
|
42
|
+
messages=[
|
|
43
|
+
{"role": "system", "content": system},
|
|
44
|
+
{"role": "user", "content": user},
|
|
45
|
+
],
|
|
46
|
+
temperature=temperature,
|
|
47
|
+
max_tokens=max_tokens,
|
|
48
|
+
)
|
|
49
|
+
return {
|
|
50
|
+
"text": response.choices[0].message.content,
|
|
51
|
+
"tokens_in": response.usage.prompt_tokens,
|
|
52
|
+
"tokens_out": response.usage.completion_tokens,
|
|
53
|
+
"model": DEEPSEEK_MODEL,
|
|
54
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Асинхронный клиент Ollama (локальный LLM)."""
|
|
3
|
+
import ollama
|
|
4
|
+
from src.config import OLLAMA_MODEL
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def chat(system: str, user: str,
|
|
8
|
+
temperature: float = 0.0) -> dict:
|
|
9
|
+
"""Асинхронный вызов Ollama. Возвращает {text, tokens_in, tokens_out, model}."""
|
|
10
|
+
client = ollama.AsyncClient()
|
|
11
|
+
response = await client.chat(
|
|
12
|
+
model=OLLAMA_MODEL,
|
|
13
|
+
messages=[
|
|
14
|
+
{"role": "system", "content": system},
|
|
15
|
+
{"role": "user", "content": user},
|
|
16
|
+
],
|
|
17
|
+
options={"temperature": temperature},
|
|
18
|
+
)
|
|
19
|
+
return {
|
|
20
|
+
"text": response["message"]["content"],
|
|
21
|
+
"tokens_in": 0,
|
|
22
|
+
"tokens_out": 0,
|
|
23
|
+
"model": OLLAMA_MODEL,
|
|
24
|
+
}
|
src/llm/client.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Единый клиент LLM с fallback:
|
|
4
|
+
cloud → DeepSeek API
|
|
5
|
+
local → Ollama
|
|
6
|
+
auto → облако, при ошибке → локально
|
|
7
|
+
"""
|
|
8
|
+
from src.storage.mode import get_mode
|
|
9
|
+
from src.llm import deepseek_client, ollama_client
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def chat(system: str, user: str,
|
|
13
|
+
temperature: float = 0.0,
|
|
14
|
+
max_tokens: int = 2000) -> dict:
|
|
15
|
+
"""Возвращает {text, tokens_in, tokens_out, model, mode}."""
|
|
16
|
+
mode = get_mode()
|
|
17
|
+
|
|
18
|
+
if mode == "local":
|
|
19
|
+
result = ollama_client.chat(system, user, temperature)
|
|
20
|
+
result["mode"] = "local"
|
|
21
|
+
return result
|
|
22
|
+
|
|
23
|
+
if mode == "cloud":
|
|
24
|
+
result = deepseek_client.chat(system, user, temperature, max_tokens)
|
|
25
|
+
result["mode"] = "cloud"
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
# auto
|
|
29
|
+
try:
|
|
30
|
+
result = deepseek_client.chat(system, user, temperature, max_tokens)
|
|
31
|
+
result["mode"] = "cloud"
|
|
32
|
+
return result
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"[Vishap] DeepSeek недоступен: {e}. Fallback → локально.")
|
|
35
|
+
try:
|
|
36
|
+
result = ollama_client.chat(system, user, temperature)
|
|
37
|
+
result["mode"] = "local"
|
|
38
|
+
return result
|
|
39
|
+
except Exception as e2:
|
|
40
|
+
return {
|
|
41
|
+
"text": f"[Ошибка LLM] cloud: {e}; local: {e2}",
|
|
42
|
+
"tokens_in": 0, "tokens_out": 0,
|
|
43
|
+
"model": "none", "mode": "error",
|
|
44
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Клиент DeepSeek API (через openai SDK).
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from openai import OpenAI, APIConnectionError, RateLimitError, APIStatusError
|
|
8
|
+
from tenacity import (
|
|
9
|
+
retry,
|
|
10
|
+
stop_after_attempt,
|
|
11
|
+
wait_exponential,
|
|
12
|
+
retry_if_exception_type,
|
|
13
|
+
before_sleep_log,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from src.config import DEEPSEEK_API_KEY, DEEPSEEK_URL, DEEPSEEK_MODEL
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_client() -> OpenAI:
|
|
22
|
+
if not DEEPSEEK_API_KEY:
|
|
23
|
+
raise ValueError("DEEPSEEK_API_KEY не задан в .env")
|
|
24
|
+
return OpenAI(
|
|
25
|
+
api_key=DEEPSEEK_API_KEY,
|
|
26
|
+
base_url="https://api.deepseek.com/v1",
|
|
27
|
+
timeout=60.0, # dogfood P0: HTTP timeout (was default 600s)
|
|
28
|
+
max_retries=0, # retries handled by tenacity, not openai
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@retry(
|
|
33
|
+
stop=stop_after_attempt(3),
|
|
34
|
+
wait=wait_exponential(multiplier=1, min=2, max=30),
|
|
35
|
+
retry=retry_if_exception_type((APIConnectionError, RateLimitError, APIStatusError, TimeoutError)),
|
|
36
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
|
37
|
+
reraise=True,
|
|
38
|
+
)
|
|
39
|
+
def chat(system: str, user: str,
|
|
40
|
+
temperature: float = 0.0,
|
|
41
|
+
max_tokens: int = 2000) -> dict:
|
|
42
|
+
"""Возвращает {text, tokens_in, tokens_out, model}."""
|
|
43
|
+
client = get_client()
|
|
44
|
+
response = client.chat.completions.create(
|
|
45
|
+
model=DEEPSEEK_MODEL,
|
|
46
|
+
messages=[
|
|
47
|
+
{"role": "system", "content": system},
|
|
48
|
+
{"role": "user", "content": user},
|
|
49
|
+
],
|
|
50
|
+
temperature=temperature,
|
|
51
|
+
max_tokens=max_tokens,
|
|
52
|
+
)
|
|
53
|
+
return {
|
|
54
|
+
"text": response.choices[0].message.content,
|
|
55
|
+
"tokens_in": response.usage.prompt_tokens,
|
|
56
|
+
"tokens_out": response.usage.completion_tokens,
|
|
57
|
+
"model": DEEPSEEK_MODEL,
|
|
58
|
+
}
|
src/llm/ollama_client.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Клиент Ollama (локальный LLM).
|
|
4
|
+
"""
|
|
5
|
+
import ollama
|
|
6
|
+
from src.config import OLLAMA_MODEL
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def chat(system: str, user: str,
|
|
10
|
+
temperature: float = 0.0) -> dict:
|
|
11
|
+
"""Возвращает {text, tokens_in, tokens_out, model}."""
|
|
12
|
+
response = ollama.chat(
|
|
13
|
+
model=OLLAMA_MODEL,
|
|
14
|
+
messages=[
|
|
15
|
+
{"role": "system", "content": system},
|
|
16
|
+
{"role": "user", "content": user},
|
|
17
|
+
],
|
|
18
|
+
options={"temperature": temperature},
|
|
19
|
+
)
|
|
20
|
+
return {
|
|
21
|
+
"text": response["message"]["content"],
|
|
22
|
+
"tokens_in": 0,
|
|
23
|
+
"tokens_out": 0,
|
|
24
|
+
"model": OLLAMA_MODEL,
|
|
25
|
+
}
|