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,690 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration Manager - Handle extension settings persistence
|
|
3
|
+
|
|
4
|
+
Singleton manager for configuration persistence across sessions.
|
|
5
|
+
Includes both admin (LLM/server) and user (workspace/preferences) settings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from codebuff_core.legacy_paths import resolve
|
|
18
|
+
from codebuff_core.managers import remote_config
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_masked_api_key(value: str) -> bool:
|
|
22
|
+
"""Check if a value looks like a masked API key (e.g., 'sk-o...b459').
|
|
23
|
+
|
|
24
|
+
Masked keys are produced by _mask_api_key(): key[:4] + '...' + key[-4:]
|
|
25
|
+
resulting in 11-char strings, or '***' for short keys.
|
|
26
|
+
Real API keys are typically 30+ chars and don't contain '...'.
|
|
27
|
+
"""
|
|
28
|
+
if not isinstance(value, str) or not value:
|
|
29
|
+
return False
|
|
30
|
+
if value == "***":
|
|
31
|
+
return True
|
|
32
|
+
return "..." in value and len(value) < 20
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _strip_masked_keys(incoming: dict, existing: dict) -> dict:
|
|
36
|
+
"""Replace masked API key values in incoming config with existing real values.
|
|
37
|
+
|
|
38
|
+
When the frontend sends back masked keys (e.g., 'sk-o...b459'), this function
|
|
39
|
+
restores the real values from the existing config to prevent data corruption.
|
|
40
|
+
"""
|
|
41
|
+
import copy
|
|
42
|
+
|
|
43
|
+
cleaned = copy.deepcopy(incoming)
|
|
44
|
+
for key, value in list(cleaned.items()):
|
|
45
|
+
if isinstance(value, dict):
|
|
46
|
+
existing_section = existing.get(key, {})
|
|
47
|
+
if isinstance(existing_section, dict):
|
|
48
|
+
# Restore real apiKey if incoming is masked
|
|
49
|
+
# (rag.vllmApiKey 처럼 `...ApiKey` 로 끝나는 키도 가려서 나간다 — 이슈 #113)
|
|
50
|
+
for field in [
|
|
51
|
+
f for f in value if f == "apiKey" or f.endswith("ApiKey")
|
|
52
|
+
]:
|
|
53
|
+
if not _is_masked_api_key(value[field]):
|
|
54
|
+
continue
|
|
55
|
+
if field in existing_section and not _is_masked_api_key(
|
|
56
|
+
existing_section[field]
|
|
57
|
+
):
|
|
58
|
+
value[field] = existing_section[field]
|
|
59
|
+
else:
|
|
60
|
+
del value[field]
|
|
61
|
+
# Restore real keys in keys array (e.g., keys[].key)
|
|
62
|
+
if "keys" in value and isinstance(value["keys"], list):
|
|
63
|
+
existing_keys = existing_section.get("keys", [])
|
|
64
|
+
existing_keys_by_id = {
|
|
65
|
+
k.get("id"): k
|
|
66
|
+
for k in existing_keys
|
|
67
|
+
if isinstance(k, dict) and "id" in k
|
|
68
|
+
}
|
|
69
|
+
for item in value["keys"]:
|
|
70
|
+
if (
|
|
71
|
+
isinstance(item, dict)
|
|
72
|
+
and "key" in item
|
|
73
|
+
and _is_masked_api_key(item["key"])
|
|
74
|
+
):
|
|
75
|
+
existing_item = existing_keys_by_id.get(item.get("id"))
|
|
76
|
+
if (
|
|
77
|
+
existing_item
|
|
78
|
+
and "key" in existing_item
|
|
79
|
+
and not _is_masked_api_key(existing_item["key"])
|
|
80
|
+
):
|
|
81
|
+
item["key"] = existing_item["key"]
|
|
82
|
+
elif key == "apiKey" and _is_masked_api_key(value):
|
|
83
|
+
if key in existing and not _is_masked_api_key(existing[key]):
|
|
84
|
+
cleaned[key] = existing[key]
|
|
85
|
+
else:
|
|
86
|
+
del cleaned[key]
|
|
87
|
+
return cleaned
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# codex 권한 모드의 허용값 (이슈 #88). 에이전트 엔진 선택은 이슈 #138 에서 지웠다 —
|
|
91
|
+
# 어느 엔진으로 보낼지는 패널의 모드 드롭다운이 정한다.
|
|
92
|
+
CODEX_PERMISSION_MODES = ("ask", "bypass")
|
|
93
|
+
# 아무것도 안 고른 사용자의 권한 모드 — ask 였다가 이슈 #180 에서 bypass 로 바꿨다.
|
|
94
|
+
# 사용자가 ask 를 골라 저장했으면 그 값이 이긴다. centri 사본은
|
|
95
|
+
# `schemas/config/codebuff_config_defaults.json`(scripts/dump_config_defaults.py)로 따라간다.
|
|
96
|
+
DEFAULT_CODEX_PERMISSION_MODE = "bypass"
|
|
97
|
+
|
|
98
|
+
# 추론 엔진 — centri 전역 설정 `features.inference_engine` (이슈 #155).
|
|
99
|
+
# Chat(사내 검색 포함, #183)·Agent·노트북 자동생성·EDA Agent 가 이 값 하나를 따른다.
|
|
100
|
+
# langchain 이면 Agent 는 deepagents, 나머지는 RAGChatAgent 로 돈다.
|
|
101
|
+
INFERENCE_ENGINES = remote_config.INFERENCE_ENGINES
|
|
102
|
+
DEFAULT_INFERENCE_ENGINE = "codex"
|
|
103
|
+
|
|
104
|
+
# agent 모드 게이트 — centri 조직 키 `features.agent_enabled` (이슈 #179).
|
|
105
|
+
# 값이 없으면 켜짐이다 (이슈 #236) — Agent 는 기본으로 보이고, centri 가 명시적으로
|
|
106
|
+
# false 를 보낼 때만 닫힌다(조직 끄기 스위치).
|
|
107
|
+
DEFAULT_AGENT_ENABLED = True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _coerce_choice(value: Any, choices: tuple, default: str) -> str:
|
|
111
|
+
"""열거형 설정을 읽을 때 허용값 밖이면 기본값으로 떨어뜨린다."""
|
|
112
|
+
return value if value in choices else default
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ConfigManager:
|
|
116
|
+
"""Manage configuration persistence"""
|
|
117
|
+
|
|
118
|
+
_instance = None
|
|
119
|
+
_config_file = None
|
|
120
|
+
|
|
121
|
+
def __init__(self):
|
|
122
|
+
try:
|
|
123
|
+
# 1순위: 환경변수
|
|
124
|
+
jupyter_config_dir = os.environ.get("JUPYTER_CONFIG_DIR")
|
|
125
|
+
|
|
126
|
+
# 2순위: 홈 디렉토리 ~/.jupyter
|
|
127
|
+
if not jupyter_config_dir:
|
|
128
|
+
# expanduser가 실패할 경우를 대비
|
|
129
|
+
try:
|
|
130
|
+
jupyter_config_dir = os.path.expanduser("~/.jupyter")
|
|
131
|
+
except Exception:
|
|
132
|
+
jupyter_config_dir = None
|
|
133
|
+
|
|
134
|
+
# 경로가 유효한지 체크하고, 없으면 생성 시도
|
|
135
|
+
if jupyter_config_dir:
|
|
136
|
+
config_path = Path(jupyter_config_dir)
|
|
137
|
+
try:
|
|
138
|
+
config_path.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
self._config_file = resolve(
|
|
140
|
+
config_path / "codebuff_config.json",
|
|
141
|
+
config_path / "hdsp_agent_config.json",
|
|
142
|
+
)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print(f"Warning: Cannot write to {jupyter_config_dir}: {e}")
|
|
145
|
+
self._config_file = None
|
|
146
|
+
|
|
147
|
+
# 3순위 (비상용): 쓰기 실패했거나 경로가 없으면 /tmp 사용
|
|
148
|
+
if not self._config_file:
|
|
149
|
+
tmp_dir = Path(tempfile.gettempdir()) / "jupyter_codebuff"
|
|
150
|
+
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
self._config_file = tmp_dir / "codebuff_config.json"
|
|
152
|
+
print(f"Using temporary config path: {self._config_file}")
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
# 최악의 경우: 메모리에서만 동작하도록 가짜 경로 설정 (서버 다운 방지)
|
|
156
|
+
print(f"Critical Error in ConfigManager init: {e}")
|
|
157
|
+
self._config_file = Path("/tmp/codebuff_config_fallback.json")
|
|
158
|
+
|
|
159
|
+
self._config = self._load_config()
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def get_instance(cls):
|
|
163
|
+
"""Get singleton instance"""
|
|
164
|
+
if cls._instance is None:
|
|
165
|
+
cls._instance = ConfigManager()
|
|
166
|
+
return cls._instance
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def reset_instance(cls):
|
|
170
|
+
"""Reset singleton instance (for testing)"""
|
|
171
|
+
cls._instance = None
|
|
172
|
+
|
|
173
|
+
def _load_config(self) -> Dict[str, Any]:
|
|
174
|
+
"""Load configuration from file"""
|
|
175
|
+
if not self._config_file.exists():
|
|
176
|
+
return self._default_config()
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
with open(self._config_file, "r") as f:
|
|
180
|
+
loaded = json.load(f)
|
|
181
|
+
# Merge with defaults to ensure all keys exist
|
|
182
|
+
defaults = self._default_config()
|
|
183
|
+
defaults.update(loaded)
|
|
184
|
+
# 옛 프롬프트 키 이름은 새 이름으로 읽는다 (이슈 #218). 파일은 다시 쓰지 않는다
|
|
185
|
+
defaults["prompts"] = remote_config.migrate_renamed_prompts(
|
|
186
|
+
defaults.get("prompts")
|
|
187
|
+
)
|
|
188
|
+
self._migrate_legacy_providers(defaults)
|
|
189
|
+
self._sanitize_masked_keys(defaults)
|
|
190
|
+
return defaults
|
|
191
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
192
|
+
print(f"Error loading config: {e}")
|
|
193
|
+
return self._default_config()
|
|
194
|
+
|
|
195
|
+
def _migrate_legacy_providers(self, config: Dict[str, Any]):
|
|
196
|
+
"""Auto-migrate gemini/openai provider to vllm."""
|
|
197
|
+
changed = False
|
|
198
|
+
if config.get("provider") in ("gemini", "openai"):
|
|
199
|
+
config["provider"] = "vllm"
|
|
200
|
+
changed = True
|
|
201
|
+
for key in ("gemini", "openai"):
|
|
202
|
+
if key in config:
|
|
203
|
+
del config[key]
|
|
204
|
+
changed = True
|
|
205
|
+
summ = config.get("summarization")
|
|
206
|
+
if isinstance(summ, dict) and summ.get("provider") in ("gemini", "openai"):
|
|
207
|
+
summ["provider"] = "vllm"
|
|
208
|
+
changed = True
|
|
209
|
+
if changed:
|
|
210
|
+
self._persist(config)
|
|
211
|
+
|
|
212
|
+
def _sanitize_masked_keys(self, config: Dict[str, Any]):
|
|
213
|
+
"""마스킹된 API 키를 빈 문자열로 교체하고 파일에 반영."""
|
|
214
|
+
changed = False
|
|
215
|
+
for section_key in ("vllm", "summarization", "embedding"):
|
|
216
|
+
section = config.get(section_key)
|
|
217
|
+
if isinstance(section, dict):
|
|
218
|
+
api_key = section.get("apiKey", "")
|
|
219
|
+
if _is_masked_api_key(api_key):
|
|
220
|
+
section["apiKey"] = ""
|
|
221
|
+
changed = True
|
|
222
|
+
if changed:
|
|
223
|
+
print(
|
|
224
|
+
"[ConfigManager] WARNING: 마스킹된 API 키가 감지되어 제거되었습니다. "
|
|
225
|
+
"설정에서 API 키를 다시 입력하세요."
|
|
226
|
+
)
|
|
227
|
+
self._persist(config)
|
|
228
|
+
|
|
229
|
+
def _persist(self, config: Dict[str, Any]):
|
|
230
|
+
"""Write config dict to file."""
|
|
231
|
+
try:
|
|
232
|
+
self._config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
with open(self._config_file, "w") as f:
|
|
234
|
+
json.dump(config, f, indent=2)
|
|
235
|
+
except IOError as e:
|
|
236
|
+
print(f"Warning: Failed to persist migrated config: {e}")
|
|
237
|
+
|
|
238
|
+
def _default_config(self) -> Dict[str, Any]:
|
|
239
|
+
"""Get default configuration (merged admin + user schema)"""
|
|
240
|
+
return {
|
|
241
|
+
# LLM Provider Settings
|
|
242
|
+
"provider": "vllm",
|
|
243
|
+
"vllm": {
|
|
244
|
+
"endpoint": "https://openrouter.ai/api/v1",
|
|
245
|
+
"apiKey": "",
|
|
246
|
+
"model": "openai/gpt-oss-120b",
|
|
247
|
+
},
|
|
248
|
+
# Summarization LLM
|
|
249
|
+
"summarization": {
|
|
250
|
+
"enabled": False,
|
|
251
|
+
"provider": "vllm",
|
|
252
|
+
"model": None,
|
|
253
|
+
"endpoint": "",
|
|
254
|
+
"apiKey": "",
|
|
255
|
+
},
|
|
256
|
+
# Embedding
|
|
257
|
+
"embedding": {
|
|
258
|
+
"provider": "openai",
|
|
259
|
+
"model": "text-embedding-3-small",
|
|
260
|
+
"apiKey": "",
|
|
261
|
+
"endpoint": "",
|
|
262
|
+
},
|
|
263
|
+
# RAG
|
|
264
|
+
"rag": {
|
|
265
|
+
"enabled": True,
|
|
266
|
+
"knowledgeBasePath": None,
|
|
267
|
+
"qdrantMode": "local",
|
|
268
|
+
"qdrantUrl": "http://localhost:6333",
|
|
269
|
+
"qdrantPath": None,
|
|
270
|
+
"collectionName": "hdsp_docs",
|
|
271
|
+
"embeddingBackend": "vllm",
|
|
272
|
+
"vllmEndpoint": "http://localhost:8000",
|
|
273
|
+
"vllmModel": "qwen3-embedding-8b",
|
|
274
|
+
"vllmDimension": 4096,
|
|
275
|
+
"vllmApiKey": "",
|
|
276
|
+
},
|
|
277
|
+
# Server Settings
|
|
278
|
+
"idleTimeout": 3600,
|
|
279
|
+
"maxOutputTokens": 4096,
|
|
280
|
+
# Prompts (None = use defaults)
|
|
281
|
+
# 키 목록의 정본은 remote_config.PROMPT_KEYS (이슈 #113)
|
|
282
|
+
"prompts": {k: None for k in remote_config.PROMPT_KEYS},
|
|
283
|
+
# Guardrails
|
|
284
|
+
"guardrails": {
|
|
285
|
+
"enableInput": True,
|
|
286
|
+
"enableOutput": True,
|
|
287
|
+
"maxInputLength": 50000,
|
|
288
|
+
"maxCodeLength": 10000,
|
|
289
|
+
},
|
|
290
|
+
# Logging
|
|
291
|
+
"rawLlmLogging": False,
|
|
292
|
+
# User Settings
|
|
293
|
+
"workspaceRoot": "",
|
|
294
|
+
"autoApprove": False,
|
|
295
|
+
"codexPermissionMode": DEFAULT_CODEX_PERMISSION_MODE,
|
|
296
|
+
"codex": {
|
|
297
|
+
"endpoint": "",
|
|
298
|
+
"apiKey": "",
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
def get_config(self) -> Dict[str, Any]:
|
|
303
|
+
"""Get current configuration"""
|
|
304
|
+
# Reload config from file to ensure we have the latest
|
|
305
|
+
self._config = self._load_config()
|
|
306
|
+
return self._effective(self._config)
|
|
307
|
+
|
|
308
|
+
def _effective(self, base: Dict[str, Any]) -> Dict[str, Any]:
|
|
309
|
+
"""pod 파일 층(`base`)에 centri 중앙 설정을 얹은 사본 (이슈 #113).
|
|
310
|
+
|
|
311
|
+
`self._config` 는 파일 층 그대로 둔다 — 저장할 때 서버 값이 파일로 새지 않게.
|
|
312
|
+
URL 이 없거나 한 번도 받지 못했으면 오늘과 같다(pod 파일). 한 번이라도 받았으면
|
|
313
|
+
조직 키는 서버 > 코드 기본값이다(파일 무시). 켜는 스위치가 시크릿 키가 아니라
|
|
314
|
+
centri 수신부의 첫 200 이라서, 수신부가 없는 동안 pod 파일 값이 사라지지 않는다.
|
|
315
|
+
"""
|
|
316
|
+
cfg = copy.deepcopy(base)
|
|
317
|
+
user_set = cfg.pop("_userSet", None)
|
|
318
|
+
snap = remote_config.get_client().snapshot()
|
|
319
|
+
_apply_connection(cfg, snap)
|
|
320
|
+
if snap is None:
|
|
321
|
+
return cfg
|
|
322
|
+
defaults = self._default_config()
|
|
323
|
+
for path in remote_config.ORG_SCHEMA:
|
|
324
|
+
if path in snap:
|
|
325
|
+
_set_path(cfg, path, snap[path])
|
|
326
|
+
else:
|
|
327
|
+
found, value = _get_path(defaults, path)
|
|
328
|
+
if found:
|
|
329
|
+
_set_path(cfg, path, value)
|
|
330
|
+
else:
|
|
331
|
+
_del_path(cfg, path)
|
|
332
|
+
chosen = set(user_set) if isinstance(user_set, list) else set()
|
|
333
|
+
for key in remote_config.USER_KEYS:
|
|
334
|
+
# 개인설정(centri 사용자 행) > pod 파일에서 고른 값 > 조직 기본값
|
|
335
|
+
for path in (f"user.{key}", f"userDefaults.{key}"):
|
|
336
|
+
if path not in snap:
|
|
337
|
+
continue
|
|
338
|
+
if path.startswith("userDefaults.") and key in chosen:
|
|
339
|
+
break
|
|
340
|
+
cfg[key] = snap[path]
|
|
341
|
+
break
|
|
342
|
+
return cfg
|
|
343
|
+
|
|
344
|
+
def save_config(self, config: Dict[str, Any]):
|
|
345
|
+
"""Save configuration to file - no validation"""
|
|
346
|
+
# Strip masked API keys to prevent overwriting real keys
|
|
347
|
+
config = _strip_masked_keys(config, self._config)
|
|
348
|
+
config = self._keep_file_org_values(config)
|
|
349
|
+
# Merge with existing config
|
|
350
|
+
self._config.update(config)
|
|
351
|
+
|
|
352
|
+
# Ensure config directory exists
|
|
353
|
+
self._config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
354
|
+
|
|
355
|
+
# Write to file
|
|
356
|
+
try:
|
|
357
|
+
with open(self._config_file, "w") as f:
|
|
358
|
+
json.dump(self._config, f, indent=2)
|
|
359
|
+
except IOError as e:
|
|
360
|
+
raise RuntimeError(f"Failed to save config: {e}")
|
|
361
|
+
|
|
362
|
+
def _keep_file_org_values(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
363
|
+
"""서버·env 가 정하는 키는 파일 값으로 되돌려 저장한다 (이슈 #113).
|
|
364
|
+
|
|
365
|
+
`get_config()` 사본을 통째로 저장하는 경로(RAG 관리자 등)가 서버 값을 파일에
|
|
366
|
+
쓰면, URL 을 지우고 재기동해도 옛 값으로 돌아가지 못한다. 사용자 기본값도
|
|
367
|
+
같다 — 사용자가 고른 값은 `update_user_config` 가 따로 쓴다. 접속 설정은 URL 이
|
|
368
|
+
없어도 env 가 얹히므로 늘 되돌린다 — 안 그러면 시크릿 API 키가 파일에 남는다.
|
|
369
|
+
"""
|
|
370
|
+
if config is self._config:
|
|
371
|
+
return config
|
|
372
|
+
config = copy.deepcopy(config)
|
|
373
|
+
paths = list(remote_config.CONNECTION_ENV) + list(remote_config.API_KEY_ENV)
|
|
374
|
+
if remote_config.get_client().enabled:
|
|
375
|
+
paths += list(remote_config.ORG_SCHEMA) + list(remote_config.USER_KEYS)
|
|
376
|
+
for path in paths:
|
|
377
|
+
top = path.split(".", 1)[0]
|
|
378
|
+
if top not in config:
|
|
379
|
+
continue
|
|
380
|
+
found, value = _get_path(self._config, path)
|
|
381
|
+
if found:
|
|
382
|
+
_set_path(config, path, copy.deepcopy(value))
|
|
383
|
+
else:
|
|
384
|
+
_del_path(config, path)
|
|
385
|
+
if config.get(top) == {} and top not in self._config:
|
|
386
|
+
del config[top]
|
|
387
|
+
return config
|
|
388
|
+
|
|
389
|
+
def get(self, key: str, default=None):
|
|
390
|
+
"""Get specific config value"""
|
|
391
|
+
return self._effective(self._config).get(key, default)
|
|
392
|
+
|
|
393
|
+
def set(self, key: str, value: Any):
|
|
394
|
+
"""Set specific config value"""
|
|
395
|
+
self._config[key] = value
|
|
396
|
+
self.save_config(self._config)
|
|
397
|
+
|
|
398
|
+
def update_config(self, updates: Dict[str, Any]):
|
|
399
|
+
"""Update configuration with partial updates (deep merge for nested dicts)"""
|
|
400
|
+
# Strip masked API keys to prevent overwriting real keys
|
|
401
|
+
updates = _strip_masked_keys(updates, self._config)
|
|
402
|
+
for key, value in updates.items():
|
|
403
|
+
if (
|
|
404
|
+
key in self._config
|
|
405
|
+
and isinstance(self._config[key], dict)
|
|
406
|
+
and isinstance(value, dict)
|
|
407
|
+
):
|
|
408
|
+
self._config[key].update(value)
|
|
409
|
+
else:
|
|
410
|
+
self._config[key] = value
|
|
411
|
+
self.save_config(self._config)
|
|
412
|
+
|
|
413
|
+
def get_codex_settings(self) -> Dict[str, str]:
|
|
414
|
+
"""codex 접속 설정 — 주소는 centri > env > 설정 파일, 키는 env > 설정 파일 (이슈 #88·#113).
|
|
415
|
+
|
|
416
|
+
endpoint 가 빈 문자열이면 codex 엔진을 쓸 수 없는 상태다.
|
|
417
|
+
"""
|
|
418
|
+
codex = self.get_config().get("codex")
|
|
419
|
+
if not isinstance(codex, dict):
|
|
420
|
+
return {"endpoint": "", "apiKey": ""}
|
|
421
|
+
return {
|
|
422
|
+
"endpoint": codex.get("endpoint", "") or "",
|
|
423
|
+
"apiKey": codex.get("apiKey", "") or "",
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
def get_inference_engine(self) -> str:
|
|
427
|
+
"""전역 추론 엔진 — 값이 없거나 허용값 밖이면 codex (이슈 #155)."""
|
|
428
|
+
features = self.get_config().get("features")
|
|
429
|
+
value = features.get("inference_engine") if isinstance(features, dict) else None
|
|
430
|
+
return _coerce_choice(value, INFERENCE_ENGINES, DEFAULT_INFERENCE_ENGINE)
|
|
431
|
+
|
|
432
|
+
def get_agent_enabled(self) -> bool:
|
|
433
|
+
"""agent 모드 실효값 — centri > (못 받았으면) pod 파일 > 기본값 (이슈 #179)."""
|
|
434
|
+
features = self.get_config().get("features")
|
|
435
|
+
value = features.get("agent_enabled") if isinstance(features, dict) else None
|
|
436
|
+
return value if isinstance(value, bool) else DEFAULT_AGENT_ENABLED
|
|
437
|
+
|
|
438
|
+
def get_user_config(self) -> Dict[str, Any]:
|
|
439
|
+
"""Get user configuration (workspace, autoApprove, codex 권한). temperature 는 0.0 고정 (이슈 #171)."""
|
|
440
|
+
config = self.get_config()
|
|
441
|
+
return {
|
|
442
|
+
"workspaceRoot": config.get("workspaceRoot", ""),
|
|
443
|
+
"autoApprove": config.get("autoApprove", False),
|
|
444
|
+
"codexPermissionMode": _coerce_choice(
|
|
445
|
+
config.get("codexPermissionMode"),
|
|
446
|
+
CODEX_PERMISSION_MODES,
|
|
447
|
+
DEFAULT_CODEX_PERMISSION_MODE,
|
|
448
|
+
),
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
def update_user_config(self, updates: Dict[str, Any]):
|
|
452
|
+
"""Update user configuration (only allowed fields)."""
|
|
453
|
+
allowed_fields = {
|
|
454
|
+
"workspaceRoot",
|
|
455
|
+
"autoApprove",
|
|
456
|
+
"codexPermissionMode",
|
|
457
|
+
}
|
|
458
|
+
# 열거형 필드는 값까지 본다 — 화이트리스트만 보면 오타가 그대로 저장돼
|
|
459
|
+
# 읽는 쪽이 매번 폴백을 해야 한다 (이슈 #88).
|
|
460
|
+
enum_fields = {
|
|
461
|
+
"codexPermissionMode": CODEX_PERMISSION_MODES,
|
|
462
|
+
}
|
|
463
|
+
filtered_updates = {
|
|
464
|
+
k: v
|
|
465
|
+
for k, v in updates.items()
|
|
466
|
+
if k in allowed_fields and (k not in enum_fields or v in enum_fields[k])
|
|
467
|
+
}
|
|
468
|
+
if not filtered_updates:
|
|
469
|
+
return
|
|
470
|
+
client = remote_config.get_client()
|
|
471
|
+
if client.can_save_user:
|
|
472
|
+
self._save_user_remote(client, filtered_updates)
|
|
473
|
+
return
|
|
474
|
+
self._track_user_choices(filtered_updates)
|
|
475
|
+
self.update_config(filtered_updates)
|
|
476
|
+
|
|
477
|
+
def _save_user_remote(self, client, updates: Dict[str, Any]):
|
|
478
|
+
"""개인설정을 centri 사용자 행에 쓴다 (이슈 #113). 실패는 예외로 올린다.
|
|
479
|
+
|
|
480
|
+
조직 기본값과 같은 값은 행을 지운다 — 패널은 모든 칸을 보내므로, 그대로 두면 한 번
|
|
481
|
+
저장한 사용자는 조직 기본값이 바뀌어도 영영 못 받는다. 저장한 키는 pod 파일의
|
|
482
|
+
`_userSet` 에서 뺀다: 서버 행을 지웠을 때 파일의 옛 선택이 되살아나지 않게.
|
|
483
|
+
"""
|
|
484
|
+
values = remote_config.sanitize_user(updates)
|
|
485
|
+
baseline = self._org_user_defaults()
|
|
486
|
+
set_values = {k: v for k, v in values.items() if v != baseline.get(k)}
|
|
487
|
+
unset = [k for k in values if k not in set_values]
|
|
488
|
+
client.save_user(set_values, unset)
|
|
489
|
+
# 저장한 키는 pod 파일 값도 코드 기본값으로 되돌린다. 조직 기본값과 같은 값을 골라
|
|
490
|
+
# 서버 행을 지웠는데 스냅샷에 `userDefaults.<키>` 가 없으면, 파일의 옛 값이 다시 보인다.
|
|
491
|
+
defaults = self._default_config()
|
|
492
|
+
changed = False
|
|
493
|
+
for key in values:
|
|
494
|
+
if key in defaults and self._config.get(key) != defaults[key]:
|
|
495
|
+
self._config[key] = defaults[key]
|
|
496
|
+
changed = True
|
|
497
|
+
elif key not in defaults and key in self._config:
|
|
498
|
+
del self._config[key]
|
|
499
|
+
changed = True
|
|
500
|
+
chosen = self._config.get("_userSet")
|
|
501
|
+
if isinstance(chosen, list) and set(chosen) & set(values):
|
|
502
|
+
self._config["_userSet"] = sorted(set(chosen) - set(values))
|
|
503
|
+
changed = True
|
|
504
|
+
if changed:
|
|
505
|
+
self.save_config(self._config)
|
|
506
|
+
|
|
507
|
+
def describe_sources(self) -> List[Dict[str, Any]]:
|
|
508
|
+
"""설정 화면 "센트리에서 받은 설정" 의 행 — 허용 목록 키의 실효값과 출처 (이슈 #183).
|
|
509
|
+
|
|
510
|
+
출처는 `portal`(centri) · `env` · `file`(pod 파일) · `default`(코드 기본값) 중 하나이고
|
|
511
|
+
`_effective` 의 우선순위를 그대로 따른다. API 키 경로는 허용 목록에 없다.
|
|
512
|
+
프롬프트는 본문 대신 글자 수를 싣고, 정한 것만 싣는다 — 안 정한 것은 스냅샷 기본값
|
|
513
|
+
(`codebuff_config_defaults.json`)이다 (이슈 #218).
|
|
514
|
+
프롬프트 행에는 그 키를 읽는 엔진·자리(`appliesTo`)를 붙인다 (이슈 #208).
|
|
515
|
+
"""
|
|
516
|
+
base = self._load_config()
|
|
517
|
+
snap = remote_config.get_client().snapshot()
|
|
518
|
+
effective = self._effective(base)
|
|
519
|
+
defaults = self._default_config()
|
|
520
|
+
rows: List[Dict[str, Any]] = []
|
|
521
|
+
for path in list(remote_config.CONNECTION_ENV) + list(remote_config.ORG_SCHEMA):
|
|
522
|
+
connection = path in remote_config.CONNECTION_ENV
|
|
523
|
+
if snap is not None and path in snap:
|
|
524
|
+
source = "portal"
|
|
525
|
+
elif connection and _connection_env(path) is not None:
|
|
526
|
+
source = "env"
|
|
527
|
+
elif (snap is None or connection) and _in_file(base, defaults, path):
|
|
528
|
+
source = "file"
|
|
529
|
+
else:
|
|
530
|
+
source = "default"
|
|
531
|
+
_, value = _get_path(effective, path)
|
|
532
|
+
if path in _FEATURE_EFFECTIVE:
|
|
533
|
+
# 안 정한 기능 키는 부르는 쪽이 기본값을 채운다 — 표도 그 값을 보인다
|
|
534
|
+
value = _FEATURE_EFFECTIVE[path](self)
|
|
535
|
+
if path.startswith("prompts."):
|
|
536
|
+
if not isinstance(value, str) or not value.strip():
|
|
537
|
+
continue
|
|
538
|
+
# 옛 프론트가 파일에 남긴 로딩 문구는 값이 아니다 (configured_prompt 와 같은 판정)
|
|
539
|
+
if value == "(프롬프트를 불러오는 중...)":
|
|
540
|
+
continue
|
|
541
|
+
value = len(value)
|
|
542
|
+
engines, where = remote_config.PROMPT_KEY_SCOPES[
|
|
543
|
+
path[len("prompts.") :]
|
|
544
|
+
]
|
|
545
|
+
rows.append(
|
|
546
|
+
{
|
|
547
|
+
"key": path,
|
|
548
|
+
"value": value,
|
|
549
|
+
"source": source,
|
|
550
|
+
# 이 키를 읽는 엔진·자리 — 화면이 현재 엔진과 대조한다 (이슈 #208)
|
|
551
|
+
"appliesTo": {"engines": list(engines), "where": where},
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
continue
|
|
555
|
+
rows.append({"key": path, "value": value, "source": source})
|
|
556
|
+
return rows
|
|
557
|
+
|
|
558
|
+
def _org_user_defaults(self) -> Dict[str, Any]:
|
|
559
|
+
"""개인설정 칸마다 사용자가 아무것도 안 골랐을 때 보이는 값."""
|
|
560
|
+
snap = remote_config.get_client().snapshot() or {}
|
|
561
|
+
defaults = self._default_config()
|
|
562
|
+
return {
|
|
563
|
+
key: snap.get(f"userDefaults.{key}", defaults.get(key))
|
|
564
|
+
for key in remote_config.USER_KEYS
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
def _track_user_choices(self, updates: Dict[str, Any]):
|
|
568
|
+
"""조직 기본값과 다른 값을 고른 키만 `_userSet` 에 남긴다 (이슈 #113).
|
|
569
|
+
|
|
570
|
+
패널은 필드를 한꺼번에 저장하므로 "저장했다" 가 곧 "골랐다" 가 아니다. 손대지
|
|
571
|
+
않은 값까지 고정하면 조직 기본값이 바뀌어도 그 사용자에게는 영영 안 닿는다.
|
|
572
|
+
"""
|
|
573
|
+
snap = remote_config.get_client().snapshot() or {}
|
|
574
|
+
defaults = self._default_config()
|
|
575
|
+
chosen = self._config.get("_userSet")
|
|
576
|
+
chosen = set(chosen) if isinstance(chosen, list) else set()
|
|
577
|
+
for key in remote_config.USER_DEFAULT_KEYS:
|
|
578
|
+
if key not in updates:
|
|
579
|
+
continue
|
|
580
|
+
baseline = snap.get(f"userDefaults.{key}", defaults.get(key))
|
|
581
|
+
if updates[key] == baseline:
|
|
582
|
+
chosen.discard(key)
|
|
583
|
+
else:
|
|
584
|
+
chosen.add(key)
|
|
585
|
+
self._config["_userSet"] = sorted(chosen)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _apply_connection(cfg: Dict[str, Any], snap: Optional[Dict[str, Any]]):
|
|
589
|
+
"""접속 설정을 얹는다 — 주소·모델은 centri > env > pod 파일, API 키는 env > pod 파일.
|
|
590
|
+
|
|
591
|
+
API 키는 centri 로도 관리 화면으로도 오지 않는다. 시크릿 env 가 유일한 운영 경로다.
|
|
592
|
+
"""
|
|
593
|
+
for path in remote_config.CONNECTION_ENV:
|
|
594
|
+
if snap is not None and path in snap:
|
|
595
|
+
_set_path(cfg, path, snap[path])
|
|
596
|
+
continue
|
|
597
|
+
value = _connection_env(path)
|
|
598
|
+
if value is not None:
|
|
599
|
+
_set_path(cfg, path, value)
|
|
600
|
+
for path, env in remote_config.API_KEY_ENV.items():
|
|
601
|
+
raw = os.environ.get(env, "")
|
|
602
|
+
if raw:
|
|
603
|
+
_set_path(cfg, path, raw)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _connection_env(path: str) -> Any:
|
|
607
|
+
"""접속 키의 env 값 — 비었거나 규격 밖(차원이 숫자가 아님)이면 None."""
|
|
608
|
+
raw = os.environ.get(remote_config.CONNECTION_ENV[path], "").strip()
|
|
609
|
+
if not raw:
|
|
610
|
+
return None
|
|
611
|
+
if path == "rag.vllmDimension":
|
|
612
|
+
return int(raw) if raw.isdigit() else None
|
|
613
|
+
return raw
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _in_file(base: Dict[str, Any], defaults: Dict[str, Any], path: str) -> bool:
|
|
617
|
+
"""파일이 이 키를 기본값과 다르게 정했는가. 파일에 없는 키는 파일 출처가 아니다."""
|
|
618
|
+
found, value = _get_path(base, path)
|
|
619
|
+
return found and (found, value) != _get_path(defaults, path)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# 기본값이 `_default_config()` 가 아니라 게터에 있는 기능 키 (이슈 #183)
|
|
623
|
+
_FEATURE_EFFECTIVE = {
|
|
624
|
+
"features.eda_enabled": lambda cm: _feature(cm, "eda_enabled") is not False,
|
|
625
|
+
"features.bdp_qa_enabled": lambda cm: _feature(cm, "bdp_qa_enabled") is not False,
|
|
626
|
+
"features.agent_enabled": lambda cm: cm.get_agent_enabled(),
|
|
627
|
+
"features.inference_engine": lambda cm: cm.get_inference_engine(),
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _feature(cm: "ConfigManager", key: str) -> Any:
|
|
632
|
+
features = cm.get_config().get("features")
|
|
633
|
+
return features.get(key) if isinstance(features, dict) else None
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _get_path(cfg: Dict[str, Any], path: str):
|
|
637
|
+
node: Any = cfg
|
|
638
|
+
for part in path.split("."):
|
|
639
|
+
if not isinstance(node, dict) or part not in node:
|
|
640
|
+
return False, None
|
|
641
|
+
node = node[part]
|
|
642
|
+
return True, node
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _set_path(cfg: Dict[str, Any], path: str, value: Any):
|
|
646
|
+
*parents, leaf = path.split(".")
|
|
647
|
+
node = cfg
|
|
648
|
+
for part in parents:
|
|
649
|
+
if not isinstance(node.get(part), dict):
|
|
650
|
+
node[part] = {}
|
|
651
|
+
node = node[part]
|
|
652
|
+
node[leaf] = value
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _del_path(cfg: Dict[str, Any], path: str):
|
|
656
|
+
*parents, leaf = path.split(".")
|
|
657
|
+
node: Any = cfg
|
|
658
|
+
for part in parents:
|
|
659
|
+
node = node.get(part) if isinstance(node, dict) else None
|
|
660
|
+
if isinstance(node, dict):
|
|
661
|
+
node.pop(leaf, None)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def get_config_manager() -> ConfigManager:
|
|
665
|
+
"""Get the singleton ConfigManager instance"""
|
|
666
|
+
return ConfigManager.get_instance()
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def configured_prompt(key: str) -> str:
|
|
670
|
+
"""관리자가 정한 `prompts.<key>` (centri > pod 파일). 없으면 빈 문자열 (이슈 #178).
|
|
671
|
+
|
|
672
|
+
빈 문자열이면 부르는 쪽이 스냅샷 기본값을 쓴다(`prompt_value`). 프롬프트 조립을 설정 오류로 막지 않는다.
|
|
673
|
+
"""
|
|
674
|
+
try:
|
|
675
|
+
value = get_config_manager().get_config().get("prompts", {}).get(key)
|
|
676
|
+
except Exception:
|
|
677
|
+
return ""
|
|
678
|
+
if not isinstance(value, str) or not value.strip():
|
|
679
|
+
return ""
|
|
680
|
+
# 옛 프론트가 파일에 남긴 로딩 문구는 값이 아니다 (agent_factory 와 같은 판정)
|
|
681
|
+
return "" if value == "(프롬프트를 불러오는 중...)" else value
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def prompt_value(key: str) -> str:
|
|
685
|
+
"""`prompts.<key>` 의 실효값 — 관리자 값(centri > pod 파일), 없으면 스냅샷 기본값 (이슈 #218).
|
|
686
|
+
|
|
687
|
+
코드 상수 폴백은 없다. 기본값의 정본은 centri 이고 codebuff 는 그것을 옮긴
|
|
688
|
+
`codebuff_config_defaults.json` 만 들고 있다.
|
|
689
|
+
"""
|
|
690
|
+
return configured_prompt(key) or remote_config.snapshot_prompt(key)
|