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.
Files changed (217) hide show
  1. codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  2. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
  3. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  4. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
  5. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
  6. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
  7. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
  8. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
  9. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
  10. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
  11. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  12. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  13. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  14. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
  15. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
  16. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
  17. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
  18. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
  19. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  20. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  21. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
  22. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
  23. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  24. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
  25. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
  26. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
  27. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
  28. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  29. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  30. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  31. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  32. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
  33. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
  34. codebuff_agent-2.1.74.dist-info/METADATA +142 -0
  35. codebuff_agent-2.1.74.dist-info/RECORD +217 -0
  36. codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
  37. codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
  38. codebuff_core/__init__.py +153 -0
  39. codebuff_core/codebuff_config_defaults.json +64 -0
  40. codebuff_core/core/__init__.py +35 -0
  41. codebuff_core/core/code_validator.py +1238 -0
  42. codebuff_core/core/error_classifier.py +579 -0
  43. codebuff_core/core/exceptions.py +49 -0
  44. codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
  45. codebuff_core/directory_policy.py +32 -0
  46. codebuff_core/factory.py +155 -0
  47. codebuff_core/interfaces.py +197 -0
  48. codebuff_core/knowledge/__init__.py +31 -0
  49. codebuff_core/knowledge/chunking.py +375 -0
  50. codebuff_core/knowledge/loader.py +339 -0
  51. codebuff_core/knowledge/watchdog_service.py +354 -0
  52. codebuff_core/legacy_paths.py +69 -0
  53. codebuff_core/llm/__init__.py +13 -0
  54. codebuff_core/llm/service.py +407 -0
  55. codebuff_core/managers/__init__.py +22 -0
  56. codebuff_core/managers/config_manager.py +690 -0
  57. codebuff_core/managers/remote_config.py +673 -0
  58. codebuff_core/managers/remote_skills.py +539 -0
  59. codebuff_core/managers/session_manager.py +255 -0
  60. codebuff_core/middleware/__init__.py +3 -0
  61. codebuff_core/middleware/error_recovery.py +394 -0
  62. codebuff_core/middleware/logging_utils.py +33 -0
  63. codebuff_core/models/__init__.py +111 -0
  64. codebuff_core/models/agent.py +306 -0
  65. codebuff_core/models/chat.py +57 -0
  66. codebuff_core/models/common.py +82 -0
  67. codebuff_core/models/rag.py +300 -0
  68. codebuff_core/platform_rules.py +127 -0
  69. codebuff_core/prompts/__init__.py +44 -0
  70. codebuff_core/prompts/auto_agent_prompts.py +1278 -0
  71. codebuff_core/services/__init__.py +30 -0
  72. codebuff_core/services/agent_service.py +345 -0
  73. codebuff_core/services/analytics_aggregate.py +837 -0
  74. codebuff_core/services/chat_service.py +898 -0
  75. codebuff_core/services/context_condenser.py +306 -0
  76. codebuff_core/services/pii_guardrail.py +393 -0
  77. codebuff_core/services/rag_chat_agent.py +2993 -0
  78. codebuff_core/services/rag_collections.py +2740 -0
  79. codebuff_core/services/rag_manager.py +890 -0
  80. codebuff_core/services/rag_service.py +145 -0
  81. codebuff_core/services/retriever.py +260 -0
  82. codebuff_core/services/vllm_embedding_service.py +290 -0
  83. codebuff_core/tests/__init__.py +1 -0
  84. codebuff_core/tests/conftest.py +81 -0
  85. codebuff_core/tests/test_error_recovery.py +647 -0
  86. codebuff_core/tests/test_factory.py +135 -0
  87. codebuff_core/tests/test_services.py +150 -0
  88. jupyter_ext/__init__.py +266 -0
  89. jupyter_ext/_version.py +4 -0
  90. jupyter_ext/agent/__init__.py +49 -0
  91. jupyter_ext/agent/agent_factory.py +2270 -0
  92. jupyter_ext/agent/ast_tools.py +451 -0
  93. jupyter_ext/agent/autofix_middleware.py +481 -0
  94. jupyter_ext/agent/background_procs.py +372 -0
  95. jupyter_ext/agent/code_history_middleware.py +211 -0
  96. jupyter_ext/agent/codex_autofix.py +160 -0
  97. jupyter_ext/agent/codex_bridge.py +3339 -0
  98. jupyter_ext/agent/codex_hooks.py +320 -0
  99. jupyter_ext/agent/codex_slash.py +685 -0
  100. jupyter_ext/agent/codex_threads.py +114 -0
  101. jupyter_ext/agent/content_injection.py +377 -0
  102. jupyter_ext/agent/continuation_middleware.py +548 -0
  103. jupyter_ext/agent/custom_skills.py +335 -0
  104. jupyter_ext/agent/file_diffs.py +99 -0
  105. jupyter_ext/agent/git_history.py +188 -0
  106. jupyter_ext/agent/guardrail_middleware.py +421 -0
  107. jupyter_ext/agent/json_repair_utils.py +54 -0
  108. jupyter_ext/agent/langchain_tools.py +2673 -0
  109. jupyter_ext/agent/mermaid_fixer.py +289 -0
  110. jupyter_ext/agent/mermaid_middleware.py +135 -0
  111. jupyter_ext/agent/middleware.py +359 -0
  112. jupyter_ext/agent/middleware_prompts.py +53 -0
  113. jupyter_ext/agent/observability.py +334 -0
  114. jupyter_ext/agent/patch_tool_calls.py +57 -0
  115. jupyter_ext/agent/prompts.py +112 -0
  116. jupyter_ext/agent/skill_middleware.py +257 -0
  117. jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
  118. jupyter_ext/agent/skills/anomaly_detection.md +12 -0
  119. jupyter_ext/agent/skills/athena_sql.md +156 -0
  120. jupyter_ext/agent/skills/bdp_table_search.md +33 -0
  121. jupyter_ext/agent/skills/bigquery.md +65 -0
  122. jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
  123. jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
  124. jupyter_ext/agent/skills/classification_evaluation.md +11 -0
  125. jupyter_ext/agent/skills/clustering_analysis.md +12 -0
  126. jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
  127. jupyter_ext/agent/skills/data_analysis.md +236 -0
  128. jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
  129. jupyter_ext/agent/skills/data_loading.md +158 -0
  130. jupyter_ext/agent/skills/data_quality_report.md +13 -0
  131. jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
  132. jupyter_ext/agent/skills/emr_script_development.md +17 -0
  133. jupyter_ext/agent/skills/experiment_tracking.md +13 -0
  134. jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
  135. jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
  136. jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
  137. jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
  138. jupyter_ext/agent/skills/inference.md +392 -0
  139. jupyter_ext/agent/skills/lambda_function_development.md +19 -0
  140. jupyter_ext/agent/skills/model_comparison.md +11 -0
  141. jupyter_ext/agent/skills/model_explainability.md +12 -0
  142. jupyter_ext/agent/skills/model_training.md +318 -0
  143. jupyter_ext/agent/skills/pyspark.md +352 -0
  144. jupyter_ext/agent/skills/python_code_quality.md +35 -0
  145. jupyter_ext/agent/skills/regression_evaluation.md +12 -0
  146. jupyter_ext/agent/skills/spring_java_development.md +35 -0
  147. jupyter_ext/agent/skills/text_preprocessing.md +12 -0
  148. jupyter_ext/agent/skills/time_series_analysis.md +13 -0
  149. jupyter_ext/agent/skills/unit_testing.md +203 -0
  150. jupyter_ext/agent/todo_prompts.py +57 -0
  151. jupyter_ext/agent/tool_report.py +45 -0
  152. jupyter_ext/analytics_ingest.py +599 -0
  153. jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  154. jupyter_ext/handlers/__init__.py +8 -0
  155. jupyter_ext/handlers/analytics_handler.py +170 -0
  156. jupyter_ext/handlers/benchmark_handler.py +794 -0
  157. jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
  158. jupyter_ext/handlers/rag_admin_handler.py +689 -0
  159. jupyter_ext/handlers/websocket.py +497 -0
  160. jupyter_ext/http_handlers.py +8744 -0
  161. jupyter_ext/idle_monitor.py +346 -0
  162. jupyter_ext/labextension/package.json +157 -0
  163. jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  164. jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
  165. jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
  166. jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
  167. jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
  168. jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
  169. jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
  170. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
  171. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  172. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  173. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  174. jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
  175. jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
  176. jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
  177. jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
  178. jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
  179. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  180. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  181. jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
  182. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
  183. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  184. jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
  185. jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
  186. jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
  187. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
  188. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  189. jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  190. jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  191. jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  192. jupyter_ext/labextension/static/style.js +4 -0
  193. jupyter_ext/labextension/static/third-party-licenses.json +724 -0
  194. jupyter_ext/log_paths.py +113 -0
  195. jupyter_ext/resource_usage.py +519 -0
  196. jupyter_ext/secrets_bootstrap.py +116 -0
  197. jupyter_ext/team_access.py +105 -0
  198. jupyter_ext/tests/__init__.py +1 -0
  199. jupyter_ext/tests/test_context.py +234 -0
  200. jupyter_ext/tests/test_factory.py +563 -0
  201. jupyter_ext/tests/test_file_finder.py +139 -0
  202. jupyter_ext/tests/test_handlers.py +58 -0
  203. jupyter_ext/tests/test_mcp_selftest.py +204 -0
  204. jupyter_ext/tests/test_mermaid_fixer.py +272 -0
  205. jupyter_ext/tests/test_resource_tiers.py +298 -0
  206. jupyter_ext/tools/__init__.py +15 -0
  207. jupyter_ext/tools/base.py +90 -0
  208. jupyter_ext/tools/context.py +268 -0
  209. jupyter_ext/tools/dir_complete.py +86 -0
  210. jupyter_ext/tools/file_finder.py +303 -0
  211. jupyter_ext/tools/file_tools.py +244 -0
  212. jupyter_ext/tools/jupyter_tools.py +296 -0
  213. jupyter_ext/tools/registry.py +159 -0
  214. jupyter_ext/tools/search_tools.py +230 -0
  215. jupyter_ext/tools/security_tools.py +657 -0
  216. jupyter_ext/tools/shell_tools.py +109 -0
  217. 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,13 @@
1
+ """
2
+ Codebuff Core - LLM Service
3
+
4
+ Multi-provider LLM interaction abstraction layer.
5
+ """
6
+
7
+ from .service import LLMService, call_llm, call_llm_stream
8
+
9
+ __all__ = [
10
+ "LLMService",
11
+ "call_llm",
12
+ "call_llm_stream",
13
+ ]
@@ -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
+ ]