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,145 @@
1
+ """
2
+ RAG 서비스 구현
3
+
4
+ IRAGService의 Embedded 구현.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from codebuff_core.interfaces import IRAGService
11
+ from codebuff_core.models.rag import SearchRequest, SearchResponse
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class EmbeddedRAGService(IRAGService):
17
+ """
18
+ RAG 서비스 구현.
19
+
20
+ 인프로세스에서 직접 RAGManager를 사용합니다.
21
+ """
22
+
23
+ def __init__(self):
24
+ """Embedded RAG 서비스 초기화"""
25
+ self._rag_manager = None
26
+ self._initialized = False
27
+ logger.info("EmbeddedRAGService 생성됨 (아직 초기화되지 않음)")
28
+
29
+ async def initialize(self) -> None:
30
+ """RAG 관리자 초기화."""
31
+ if self._initialized:
32
+ return
33
+ try:
34
+ from codebuff_core.services.rag_manager import get_rag_manager
35
+
36
+ self._rag_manager = get_rag_manager()
37
+ success = await self._rag_manager.initialize()
38
+ self._initialized = success
39
+ if success:
40
+ logger.info("EmbeddedRAGService: RAG 초기화 완료")
41
+ else:
42
+ logger.info("EmbeddedRAGService: RAG 비활성화 (설정)")
43
+ except Exception as e:
44
+ logger.warning(f"EmbeddedRAGService: RAG 초기화 실패: {e}")
45
+ self._initialized = False
46
+
47
+ async def shutdown(self) -> None:
48
+ """RAG 서비스 종료"""
49
+ self._rag_manager = None
50
+ self._initialized = False
51
+ logger.info("EmbeddedRAGService 종료됨")
52
+
53
+ def is_ready(self) -> bool:
54
+ """RAG 서비스 준비 상태 확인"""
55
+ if not self._initialized or not self._rag_manager:
56
+ return False
57
+ return self._rag_manager.is_ready
58
+
59
+ async def search(self, request: SearchRequest) -> SearchResponse:
60
+ """지식 베이스 검색"""
61
+ logger.info(f"[Embedded] RAG 검색: {request.query[:100]}...")
62
+
63
+ if not self.is_ready():
64
+ return SearchResponse(
65
+ results=[],
66
+ total=0,
67
+ query=request.query,
68
+ )
69
+
70
+ results = await self._rag_manager.search(
71
+ query=request.query,
72
+ collection=request.collection,
73
+ limit=request.limit,
74
+ score_threshold=request.scoreThreshold,
75
+ )
76
+
77
+ return SearchResponse(
78
+ results=results.get("results", []),
79
+ total=results.get("total", 0),
80
+ query=request.query,
81
+ )
82
+
83
+ async def get_context_for_query(
84
+ self,
85
+ query: str,
86
+ detected_libraries: Optional[List[str]] = None,
87
+ max_results: int = 5,
88
+ ) -> Optional[str]:
89
+ """쿼리에 대한 포맷된 컨텍스트 가져오기"""
90
+ logger.info(f"[Embedded] 쿼리 컨텍스트 조회: {query[:100]}...")
91
+
92
+ if not self.is_ready():
93
+ return None
94
+
95
+ try:
96
+ return await self._rag_manager.get_context_for_query(
97
+ query=query,
98
+ detected_libraries=detected_libraries,
99
+ max_results=max_results,
100
+ )
101
+ except Exception as e:
102
+ logger.error(f"RAG 컨텍스트 조회 실패: {e}")
103
+ return None
104
+
105
+ async def get_index_status(self) -> Dict[str, Any]:
106
+ """현재 인덱스 상태 가져오기"""
107
+ logger.info("[Embedded] 인덱스 상태 조회")
108
+
109
+ if not self.is_ready():
110
+ return {
111
+ "ready": False,
112
+ "documentCount": 0,
113
+ "collections": [],
114
+ "message": "RAG 서비스가 초기화되지 않음",
115
+ }
116
+
117
+ try:
118
+ return await self._rag_manager.get_index_status()
119
+ except Exception as e:
120
+ logger.error(f"인덱스 상태 조회 실패: {e}")
121
+ return {
122
+ "ready": False,
123
+ "documentCount": 0,
124
+ "collections": [],
125
+ "message": str(e),
126
+ }
127
+
128
+ async def trigger_reindex(self, force: bool = False) -> Dict[str, Any]:
129
+ """재인덱싱 작업 트리거"""
130
+ logger.info(f"[Embedded] 재인덱싱 트리거 (force={force})")
131
+
132
+ if not self.is_ready():
133
+ return {
134
+ "success": False,
135
+ "message": "RAG 서비스가 초기화되지 않음",
136
+ }
137
+
138
+ try:
139
+ return await self._rag_manager.trigger_reindex(force=force)
140
+ except Exception as e:
141
+ logger.error(f"재인덱싱 트리거 실패: {e}")
142
+ return {
143
+ "success": False,
144
+ "message": str(e),
145
+ }
@@ -0,0 +1,260 @@
1
+ """
2
+ Retriever - Dense vector search implementation.
3
+
4
+ Provides semantic similarity search via Qdrant vector database.
5
+ """
6
+
7
+ import logging
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, Optional
11
+
12
+ if TYPE_CHECKING:
13
+ from codebuff_core.models.rag import RAGConfig
14
+
15
+ from codebuff_core.services.vllm_embedding_service import VLLMEmbeddingService
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class ChunkScoreDetails:
22
+ """청크별 점수 상세 정보"""
23
+
24
+ chunk_id: str
25
+ content: str
26
+ score: float # Vector similarity score (0-1)
27
+ rank: int
28
+ metadata: Dict[str, Any]
29
+ passed_threshold: bool
30
+
31
+
32
+ class DebugSearchResult(NamedTuple):
33
+ """디버그 검색 결과"""
34
+
35
+ chunks: List[ChunkScoreDetails]
36
+ search_ms: float
37
+ total_candidates: int
38
+
39
+
40
+ class Retriever:
41
+ """
42
+ Dense vector retrieval using Qdrant.
43
+
44
+ Features:
45
+ - Dense vector search via Qdrant
46
+ - Metadata filtering
47
+ - Score thresholding
48
+
49
+ Usage:
50
+ retriever = Retriever(client, embedding_service, config)
51
+ results = await retriever.search("query", top_k=5)
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ client, # Qdrant client
57
+ embedding_service: "VLLMEmbeddingService",
58
+ config: "RAGConfig",
59
+ ):
60
+ self._client = client
61
+ self._embedding_service = embedding_service
62
+ self._config = config
63
+
64
+ async def search(
65
+ self,
66
+ query: str,
67
+ collection_name: Optional[str] = None,
68
+ top_k: Optional[int] = None,
69
+ filters: Optional[Dict[str, Any]] = None,
70
+ score_threshold: Optional[float] = None,
71
+ ) -> List[Dict[str, Any]]:
72
+ """
73
+ Perform dense vector search.
74
+
75
+ Args:
76
+ query: Search query
77
+ collection_name: Target collection (default from config)
78
+ top_k: Number of results (default from config)
79
+ filters: Metadata filters
80
+ score_threshold: Minimum score (default from config)
81
+
82
+ Returns:
83
+ List of results with content, score, metadata
84
+ """
85
+ effective_top_k = top_k or self._config.top_k
86
+ target_collection = collection_name or self._config.qdrant.collection_name
87
+ effective_threshold = score_threshold or self._config.score_threshold
88
+
89
+ # Generate query embedding
90
+ query_embedding = await self._embedding_service.embed_query(query)
91
+
92
+ # Build filter condition
93
+ qdrant_filter = self._build_filter(filters) if filters else None
94
+
95
+ # Dense vector search
96
+ try:
97
+ response = self._client.query_points(
98
+ collection_name=target_collection,
99
+ query=query_embedding,
100
+ query_filter=qdrant_filter,
101
+ limit=effective_top_k,
102
+ score_threshold=effective_threshold
103
+ * 0.5, # Lower for initial retrieval
104
+ with_payload=True,
105
+ with_vectors=False,
106
+ )
107
+ results = response.points
108
+ except Exception as e:
109
+ logger.error(f"Search failed: {e}")
110
+ return []
111
+
112
+ if not results:
113
+ logger.debug(f"No results for query: {query[:50]}...")
114
+ return []
115
+
116
+ return self._format_results(results, effective_threshold)
117
+
118
+ def _build_filter(self, filters: Dict[str, Any]) -> Optional[Dict[str, Any]]:
119
+ """Convert filter dict to Qdrant filter format"""
120
+ if not filters:
121
+ return None
122
+
123
+ conditions = []
124
+ for key, value in filters.items():
125
+ if isinstance(value, list):
126
+ # Multiple values - any match
127
+ conditions.append(
128
+ {"should": [{"key": key, "match": {"value": v}} for v in value]}
129
+ )
130
+ else:
131
+ conditions.append({"key": key, "match": {"value": value}})
132
+
133
+ return {"must": conditions} if conditions else None
134
+
135
+ def _format_results(
136
+ self, results: List, score_threshold: float
137
+ ) -> List[Dict[str, Any]]:
138
+ """Format Qdrant results to standard format"""
139
+ formatted = []
140
+ for r in results:
141
+ if r.score < score_threshold:
142
+ continue
143
+
144
+ formatted.append(
145
+ {
146
+ "content": r.payload.get("content", ""),
147
+ "score": round(r.score, 4),
148
+ "metadata": {k: v for k, v in r.payload.items() if k != "content"},
149
+ }
150
+ )
151
+ return formatted
152
+
153
+ def search_sync(
154
+ self,
155
+ query: str,
156
+ top_k: Optional[int] = None,
157
+ filters: Optional[Dict[str, Any]] = None,
158
+ ) -> List[Dict[str, Any]]:
159
+ """
160
+ Synchronous search wrapper for non-async contexts.
161
+
162
+ Note: Qdrant client operations are synchronous,
163
+ so this is just a convenience method.
164
+ """
165
+ import asyncio
166
+
167
+ try:
168
+ asyncio.get_running_loop()
169
+ logger.warning(
170
+ "search_sync called from async context, use search() instead"
171
+ )
172
+ except RuntimeError:
173
+ pass
174
+
175
+ return asyncio.run(self.search(query, top_k, filters))
176
+
177
+ async def search_with_debug(
178
+ self,
179
+ query: str,
180
+ collection_name: Optional[str] = None,
181
+ top_k: Optional[int] = None,
182
+ filters: Optional[Dict[str, Any]] = None,
183
+ score_threshold: Optional[float] = None,
184
+ ) -> DebugSearchResult:
185
+ """
186
+ 전체 점수 정보를 포함한 디버그 검색 수행.
187
+
188
+ Args:
189
+ query: Search query
190
+ collection_name: Target collection (default from config)
191
+ top_k: Number of results (default from config)
192
+ filters: Metadata filters
193
+ score_threshold: Minimum score (default from config)
194
+
195
+ Returns:
196
+ DebugSearchResult with detailed scoring information
197
+ """
198
+ start_time = time.perf_counter()
199
+
200
+ effective_top_k = top_k or self._config.top_k
201
+ target_collection = collection_name or self._config.qdrant.collection_name
202
+ effective_threshold = score_threshold or self._config.score_threshold
203
+
204
+ # Generate query embedding
205
+ query_embedding = await self._embedding_service.embed_query(query)
206
+
207
+ # Build filter condition
208
+ qdrant_filter = self._build_filter(filters) if filters else None
209
+
210
+ # Vector search with timing
211
+ try:
212
+ # 디버그용으로 더 많은 결과 (3배)를 낮은 threshold로 가져옴
213
+ response = self._client.query_points(
214
+ collection_name=target_collection,
215
+ query=query_embedding,
216
+ query_filter=qdrant_filter,
217
+ limit=effective_top_k * 3,
218
+ score_threshold=effective_threshold * 0.3,
219
+ with_payload=True,
220
+ with_vectors=False,
221
+ )
222
+ results = response.points
223
+ except Exception as e:
224
+ logger.error(f"Search failed: {e}")
225
+ return DebugSearchResult(
226
+ chunks=[],
227
+ search_ms=0.0,
228
+ total_candidates=0,
229
+ )
230
+
231
+ search_ms = (time.perf_counter() - start_time) * 1000
232
+
233
+ if not results:
234
+ return DebugSearchResult(
235
+ chunks=[],
236
+ search_ms=round(search_ms, 2),
237
+ total_candidates=0,
238
+ )
239
+
240
+ # Build detailed results
241
+ chunks = []
242
+ for rank, result in enumerate(results, start=1):
243
+ chunks.append(
244
+ ChunkScoreDetails(
245
+ chunk_id=str(result.id),
246
+ content=result.payload.get("content", ""),
247
+ score=round(result.score, 4),
248
+ rank=rank,
249
+ metadata={
250
+ k: v for k, v in result.payload.items() if k != "content"
251
+ },
252
+ passed_threshold=result.score >= effective_threshold,
253
+ )
254
+ )
255
+
256
+ return DebugSearchResult(
257
+ chunks=chunks,
258
+ search_ms=round(search_ms, 2),
259
+ total_candidates=len(results),
260
+ )
@@ -0,0 +1,290 @@
1
+ """
2
+ vLLM Embedding Service - Remote embedding generation using vLLM server.
3
+
4
+ Features:
5
+ - GPU-accelerated embeddings via vLLM server
6
+ - OpenAI-compatible API interface
7
+ - Retry logic for reliability
8
+ - Support for large models (qwen3-embedding-8b, gte-Qwen2-7B, etc.)
9
+
10
+ Prerequisites:
11
+ - vLLM embedding server running (e.g., http://10.222.52.31:8000)
12
+ - Model loaded on vLLM server
13
+ """
14
+
15
+ import logging
16
+ from typing import TYPE_CHECKING, List, Optional
17
+
18
+ import httpx
19
+
20
+ if TYPE_CHECKING:
21
+ from codebuff_core.models.rag import EmbeddingConfig
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class VLLMEmbeddingService:
27
+ """
28
+ Remote embedding generation using vLLM server.
29
+
30
+ Design Principles:
31
+ - Stateless client (vLLM server holds the model)
32
+ - Retry logic for network resilience
33
+ - OpenAI-compatible API interface
34
+
35
+ Usage:
36
+ service = get_vllm_embedding_service()
37
+ embeddings = service.embed_texts(["text1", "text2"])
38
+ query_embedding = service.embed_query("search query")
39
+ """
40
+
41
+ _instance: Optional["VLLMEmbeddingService"] = None
42
+ _initialized: bool = False
43
+
44
+ def __new__(cls, *args, **kwargs):
45
+ if cls._instance is None:
46
+ cls._instance = super().__new__(cls)
47
+ return cls._instance
48
+
49
+ def __init__(self, config: Optional["EmbeddingConfig"] = None):
50
+ if self._initialized:
51
+ return
52
+ self._initialized = True
53
+
54
+ from codebuff_core.models.rag import EmbeddingConfig
55
+
56
+ self._config = config or EmbeddingConfig()
57
+
58
+ # 환경변수 우선 → config 파일 폴백
59
+ from codebuff_core.managers.config_manager import get_config_manager
60
+
61
+ cfg = get_config_manager().get_config()
62
+ rag = cfg.get("rag", {})
63
+
64
+ # get_config() 가 centri > env > 파일로 해석해 둔 값이다 (이슈 #113)
65
+ self._endpoint = rag.get("vllmEndpoint", "") or "http://localhost:8000"
66
+ self._model = rag.get("vllmModel", "") or "qwen3-embedding-8b"
67
+ self._dimension = int(rag.get("vllmDimension", 4096) or 4096)
68
+ rag_api_key = rag.get("vllmApiKey", "")
69
+ if rag_api_key in ("***", ""):
70
+ rag_api_key = ""
71
+ self._api_key = rag_api_key or cfg.get("vllm", {}).get("apiKey", "")
72
+
73
+ # HTTP client with retry (include auth header if API key provided)
74
+ headers = {}
75
+ if self._api_key:
76
+ headers["Authorization"] = f"Bearer {self._api_key}"
77
+ self._client = httpx.AsyncClient(
78
+ base_url=self._endpoint,
79
+ timeout=httpx.Timeout(None),
80
+ limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
81
+ headers=headers,
82
+ )
83
+
84
+ logger.info(
85
+ f"vLLM Embedding Service initialized: "
86
+ f"endpoint={self._endpoint}, model={self._model}, dim={self._dimension}, "
87
+ f"auth={'enabled' if self._api_key else 'disabled'}"
88
+ )
89
+
90
+ @property
91
+ def dimension(self) -> int:
92
+ """Get embedding dimension"""
93
+ return self._dimension
94
+
95
+ async def _call_vllm_api(
96
+ self, texts: List[str], max_retries: int = 3, timeout: Optional[float] = None
97
+ ) -> List[List[float]]:
98
+ """
99
+ Call vLLM embedding API with retry logic.
100
+
101
+ Args:
102
+ texts: List of text strings to embed
103
+ max_retries: Maximum number of retry attempts
104
+
105
+ Returns:
106
+ List of embedding vectors
107
+
108
+ Raises:
109
+ Exception if all retries fail
110
+ """
111
+ payload = {
112
+ "model": self._model,
113
+ "input": texts,
114
+ "dimensions": self._dimension,
115
+ }
116
+
117
+ last_error = None
118
+ for attempt in range(max_retries):
119
+ try:
120
+ # base_url에 이미 /v1이 포함될 수 있으므로 상대 경로 사용
121
+ base = self._endpoint.rstrip("/")
122
+ if base.endswith("/v1"):
123
+ path = "/embeddings"
124
+ else:
125
+ path = "/v1/embeddings"
126
+ response = await self._client.post(path, json=payload, timeout=timeout)
127
+ response.raise_for_status()
128
+
129
+ data = response.json()
130
+ # OpenRouter 등 일부 서비스는 HTTP 200으로 오면서 error body를 반환
131
+ if "error" in data and "data" not in data:
132
+ err_msg = data["error"].get("message", str(data["error"]))
133
+ err_code = data["error"].get("code", "")
134
+ raise Exception(f"API error {err_code}: {err_msg}")
135
+ # Sort by index to ensure correct order
136
+ sorted_items = sorted(data["data"], key=lambda x: x["index"])
137
+ embeddings = [item["embedding"] for item in sorted_items]
138
+ return embeddings
139
+
140
+ except httpx.HTTPStatusError as e:
141
+ last_error = e
142
+ logger.warning(
143
+ f"vLLM API HTTP error (attempt {attempt + 1}/{max_retries}): "
144
+ f"{e.response.status_code} - {e.response.text}"
145
+ )
146
+ except httpx.RequestError as e:
147
+ last_error = e
148
+ logger.warning(
149
+ f"vLLM API connection error (attempt {attempt + 1}/{max_retries}): {e}"
150
+ )
151
+ # connection pool 오염 시 client 재생성
152
+ try:
153
+ await self._client.aclose()
154
+ except Exception:
155
+ pass
156
+ headers = {}
157
+ if self._api_key:
158
+ headers["Authorization"] = f"Bearer {self._api_key}"
159
+ self._client = httpx.AsyncClient(
160
+ base_url=self._endpoint,
161
+ timeout=httpx.Timeout(None),
162
+ limits=httpx.Limits(
163
+ max_keepalive_connections=5, max_connections=10
164
+ ),
165
+ headers=headers,
166
+ )
167
+ except Exception as e:
168
+ last_error = e
169
+ logger.error(f"Unexpected error calling vLLM API: {e}")
170
+ break
171
+
172
+ # 에러 메시지에 응답 본문 포함 (토큰 초과 등 상세 메시지 전달)
173
+ detail = str(last_error)
174
+ if hasattr(last_error, "response") and hasattr(last_error.response, "text"):
175
+ detail = f"{last_error} | body={last_error.response.text[:300]}"
176
+ raise Exception(f"vLLM embed failed after {max_retries} attempts: {detail}")
177
+
178
+ async def embed_texts(self, texts: List[str]) -> List[List[float]]:
179
+ """
180
+ Generate embeddings for a list of texts (documents/passages).
181
+
182
+ Args:
183
+ texts: List of text strings to embed
184
+
185
+ Returns:
186
+ List of embedding vectors (as lists of floats)
187
+ """
188
+ if not texts:
189
+ return []
190
+
191
+ try:
192
+ return await self._call_vllm_api(texts)
193
+ except Exception as e:
194
+ logger.error(f"Failed to generate embeddings via vLLM: {e}")
195
+ raise
196
+
197
+ async def embed_query(self, query: str) -> List[float]:
198
+ """
199
+ Generate embedding for a single query.
200
+
201
+ Args:
202
+ query: Query string
203
+
204
+ Returns:
205
+ Embedding vector as list of floats
206
+ """
207
+ if not query:
208
+ raise ValueError("Query cannot be empty")
209
+
210
+ try:
211
+ embeddings = await self._call_vllm_api([query], timeout=10.0)
212
+ return embeddings[0]
213
+ except Exception as e:
214
+ logger.error(f"Failed to generate query embedding via vLLM: {e}")
215
+ raise
216
+
217
+ async def embed_batch(
218
+ self, texts: List[str], batch_size: Optional[int] = None
219
+ ) -> List[List[float]]:
220
+ """
221
+ Generate embeddings with batching for large document sets.
222
+
223
+ Args:
224
+ texts: List of text strings to embed
225
+ batch_size: Override default batch size (for vLLM, can handle large batches)
226
+
227
+ Returns:
228
+ List of embedding vectors
229
+ """
230
+ if not texts:
231
+ return []
232
+
233
+ # vLLM can handle large batches efficiently
234
+ effective_batch_size = batch_size or 100
235
+ all_embeddings = []
236
+
237
+ for i in range(0, len(texts), effective_batch_size):
238
+ batch = texts[i : i + effective_batch_size]
239
+ embeddings = await self._call_vllm_api(batch)
240
+ all_embeddings.extend(embeddings)
241
+
242
+ return all_embeddings
243
+
244
+ def get_model_info(self) -> dict:
245
+ """Get information about the vLLM embedding service"""
246
+ return {
247
+ "backend": "vllm",
248
+ "endpoint": self._endpoint,
249
+ "model_name": self._model,
250
+ "dimension": self._dimension,
251
+ }
252
+
253
+ async def close(self):
254
+ """Close HTTP client connection"""
255
+ await self._client.aclose()
256
+
257
+
258
+ # ============ Singleton Accessor ============
259
+
260
+ _vllm_embedding_service: Optional[VLLMEmbeddingService] = None
261
+
262
+
263
+ def get_vllm_embedding_service(
264
+ config: Optional["EmbeddingConfig"] = None,
265
+ ) -> VLLMEmbeddingService:
266
+ """
267
+ Get the singleton VLLMEmbeddingService instance.
268
+
269
+ Args:
270
+ config: Optional EmbeddingConfig (only used on first call)
271
+
272
+ Returns:
273
+ VLLMEmbeddingService singleton instance
274
+ """
275
+ global _vllm_embedding_service
276
+ if _vllm_embedding_service is None:
277
+ _vllm_embedding_service = VLLMEmbeddingService(config)
278
+ return _vllm_embedding_service
279
+
280
+
281
+ def reset_vllm_embedding_service() -> None:
282
+ """
283
+ Reset the singleton instance (for testing purposes).
284
+ """
285
+ global _vllm_embedding_service
286
+ if _vllm_embedding_service is not None:
287
+ _vllm_embedding_service._initialized = False
288
+ _vllm_embedding_service = None
289
+ VLLMEmbeddingService._instance = None
290
+ VLLMEmbeddingService._initialized = False
@@ -0,0 +1 @@
1
+ """Codebuff Core - Tests"""