codebuff-agent 2.1.74__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.
- codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
- codebuff_agent-2.1.74.dist-info/METADATA +142 -0
- codebuff_agent-2.1.74.dist-info/RECORD +217 -0
- codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
- codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
- codebuff_core/__init__.py +153 -0
- codebuff_core/codebuff_config_defaults.json +64 -0
- codebuff_core/core/__init__.py +35 -0
- codebuff_core/core/code_validator.py +1238 -0
- codebuff_core/core/error_classifier.py +579 -0
- codebuff_core/core/exceptions.py +49 -0
- codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
- codebuff_core/directory_policy.py +32 -0
- codebuff_core/factory.py +155 -0
- codebuff_core/interfaces.py +197 -0
- codebuff_core/knowledge/__init__.py +31 -0
- codebuff_core/knowledge/chunking.py +375 -0
- codebuff_core/knowledge/loader.py +339 -0
- codebuff_core/knowledge/watchdog_service.py +354 -0
- codebuff_core/legacy_paths.py +69 -0
- codebuff_core/llm/__init__.py +13 -0
- codebuff_core/llm/service.py +407 -0
- codebuff_core/managers/__init__.py +22 -0
- codebuff_core/managers/config_manager.py +690 -0
- codebuff_core/managers/remote_config.py +673 -0
- codebuff_core/managers/remote_skills.py +539 -0
- codebuff_core/managers/session_manager.py +255 -0
- codebuff_core/middleware/__init__.py +3 -0
- codebuff_core/middleware/error_recovery.py +394 -0
- codebuff_core/middleware/logging_utils.py +33 -0
- codebuff_core/models/__init__.py +111 -0
- codebuff_core/models/agent.py +306 -0
- codebuff_core/models/chat.py +57 -0
- codebuff_core/models/common.py +82 -0
- codebuff_core/models/rag.py +300 -0
- codebuff_core/platform_rules.py +127 -0
- codebuff_core/prompts/__init__.py +44 -0
- codebuff_core/prompts/auto_agent_prompts.py +1278 -0
- codebuff_core/services/__init__.py +30 -0
- codebuff_core/services/agent_service.py +345 -0
- codebuff_core/services/analytics_aggregate.py +837 -0
- codebuff_core/services/chat_service.py +898 -0
- codebuff_core/services/context_condenser.py +306 -0
- codebuff_core/services/pii_guardrail.py +393 -0
- codebuff_core/services/rag_chat_agent.py +2993 -0
- codebuff_core/services/rag_collections.py +2740 -0
- codebuff_core/services/rag_manager.py +890 -0
- codebuff_core/services/rag_service.py +145 -0
- codebuff_core/services/retriever.py +260 -0
- codebuff_core/services/vllm_embedding_service.py +290 -0
- codebuff_core/tests/__init__.py +1 -0
- codebuff_core/tests/conftest.py +81 -0
- codebuff_core/tests/test_error_recovery.py +647 -0
- codebuff_core/tests/test_factory.py +135 -0
- codebuff_core/tests/test_services.py +150 -0
- jupyter_ext/__init__.py +266 -0
- jupyter_ext/_version.py +4 -0
- jupyter_ext/agent/__init__.py +49 -0
- jupyter_ext/agent/agent_factory.py +2270 -0
- jupyter_ext/agent/ast_tools.py +451 -0
- jupyter_ext/agent/autofix_middleware.py +481 -0
- jupyter_ext/agent/background_procs.py +372 -0
- jupyter_ext/agent/code_history_middleware.py +211 -0
- jupyter_ext/agent/codex_autofix.py +160 -0
- jupyter_ext/agent/codex_bridge.py +3339 -0
- jupyter_ext/agent/codex_hooks.py +320 -0
- jupyter_ext/agent/codex_slash.py +685 -0
- jupyter_ext/agent/codex_threads.py +114 -0
- jupyter_ext/agent/content_injection.py +377 -0
- jupyter_ext/agent/continuation_middleware.py +548 -0
- jupyter_ext/agent/custom_skills.py +335 -0
- jupyter_ext/agent/file_diffs.py +99 -0
- jupyter_ext/agent/git_history.py +188 -0
- jupyter_ext/agent/guardrail_middleware.py +421 -0
- jupyter_ext/agent/json_repair_utils.py +54 -0
- jupyter_ext/agent/langchain_tools.py +2673 -0
- jupyter_ext/agent/mermaid_fixer.py +289 -0
- jupyter_ext/agent/mermaid_middleware.py +135 -0
- jupyter_ext/agent/middleware.py +359 -0
- jupyter_ext/agent/middleware_prompts.py +53 -0
- jupyter_ext/agent/observability.py +334 -0
- jupyter_ext/agent/patch_tool_calls.py +57 -0
- jupyter_ext/agent/prompts.py +112 -0
- jupyter_ext/agent/skill_middleware.py +257 -0
- jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
- jupyter_ext/agent/skills/anomaly_detection.md +12 -0
- jupyter_ext/agent/skills/athena_sql.md +156 -0
- jupyter_ext/agent/skills/bdp_table_search.md +33 -0
- jupyter_ext/agent/skills/bigquery.md +65 -0
- jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
- jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
- jupyter_ext/agent/skills/classification_evaluation.md +11 -0
- jupyter_ext/agent/skills/clustering_analysis.md +12 -0
- jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
- jupyter_ext/agent/skills/data_analysis.md +236 -0
- jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
- jupyter_ext/agent/skills/data_loading.md +158 -0
- jupyter_ext/agent/skills/data_quality_report.md +13 -0
- jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
- jupyter_ext/agent/skills/emr_script_development.md +17 -0
- jupyter_ext/agent/skills/experiment_tracking.md +13 -0
- jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
- jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
- jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
- jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
- jupyter_ext/agent/skills/inference.md +392 -0
- jupyter_ext/agent/skills/lambda_function_development.md +19 -0
- jupyter_ext/agent/skills/model_comparison.md +11 -0
- jupyter_ext/agent/skills/model_explainability.md +12 -0
- jupyter_ext/agent/skills/model_training.md +318 -0
- jupyter_ext/agent/skills/pyspark.md +352 -0
- jupyter_ext/agent/skills/python_code_quality.md +35 -0
- jupyter_ext/agent/skills/regression_evaluation.md +12 -0
- jupyter_ext/agent/skills/spring_java_development.md +35 -0
- jupyter_ext/agent/skills/text_preprocessing.md +12 -0
- jupyter_ext/agent/skills/time_series_analysis.md +13 -0
- jupyter_ext/agent/skills/unit_testing.md +203 -0
- jupyter_ext/agent/todo_prompts.py +57 -0
- jupyter_ext/agent/tool_report.py +45 -0
- jupyter_ext/analytics_ingest.py +599 -0
- jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- jupyter_ext/handlers/__init__.py +8 -0
- jupyter_ext/handlers/analytics_handler.py +170 -0
- jupyter_ext/handlers/benchmark_handler.py +794 -0
- jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
- jupyter_ext/handlers/rag_admin_handler.py +689 -0
- jupyter_ext/handlers/websocket.py +497 -0
- jupyter_ext/http_handlers.py +8744 -0
- jupyter_ext/idle_monitor.py +346 -0
- jupyter_ext/labextension/package.json +157 -0
- jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
- jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
- jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
- jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
- jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
- jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
- jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
- jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
- jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
- jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
- jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
- jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- jupyter_ext/labextension/static/style.js +4 -0
- jupyter_ext/labextension/static/third-party-licenses.json +724 -0
- jupyter_ext/log_paths.py +113 -0
- jupyter_ext/resource_usage.py +519 -0
- jupyter_ext/secrets_bootstrap.py +116 -0
- jupyter_ext/team_access.py +105 -0
- jupyter_ext/tests/__init__.py +1 -0
- jupyter_ext/tests/test_context.py +234 -0
- jupyter_ext/tests/test_factory.py +563 -0
- jupyter_ext/tests/test_file_finder.py +139 -0
- jupyter_ext/tests/test_handlers.py +58 -0
- jupyter_ext/tests/test_mcp_selftest.py +204 -0
- jupyter_ext/tests/test_mermaid_fixer.py +272 -0
- jupyter_ext/tests/test_resource_tiers.py +298 -0
- jupyter_ext/tools/__init__.py +15 -0
- jupyter_ext/tools/base.py +90 -0
- jupyter_ext/tools/context.py +268 -0
- jupyter_ext/tools/dir_complete.py +86 -0
- jupyter_ext/tools/file_finder.py +303 -0
- jupyter_ext/tools/file_tools.py +244 -0
- jupyter_ext/tools/jupyter_tools.py +296 -0
- jupyter_ext/tools/registry.py +159 -0
- jupyter_ext/tools/search_tools.py +230 -0
- jupyter_ext/tools/security_tools.py +657 -0
- jupyter_ext/tools/shell_tools.py +109 -0
- jupyter_ext/tools/user_tools.py +132 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""hdsp_agent → codebuff 리네이밍(이슈 #87) 하위호환 경로 해석.
|
|
2
|
+
|
|
3
|
+
사용자 홈에 이미 쓰여 있는 것들 — Qdrant 인덱스, API 키가 든 설정 파일, 세션 —
|
|
4
|
+
은 이름이 바뀌어도 그대로 읽혀야 한다. 새 경로가 없고 옛 경로가 있으면 옛 경로를
|
|
5
|
+
쓴다. **경로를 옮기거나 지우지 않는다** — 기존 사용자는 계속 옛 트리를 읽고 쓰고,
|
|
6
|
+
새 사용자만 새 트리를 쓴다. 마이그레이션은 이 코드가 할 일이 아니다.
|
|
7
|
+
|
|
8
|
+
**판정은 프로세스당 한 번이다**(`_decide` 의 `lru_cache`). 호출마다 다시 재면 같은
|
|
9
|
+
프로세스 안에서 답이 **뒤집힌다** — 누군가 `os.makedirs` 로 새 경로를 만드는 순간
|
|
10
|
+
그 뒤의 호출이 빈 새 디렉터리를 가리키고, 기존 인덱스는 살아 있는데 안 보이게
|
|
11
|
+
된다. 실제로 `rag_admin_handler` 가 그 `makedirs` 를 한다.
|
|
12
|
+
|
|
13
|
+
**`CODEBUFF_HOME` 이 있으면 상태를 그 아래에 둔다**(이슈 #116). 워크트리에서 띄운
|
|
14
|
+
JupyterLab 이 주 저장소의 서버와 Qdrant 락·세션을 나눠 쓰지 않게 하려는 것이라
|
|
15
|
+
(`scripts/jl_env.sh`), 그때는 옛 경로 폴백을 하지 않는다 — 폴백하면 워크트리 서버가
|
|
16
|
+
사람의 `~/.hdsp_agent` 를 열어 격리가 깨진다.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from functools import lru_cache
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
LEGACY_HOME_DIR = "~/.hdsp_agent"
|
|
27
|
+
HOME_DIR = "~/.codebuff"
|
|
28
|
+
HOME_ENV = "CODEBUFF_HOME"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def override_home() -> Optional[Path]:
|
|
32
|
+
"""`CODEBUFF_HOME` 이 설정돼 있으면 그 경로, 아니면 None."""
|
|
33
|
+
value = os.environ.get(HOME_ENV)
|
|
34
|
+
return Path(value) if value else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@lru_cache(maxsize=None)
|
|
38
|
+
def _decide(new_path: str, legacy_path: str) -> Path:
|
|
39
|
+
new_p = Path(os.path.expanduser(new_path))
|
|
40
|
+
legacy_p = Path(os.path.expanduser(legacy_path))
|
|
41
|
+
if not new_p.exists() and legacy_p.exists():
|
|
42
|
+
return legacy_p
|
|
43
|
+
return new_p
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def resolve(new_path, legacy_path) -> Path:
|
|
47
|
+
"""새 경로가 없고 옛 경로가 있으면 옛 경로를, 아니면 새 경로를 돌려준다."""
|
|
48
|
+
return _decide(str(new_path), str(legacy_path))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_home(subpath: str) -> Path:
|
|
52
|
+
"""`~/.codebuff/<subpath>` 를 옛 `~/.hdsp_agent/<subpath>` 폴백과 함께 해석한다."""
|
|
53
|
+
home = override_home()
|
|
54
|
+
if home is not None:
|
|
55
|
+
return home / subpath
|
|
56
|
+
return resolve(f"{HOME_DIR}/{subpath}", f"{LEGACY_HOME_DIR}/{subpath}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_jupyter_config(name: str, legacy_name: str) -> Path:
|
|
60
|
+
"""`~/.jupyter/<name>` 을 옛 이름 폴백과 함께 해석한다.
|
|
61
|
+
|
|
62
|
+
이 함수로 찾는 것은 세션·검색 이력 같은 상태 파일이라 `CODEBUFF_HOME` 을 따른다.
|
|
63
|
+
API 키가 든 설정 파일은 이 함수를 거치지 않으므로 워크트리에서도 공유된다.
|
|
64
|
+
"""
|
|
65
|
+
home = override_home()
|
|
66
|
+
if home is not None:
|
|
67
|
+
return home / name
|
|
68
|
+
jupyter_dir = Path(os.path.expanduser("~/.jupyter"))
|
|
69
|
+
return resolve(jupyter_dir / name, jupyter_dir / legacy_name)
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM 서비스 - vLLM (OpenAI 호환) 프로바이더와의 상호작용 처리
|
|
3
|
+
|
|
4
|
+
vLLM 프로바이더를 통해 OpenAI 호환 API를 지원합니다.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import asyncio
|
|
13
|
+
from typing import Dict, Any, Optional
|
|
14
|
+
from contextlib import asynccontextmanager
|
|
15
|
+
import aiohttp
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
DEFAULT_MAX_OUTPUT_TOKENS = 4096
|
|
20
|
+
_MAX_CONTINUATIONS = 3
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _messages(full_prompt: str, system_prompt: Optional[str]) -> list:
|
|
24
|
+
"""system 은 부르는 쪽이 줄 때만 싣는다 (이슈 #218).
|
|
25
|
+
|
|
26
|
+
예전에는 비면 채팅 안내 문구를 넣었다 — 요약·계획 JSON·노트북 같은 채팅이 아닌 호출에도
|
|
27
|
+
"기능을 적극 안내하라" 가 실렸다. 채팅 문구는 `prompts.chat_system_prompt` 이고 채팅
|
|
28
|
+
호출자(chat_service)가 넘긴다.
|
|
29
|
+
"""
|
|
30
|
+
messages = []
|
|
31
|
+
if system_prompt:
|
|
32
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
33
|
+
messages.append({"role": "user", "content": full_prompt})
|
|
34
|
+
return messages
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LLMService:
|
|
38
|
+
"""다양한 LLM 프로바이더와 상호작용하는 서비스"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, config: Dict[str, Any]):
|
|
41
|
+
self.config = config
|
|
42
|
+
self.provider = config.get("provider", "vllm")
|
|
43
|
+
|
|
44
|
+
# ========== 설정 헬퍼 ==========
|
|
45
|
+
|
|
46
|
+
def _get_vllm_config(self) -> tuple[str, str, Dict[str, str]]:
|
|
47
|
+
"""vLLM 설정 반환: (model, url, headers).
|
|
48
|
+
|
|
49
|
+
config 값이 먼저다 — `ConfigManager.get_config()` 가 이미 centri > env > 파일로
|
|
50
|
+
해석해 둔다 (이슈 #113). env 는 그 해석을 거치지 않은 config 의 폴백이다.
|
|
51
|
+
"""
|
|
52
|
+
cfg = self.config.get("vllm", {})
|
|
53
|
+
endpoint = (
|
|
54
|
+
cfg.get("endpoint")
|
|
55
|
+
or os.environ.get("VLLM_ENDPOINT")
|
|
56
|
+
or "http://localhost:8000"
|
|
57
|
+
).rstrip("/")
|
|
58
|
+
model = cfg.get("model") or os.environ.get("VLLM_MODEL") or "default"
|
|
59
|
+
# endpoint가 이미 /v1을 포함하면 중복 추가 방지
|
|
60
|
+
if endpoint.endswith("/v1"):
|
|
61
|
+
url = f"{endpoint}/chat/completions"
|
|
62
|
+
else:
|
|
63
|
+
url = f"{endpoint}/v1/chat/completions"
|
|
64
|
+
headers = {"Content-Type": "application/json"}
|
|
65
|
+
api_key = cfg.get("apiKey") or os.environ.get("VLLM_API_KEY", "")
|
|
66
|
+
if not api_key:
|
|
67
|
+
raise Exception("LLM API 키가 설정되지 않았습니다. 관리자에게 문의하세요.")
|
|
68
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
69
|
+
return model, url, headers
|
|
70
|
+
|
|
71
|
+
# ========== 메시지/페이로드 빌더 ==========
|
|
72
|
+
|
|
73
|
+
def _build_prompt(self, prompt: str, context: Optional[str] = None) -> str:
|
|
74
|
+
"""선택적 컨텍스트와 함께 전체 프롬프트 구성"""
|
|
75
|
+
if context:
|
|
76
|
+
return f"Context:\n{context}\n\nUser Request:\n{prompt}"
|
|
77
|
+
return prompt
|
|
78
|
+
|
|
79
|
+
async def _retry_with_backoff(
|
|
80
|
+
self,
|
|
81
|
+
operation,
|
|
82
|
+
max_retries: int = 3,
|
|
83
|
+
provider: str = "API",
|
|
84
|
+
retryable_statuses: tuple = (503, 429),
|
|
85
|
+
):
|
|
86
|
+
"""지수 백오프 재시도 로직으로 작업 실행"""
|
|
87
|
+
for attempt in range(max_retries):
|
|
88
|
+
try:
|
|
89
|
+
return await operation()
|
|
90
|
+
except asyncio.TimeoutError:
|
|
91
|
+
if attempt < max_retries - 1:
|
|
92
|
+
wait_time = (2**attempt) * 3
|
|
93
|
+
print(
|
|
94
|
+
f"[LLMService] 요청 시간 초과. {wait_time}초 후 재시도... (시도 {attempt + 1}/{max_retries})"
|
|
95
|
+
)
|
|
96
|
+
await asyncio.sleep(wait_time)
|
|
97
|
+
continue
|
|
98
|
+
raise Exception(f"{max_retries}회 재시도 후 요청 시간 초과")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
error_msg = str(e)
|
|
101
|
+
# 속도 제한 (429) 오류는 재시도 가능
|
|
102
|
+
if "rate limit" in error_msg.lower() or "(429)" in error_msg:
|
|
103
|
+
if attempt < max_retries - 1:
|
|
104
|
+
wait_time = 40 + (attempt * 20)
|
|
105
|
+
print(
|
|
106
|
+
f"[LLMService] 속도 제한 도달. 재시도 전 {wait_time}초 대기... (시도 {attempt + 1}/{max_retries})"
|
|
107
|
+
)
|
|
108
|
+
await asyncio.sleep(wait_time)
|
|
109
|
+
continue
|
|
110
|
+
raise Exception(
|
|
111
|
+
f"{max_retries}회 재시도 후 속도 제한 초과. 잠시 후 다시 시도하세요."
|
|
112
|
+
)
|
|
113
|
+
# 서버 과부하 (503) 오류도 재시도 가능
|
|
114
|
+
if "overloaded" in error_msg.lower() or "(503)" in error_msg:
|
|
115
|
+
if attempt < max_retries - 1:
|
|
116
|
+
wait_time = (2**attempt) * 5
|
|
117
|
+
print(
|
|
118
|
+
f"[LLMService] 서버 과부하. {wait_time}초 후 재시도... (시도 {attempt + 1}/{max_retries})"
|
|
119
|
+
)
|
|
120
|
+
await asyncio.sleep(wait_time)
|
|
121
|
+
continue
|
|
122
|
+
raise
|
|
123
|
+
# 다른 API 오류는 즉시 실패
|
|
124
|
+
if "API error" in error_msg and "rate limit" not in error_msg.lower():
|
|
125
|
+
raise
|
|
126
|
+
if "timeout" in error_msg.lower():
|
|
127
|
+
raise
|
|
128
|
+
# 네트워크 오류 재시도
|
|
129
|
+
if attempt < max_retries - 1:
|
|
130
|
+
wait_time = (2**attempt) * 2
|
|
131
|
+
print(
|
|
132
|
+
f"[LLMService] 네트워크 오류: {e}. {wait_time}초 후 재시도... (시도 {attempt + 1}/{max_retries})"
|
|
133
|
+
)
|
|
134
|
+
await asyncio.sleep(wait_time)
|
|
135
|
+
continue
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
@asynccontextmanager
|
|
139
|
+
async def _request(
|
|
140
|
+
self,
|
|
141
|
+
url: str,
|
|
142
|
+
payload: Dict[str, Any],
|
|
143
|
+
headers: Optional[Dict[str, str]] = None,
|
|
144
|
+
timeout_seconds: int = 60,
|
|
145
|
+
provider: str = "API",
|
|
146
|
+
):
|
|
147
|
+
"""자동 세션 정리가 포함된 HTTP POST 요청 컨텍스트 관리자"""
|
|
148
|
+
timeout = aiohttp.ClientTimeout(total=timeout_seconds)
|
|
149
|
+
connector = aiohttp.TCPConnector(ssl=False)
|
|
150
|
+
async with aiohttp.ClientSession(
|
|
151
|
+
timeout=timeout, connector=connector
|
|
152
|
+
) as session:
|
|
153
|
+
async with session.post(url, json=payload, headers=headers) as response:
|
|
154
|
+
if response.status != 200:
|
|
155
|
+
error_text = await response.text()
|
|
156
|
+
print(f"[LLMService] {provider} API 오류: {error_text}")
|
|
157
|
+
if response.status in (401, 403):
|
|
158
|
+
raise Exception(
|
|
159
|
+
"API 키가 유효하지 않거나 만료되었습니다. "
|
|
160
|
+
"관리자에게 문의하세요."
|
|
161
|
+
)
|
|
162
|
+
elif response.status == 429:
|
|
163
|
+
raise Exception("요청 한도 초과. 잠시 후 다시 시도해주세요.")
|
|
164
|
+
else:
|
|
165
|
+
raise Exception(
|
|
166
|
+
f"{provider} API 서버 오류 ({response.status}). "
|
|
167
|
+
"엔드포인트 설정을 확인하거나 잠시 후 재시도하세요."
|
|
168
|
+
)
|
|
169
|
+
yield response
|
|
170
|
+
|
|
171
|
+
async def _request_json(
|
|
172
|
+
self,
|
|
173
|
+
url: str,
|
|
174
|
+
payload: Dict[str, Any],
|
|
175
|
+
headers: Optional[Dict[str, str]] = None,
|
|
176
|
+
timeout_seconds: int = 60,
|
|
177
|
+
provider: str = "API",
|
|
178
|
+
) -> Dict[str, Any]:
|
|
179
|
+
"""요청 수행 및 JSON 응답 반환"""
|
|
180
|
+
async with self._request(
|
|
181
|
+
url, payload, headers, timeout_seconds, provider
|
|
182
|
+
) as response:
|
|
183
|
+
return await response.json()
|
|
184
|
+
|
|
185
|
+
async def _stream_response(
|
|
186
|
+
self,
|
|
187
|
+
url: str,
|
|
188
|
+
payload: Dict[str, Any],
|
|
189
|
+
headers: Optional[Dict[str, str]],
|
|
190
|
+
provider: str,
|
|
191
|
+
line_parser,
|
|
192
|
+
):
|
|
193
|
+
"""응답 스트리밍 및 파싱된 콘텐츠 yield. finish_reason을 _last_finish_reason에 저장."""
|
|
194
|
+
self._last_finish_reason = None
|
|
195
|
+
async with self._request(
|
|
196
|
+
url, payload, headers, timeout_seconds=120, provider=provider
|
|
197
|
+
) as response:
|
|
198
|
+
async for line in response.content:
|
|
199
|
+
line_text = line.decode("utf-8").strip()
|
|
200
|
+
content, finish_reason = line_parser(line_text)
|
|
201
|
+
if finish_reason:
|
|
202
|
+
self._last_finish_reason = finish_reason
|
|
203
|
+
if content:
|
|
204
|
+
yield content
|
|
205
|
+
|
|
206
|
+
# ========== 응답 파서 ==========
|
|
207
|
+
|
|
208
|
+
def _parse_openai_response(self, data: Dict[str, Any]) -> str:
|
|
209
|
+
"""OpenAI 호환 응답 형식 파싱 (OpenAI 및 vLLM에서 사용)"""
|
|
210
|
+
if "choices" in data and len(data["choices"]) > 0:
|
|
211
|
+
choice = data["choices"][0]
|
|
212
|
+
if "message" in choice and "content" in choice["message"]:
|
|
213
|
+
return choice["message"]["content"]
|
|
214
|
+
elif "text" in choice:
|
|
215
|
+
return choice["text"]
|
|
216
|
+
raise Exception("API로부터 유효한 응답 없음")
|
|
217
|
+
|
|
218
|
+
def _parse_sse_line(
|
|
219
|
+
self, line_text: str, extractor
|
|
220
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
221
|
+
"""주어진 추출기 함수로 SSE 라인 파싱. (content, finish_reason) 반환."""
|
|
222
|
+
if not line_text.startswith("data: "):
|
|
223
|
+
return None, None
|
|
224
|
+
data_str = line_text[6:]
|
|
225
|
+
if data_str == "[DONE]":
|
|
226
|
+
return None, None
|
|
227
|
+
try:
|
|
228
|
+
data = json.loads(data_str)
|
|
229
|
+
return extractor(data)
|
|
230
|
+
except json.JSONDecodeError:
|
|
231
|
+
return None, None
|
|
232
|
+
|
|
233
|
+
def _extract_openai_chunk(
|
|
234
|
+
self, data: Dict[str, Any]
|
|
235
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
236
|
+
"""OpenAI 스트림 데이터에서 (content, finish_reason) 튜플 추출"""
|
|
237
|
+
if "choices" in data and len(data["choices"]) > 0:
|
|
238
|
+
choice = data["choices"][0]
|
|
239
|
+
delta = choice.get("delta", {})
|
|
240
|
+
content = delta.get("content", "") or None
|
|
241
|
+
finish_reason = choice.get("finish_reason")
|
|
242
|
+
return content, finish_reason
|
|
243
|
+
return None, None
|
|
244
|
+
|
|
245
|
+
def _parse_openai_stream_line(
|
|
246
|
+
self, line_text: str
|
|
247
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
248
|
+
"""OpenAI 호환 스트림의 단일 SSE 라인 파싱. (content, finish_reason) 반환."""
|
|
249
|
+
return self._parse_sse_line(line_text, self._extract_openai_chunk)
|
|
250
|
+
|
|
251
|
+
def _build_openai_payload(
|
|
252
|
+
self,
|
|
253
|
+
model: str,
|
|
254
|
+
messages: list,
|
|
255
|
+
max_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS,
|
|
256
|
+
temperature: float = 0.0,
|
|
257
|
+
stream: bool = False,
|
|
258
|
+
) -> Dict[str, Any]:
|
|
259
|
+
"""OpenAI 호환 요청 페이로드 구성"""
|
|
260
|
+
return {
|
|
261
|
+
"model": model,
|
|
262
|
+
"messages": messages,
|
|
263
|
+
"max_tokens": max_tokens,
|
|
264
|
+
"temperature": temperature,
|
|
265
|
+
"stream": stream,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async def generate_response_stream(
|
|
269
|
+
self,
|
|
270
|
+
prompt: str,
|
|
271
|
+
context: Optional[str] = None,
|
|
272
|
+
max_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS,
|
|
273
|
+
system_prompt: Optional[str] = None,
|
|
274
|
+
):
|
|
275
|
+
"""설정된 LLM 프로바이더로부터 스트리밍 응답 생성 (비동기 제너레이터).
|
|
276
|
+
|
|
277
|
+
finish_reason이 'length' 또는 'max_tokens'이면 자동으로 continuation 요청하여
|
|
278
|
+
응답이 잘리지 않도록 합니다 (최대 _MAX_CONTINUATIONS회).
|
|
279
|
+
"""
|
|
280
|
+
if self.provider != "vllm":
|
|
281
|
+
raise ValueError(f"지원되지 않는 프로바이더: {self.provider}")
|
|
282
|
+
|
|
283
|
+
messages = _messages(self._build_prompt(prompt, context), system_prompt)
|
|
284
|
+
accumulated = ""
|
|
285
|
+
|
|
286
|
+
for attempt in range(1 + _MAX_CONTINUATIONS):
|
|
287
|
+
async for chunk in self._call_vllm_stream_internal(messages, max_tokens):
|
|
288
|
+
accumulated += chunk
|
|
289
|
+
yield chunk
|
|
290
|
+
|
|
291
|
+
if self._last_finish_reason not in ("length", "max_tokens"):
|
|
292
|
+
break # 정상 종료
|
|
293
|
+
|
|
294
|
+
# Truncation 감지 → continuation
|
|
295
|
+
logger.info(
|
|
296
|
+
f"[LLMService] Auto-continuation triggered "
|
|
297
|
+
f"(attempt {attempt + 1}/{_MAX_CONTINUATIONS}, "
|
|
298
|
+
f"finish_reason={self._last_finish_reason})"
|
|
299
|
+
)
|
|
300
|
+
messages.append({"role": "assistant", "content": accumulated})
|
|
301
|
+
messages.append(
|
|
302
|
+
{
|
|
303
|
+
"role": "user",
|
|
304
|
+
"content": (
|
|
305
|
+
"[SYSTEM] 응답이 max_tokens 한도에 도달하여 잘렸습니다. "
|
|
306
|
+
"이전 내용에 이어서 계속 작성하세요. "
|
|
307
|
+
"이전 내용을 반복하지 말고, 잘린 지점부터 이어서 작성하세요."
|
|
308
|
+
),
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
accumulated = "" # 다음 chunk부터 새로 축적
|
|
312
|
+
|
|
313
|
+
async def generate_response(
|
|
314
|
+
self,
|
|
315
|
+
prompt: str,
|
|
316
|
+
context: Optional[str] = None,
|
|
317
|
+
system_prompt: Optional[str] = None,
|
|
318
|
+
max_tokens: Optional[int] = None,
|
|
319
|
+
) -> str:
|
|
320
|
+
"""설정된 LLM 프로바이더로부터 응답 생성"""
|
|
321
|
+
if self.provider == "vllm":
|
|
322
|
+
return await self._call_vllm(prompt, context, system_prompt, max_tokens)
|
|
323
|
+
else:
|
|
324
|
+
raise ValueError(f"지원되지 않는 프로바이더: {self.provider}")
|
|
325
|
+
|
|
326
|
+
async def _call_vllm(
|
|
327
|
+
self,
|
|
328
|
+
prompt: str,
|
|
329
|
+
context: Optional[str] = None,
|
|
330
|
+
system_prompt: Optional[str] = None,
|
|
331
|
+
max_tokens: Optional[int] = None,
|
|
332
|
+
) -> str:
|
|
333
|
+
"""OpenAI 호환 API로 vLLM 엔드포인트 호출"""
|
|
334
|
+
model, url, headers = self._get_vllm_config()
|
|
335
|
+
messages = _messages(self._build_prompt(prompt, context), system_prompt)
|
|
336
|
+
payload = self._build_openai_payload(
|
|
337
|
+
model,
|
|
338
|
+
messages,
|
|
339
|
+
max_tokens=max_tokens or DEFAULT_MAX_OUTPUT_TOKENS,
|
|
340
|
+
stream=False,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
data = await self._request_json(url, payload, headers, provider="vLLM")
|
|
344
|
+
return self._parse_openai_response(data)
|
|
345
|
+
|
|
346
|
+
async def _call_vllm_stream_internal(
|
|
347
|
+
self, messages: list, max_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS
|
|
348
|
+
):
|
|
349
|
+
"""messages를 직접 받아 스트리밍 호출 (내부용)"""
|
|
350
|
+
model, url, headers = self._get_vllm_config()
|
|
351
|
+
payload = self._build_openai_payload(
|
|
352
|
+
model, messages, max_tokens=max_tokens, stream=True
|
|
353
|
+
)
|
|
354
|
+
async for content in self._stream_response(
|
|
355
|
+
url, payload, headers, "vLLM", self._parse_openai_stream_line
|
|
356
|
+
):
|
|
357
|
+
yield content
|
|
358
|
+
|
|
359
|
+
async def _call_vllm_stream(
|
|
360
|
+
self,
|
|
361
|
+
prompt: str,
|
|
362
|
+
context: Optional[str] = None,
|
|
363
|
+
max_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS,
|
|
364
|
+
system_prompt: Optional[str] = None,
|
|
365
|
+
):
|
|
366
|
+
"""스트리밍으로 vLLM 엔드포인트 호출 (기존 인터페이스 유지)"""
|
|
367
|
+
messages = _messages(self._build_prompt(prompt, context), system_prompt)
|
|
368
|
+
async for chunk in self._call_vllm_stream_internal(messages, max_tokens):
|
|
369
|
+
yield chunk
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# Auto-Agent를 위한 모듈 수준 헬퍼 함수
|
|
373
|
+
async def call_llm(
|
|
374
|
+
prompt: str, config: Dict[str, Any], context: Optional[str] = None
|
|
375
|
+
) -> str:
|
|
376
|
+
"""
|
|
377
|
+
주어진 설정으로 LLM을 호출하는 편의 함수.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
prompt: LLM에 보낼 프롬프트
|
|
381
|
+
config: LLM 설정 딕셔너리
|
|
382
|
+
context: 포함할 선택적 컨텍스트
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
LLM 응답 문자열
|
|
386
|
+
"""
|
|
387
|
+
service = LLMService(config)
|
|
388
|
+
return await service.generate_response(prompt, context)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
async def call_llm_stream(
|
|
392
|
+
prompt: str, config: Dict[str, Any], context: Optional[str] = None
|
|
393
|
+
):
|
|
394
|
+
"""
|
|
395
|
+
주어진 설정으로 LLM 응답을 스트리밍하는 편의 함수.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
prompt: LLM에 보낼 프롬프트
|
|
399
|
+
config: LLM 설정 딕셔너리
|
|
400
|
+
context: 포함할 선택적 컨텍스트
|
|
401
|
+
|
|
402
|
+
Yields:
|
|
403
|
+
도착하는 응답 청크
|
|
404
|
+
"""
|
|
405
|
+
service = LLMService(config)
|
|
406
|
+
async for chunk in service.generate_response_stream(prompt, context):
|
|
407
|
+
yield chunk
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Codebuff Core - Managers
|
|
3
|
+
|
|
4
|
+
Singleton managers for configuration, session, and RAG orchestration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .config_manager import ConfigManager, get_config_manager
|
|
8
|
+
from .session_manager import (
|
|
9
|
+
ChatMessage,
|
|
10
|
+
Session,
|
|
11
|
+
SessionManager,
|
|
12
|
+
get_session_manager,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ConfigManager",
|
|
17
|
+
"get_config_manager",
|
|
18
|
+
"ChatMessage",
|
|
19
|
+
"Session",
|
|
20
|
+
"SessionManager",
|
|
21
|
+
"get_session_manager",
|
|
22
|
+
]
|