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,296 @@
|
|
|
1
|
+
"""Jupyter 노트북 도구."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from .base import BaseTool, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class JupyterCellTool(BaseTool):
|
|
11
|
+
"""Jupyter 셀 작업 도구."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, kernel_manager: Any, contents_manager: Any):
|
|
14
|
+
self._kernel_manager = kernel_manager
|
|
15
|
+
self._contents_manager = contents_manager
|
|
16
|
+
self._notebook_path: Optional[str] = None
|
|
17
|
+
self._notebook_model: Optional[dict] = None
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return "jupyter_cell"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def description(self) -> str:
|
|
25
|
+
return """Create, read, update, delete, or execute Jupyter notebook cells.
|
|
26
|
+
Operations:
|
|
27
|
+
- CREATE: Create a new code cell with the given code
|
|
28
|
+
- READ: Read the content of a cell at the given index
|
|
29
|
+
- UPDATE: Update the code of an existing cell
|
|
30
|
+
- DELETE: Delete a cell at the given index
|
|
31
|
+
- EXECUTE: Execute a cell and return the output"""
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def parameters(self) -> dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"operation": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"enum": ["CREATE", "READ", "UPDATE", "DELETE", "EXECUTE"],
|
|
41
|
+
"description": "The operation to perform",
|
|
42
|
+
},
|
|
43
|
+
"code": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "The code content (for CREATE, UPDATE, EXECUTE)",
|
|
46
|
+
},
|
|
47
|
+
"cell_index": {
|
|
48
|
+
"type": "integer",
|
|
49
|
+
"description": "The cell index (for READ, UPDATE, DELETE, EXECUTE)",
|
|
50
|
+
},
|
|
51
|
+
"insert_after": {
|
|
52
|
+
"type": "integer",
|
|
53
|
+
"description": "Insert new cell after this index (for CREATE)",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
"required": ["operation"],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def risk_level(self) -> str:
|
|
61
|
+
return "medium"
|
|
62
|
+
|
|
63
|
+
def set_notebook(self, path: str, model: dict) -> None:
|
|
64
|
+
"""현재 노트북 컨텍스트를 설정합니다."""
|
|
65
|
+
self._notebook_path = path
|
|
66
|
+
self._notebook_model = model
|
|
67
|
+
|
|
68
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
69
|
+
"""Jupyter 셀 작업을 실행합니다."""
|
|
70
|
+
operation = kwargs.get("operation", "").upper()
|
|
71
|
+
|
|
72
|
+
if operation == "CREATE":
|
|
73
|
+
return await self._create_cell(
|
|
74
|
+
kwargs.get("code", ""), kwargs.get("insert_after")
|
|
75
|
+
)
|
|
76
|
+
elif operation == "READ":
|
|
77
|
+
return await self._read_cell(kwargs.get("cell_index", 0))
|
|
78
|
+
elif operation == "UPDATE":
|
|
79
|
+
return await self._update_cell(
|
|
80
|
+
kwargs.get("cell_index", 0), kwargs.get("code", "")
|
|
81
|
+
)
|
|
82
|
+
elif operation == "DELETE":
|
|
83
|
+
return await self._delete_cell(kwargs.get("cell_index", 0))
|
|
84
|
+
elif operation == "EXECUTE":
|
|
85
|
+
return await self._execute_cell(
|
|
86
|
+
kwargs.get("cell_index"), kwargs.get("code")
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
return ToolResult(success=False, error=f"알 수 없는 연산: {operation}")
|
|
90
|
+
|
|
91
|
+
async def _create_cell(
|
|
92
|
+
self, code: str, insert_after: Optional[int] = None
|
|
93
|
+
) -> ToolResult:
|
|
94
|
+
"""새 셀을 생성합니다."""
|
|
95
|
+
if not self._notebook_model:
|
|
96
|
+
return ToolResult(success=False, error="노트북 컨텍스트가 설정되지 않음")
|
|
97
|
+
|
|
98
|
+
cells = self._notebook_model.get("content", {}).get("cells", [])
|
|
99
|
+
new_cell = {
|
|
100
|
+
"cell_type": "code",
|
|
101
|
+
"source": code,
|
|
102
|
+
"metadata": {},
|
|
103
|
+
"outputs": [],
|
|
104
|
+
"execution_count": None,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if insert_after is not None and 0 <= insert_after < len(cells):
|
|
108
|
+
cells.insert(insert_after + 1, new_cell)
|
|
109
|
+
cell_index = insert_after + 1
|
|
110
|
+
else:
|
|
111
|
+
cells.append(new_cell)
|
|
112
|
+
cell_index = len(cells) - 1
|
|
113
|
+
|
|
114
|
+
return ToolResult(
|
|
115
|
+
success=True,
|
|
116
|
+
data={"cell_index": cell_index, "operation": "created"},
|
|
117
|
+
metadata={"cell_type": "code"},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
async def _read_cell(self, cell_index: int) -> ToolResult:
|
|
121
|
+
"""셀 내용을 읽습니다."""
|
|
122
|
+
if not self._notebook_model:
|
|
123
|
+
return ToolResult(success=False, error="노트북 컨텍스트가 설정되지 않음")
|
|
124
|
+
|
|
125
|
+
cells = self._notebook_model.get("content", {}).get("cells", [])
|
|
126
|
+
if not 0 <= cell_index < len(cells):
|
|
127
|
+
return ToolResult(success=False, error=f"잘못된 셀 인덱스: {cell_index}")
|
|
128
|
+
|
|
129
|
+
cell = cells[cell_index]
|
|
130
|
+
return ToolResult(
|
|
131
|
+
success=True,
|
|
132
|
+
data={
|
|
133
|
+
"source": cell.get("source", ""),
|
|
134
|
+
"cell_type": cell.get("cell_type", "code"),
|
|
135
|
+
"outputs": cell.get("outputs", []),
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def _update_cell(self, cell_index: int, code: str) -> ToolResult:
|
|
140
|
+
"""셀 내용을 업데이트합니다."""
|
|
141
|
+
if not self._notebook_model:
|
|
142
|
+
return ToolResult(success=False, error="노트북 컨텍스트가 설정되지 않음")
|
|
143
|
+
|
|
144
|
+
cells = self._notebook_model.get("content", {}).get("cells", [])
|
|
145
|
+
if not 0 <= cell_index < len(cells):
|
|
146
|
+
return ToolResult(success=False, error=f"잘못된 셀 인덱스: {cell_index}")
|
|
147
|
+
|
|
148
|
+
cells[cell_index]["source"] = code
|
|
149
|
+
return ToolResult(
|
|
150
|
+
success=True,
|
|
151
|
+
data={"cell_index": cell_index, "operation": "updated"},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def _delete_cell(self, cell_index: int) -> ToolResult:
|
|
155
|
+
"""셀을 삭제합니다."""
|
|
156
|
+
if not self._notebook_model:
|
|
157
|
+
return ToolResult(success=False, error="노트북 컨텍스트가 설정되지 않음")
|
|
158
|
+
|
|
159
|
+
cells = self._notebook_model.get("content", {}).get("cells", [])
|
|
160
|
+
if not 0 <= cell_index < len(cells):
|
|
161
|
+
return ToolResult(success=False, error=f"잘못된 셀 인덱스: {cell_index}")
|
|
162
|
+
|
|
163
|
+
del cells[cell_index]
|
|
164
|
+
return ToolResult(
|
|
165
|
+
success=True,
|
|
166
|
+
data={"cell_index": cell_index, "operation": "deleted"},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
async def _execute_cell(
|
|
170
|
+
self, cell_index: Optional[int], code: Optional[str]
|
|
171
|
+
) -> ToolResult:
|
|
172
|
+
"""셀 또는 코드를 실행합니다."""
|
|
173
|
+
# 코드가 직접 제공되면 실행
|
|
174
|
+
if code:
|
|
175
|
+
return await self._execute_code(code)
|
|
176
|
+
|
|
177
|
+
# 그렇지 않으면 주어진 인덱스의 셀 실행
|
|
178
|
+
if cell_index is not None and self._notebook_model:
|
|
179
|
+
cells = self._notebook_model.get("content", {}).get("cells", [])
|
|
180
|
+
if 0 <= cell_index < len(cells):
|
|
181
|
+
cell_code = cells[cell_index].get("source", "")
|
|
182
|
+
return await self._execute_code(cell_code)
|
|
183
|
+
|
|
184
|
+
return ToolResult(
|
|
185
|
+
success=False, error="코드 또는 유효한 셀 인덱스가 제공되지 않음"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
async def _execute_code(self, code: str) -> ToolResult:
|
|
189
|
+
"""커널에서 코드를 실행합니다."""
|
|
190
|
+
# 실제 커널과 통합됨
|
|
191
|
+
# 현재는 플레이스홀더 반환
|
|
192
|
+
return ToolResult(
|
|
193
|
+
success=True,
|
|
194
|
+
data={
|
|
195
|
+
"output": "코드 실행 플레이스홀더",
|
|
196
|
+
"execution_count": 1,
|
|
197
|
+
},
|
|
198
|
+
metadata={"executed": True},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class JupyterMarkdownTool(BaseTool):
|
|
203
|
+
"""Jupyter 마크다운 셀 작업 도구."""
|
|
204
|
+
|
|
205
|
+
def __init__(self, contents_manager: Any):
|
|
206
|
+
self._contents_manager = contents_manager
|
|
207
|
+
self._notebook_model: Optional[dict] = None
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def name(self) -> str:
|
|
211
|
+
return "jupyter_markdown"
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def description(self) -> str:
|
|
215
|
+
return """Create or update markdown cells in the Jupyter notebook.
|
|
216
|
+
Use this for adding documentation, explanations, or headers."""
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def parameters(self) -> dict[str, Any]:
|
|
220
|
+
return {
|
|
221
|
+
"type": "object",
|
|
222
|
+
"properties": {
|
|
223
|
+
"operation": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"enum": ["CREATE", "UPDATE"],
|
|
226
|
+
"description": "The operation to perform",
|
|
227
|
+
},
|
|
228
|
+
"content": {
|
|
229
|
+
"type": "string",
|
|
230
|
+
"description": "The markdown content",
|
|
231
|
+
},
|
|
232
|
+
"cell_index": {
|
|
233
|
+
"type": "integer",
|
|
234
|
+
"description": "The cell index (for UPDATE)",
|
|
235
|
+
},
|
|
236
|
+
"insert_after": {
|
|
237
|
+
"type": "integer",
|
|
238
|
+
"description": "Insert new cell after this index (for CREATE)",
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
"required": ["operation", "content"],
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def risk_level(self) -> str:
|
|
246
|
+
return "low"
|
|
247
|
+
|
|
248
|
+
def set_notebook(self, model: dict) -> None:
|
|
249
|
+
"""현재 노트북 컨텍스트를 설정합니다."""
|
|
250
|
+
self._notebook_model = model
|
|
251
|
+
|
|
252
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
253
|
+
"""마크다운 셀 작업을 실행합니다."""
|
|
254
|
+
operation = kwargs.get("operation", "").upper()
|
|
255
|
+
content = kwargs.get("content", "")
|
|
256
|
+
|
|
257
|
+
if not self._notebook_model:
|
|
258
|
+
return ToolResult(success=False, error="노트북 컨텍스트가 설정되지 않음")
|
|
259
|
+
|
|
260
|
+
cells = self._notebook_model.get("content", {}).get("cells", [])
|
|
261
|
+
|
|
262
|
+
if operation == "CREATE":
|
|
263
|
+
new_cell = {
|
|
264
|
+
"cell_type": "markdown",
|
|
265
|
+
"source": content,
|
|
266
|
+
"metadata": {},
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
insert_after = kwargs.get("insert_after")
|
|
270
|
+
if insert_after is not None and 0 <= insert_after < len(cells):
|
|
271
|
+
cells.insert(insert_after + 1, new_cell)
|
|
272
|
+
cell_index = insert_after + 1
|
|
273
|
+
else:
|
|
274
|
+
cells.append(new_cell)
|
|
275
|
+
cell_index = len(cells) - 1
|
|
276
|
+
|
|
277
|
+
return ToolResult(
|
|
278
|
+
success=True,
|
|
279
|
+
data={"cell_index": cell_index, "operation": "created"},
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
elif operation == "UPDATE":
|
|
283
|
+
cell_index = kwargs.get("cell_index", 0)
|
|
284
|
+
if not 0 <= cell_index < len(cells):
|
|
285
|
+
return ToolResult(
|
|
286
|
+
success=False, error=f"잘못된 셀 인덱스: {cell_index}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
cells[cell_index]["source"] = content
|
|
290
|
+
cells[cell_index]["cell_type"] = "markdown"
|
|
291
|
+
return ToolResult(
|
|
292
|
+
success=True,
|
|
293
|
+
data={"cell_index": cell_index, "operation": "updated"},
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return ToolResult(success=False, error=f"알 수 없는 연산: {operation}")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""도구 관리 및 실행을 위한 도구 레지스트리."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from codebuff_core.core.exceptions import ToolError
|
|
8
|
+
|
|
9
|
+
from .base import BaseTool, ToolResult
|
|
10
|
+
from .jupyter_tools import JupyterCellTool, JupyterMarkdownTool
|
|
11
|
+
from .file_tools import FileReadTool, FileWriteTool, FileListTool
|
|
12
|
+
from .shell_tools import ShellExecuteTool
|
|
13
|
+
from .search_tools import SearchCodeTool, WebSearchTool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolRegistry:
|
|
17
|
+
"""도구 관리를 위한 레지스트리."""
|
|
18
|
+
|
|
19
|
+
# HITL 승인이 필요한 도구들
|
|
20
|
+
HIGH_RISK_TOOLS = {"shell_execute", "file_write"}
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
kernel_manager: Any = None,
|
|
25
|
+
contents_manager: Any = None,
|
|
26
|
+
base_path: str = ".",
|
|
27
|
+
tavily_api_key: Optional[str] = None,
|
|
28
|
+
):
|
|
29
|
+
self._tools: dict[str, BaseTool] = {}
|
|
30
|
+
self._base_path = base_path
|
|
31
|
+
self._kernel_manager = kernel_manager
|
|
32
|
+
self._contents_manager = contents_manager
|
|
33
|
+
|
|
34
|
+
self._register_default_tools(tavily_api_key)
|
|
35
|
+
|
|
36
|
+
def _register_default_tools(self, tavily_api_key: Optional[str] = None) -> None:
|
|
37
|
+
"""모든 기본 도구를 등록합니다."""
|
|
38
|
+
# Jupyter 도구
|
|
39
|
+
self._tools["jupyter_cell"] = JupyterCellTool(
|
|
40
|
+
self._kernel_manager,
|
|
41
|
+
self._contents_manager,
|
|
42
|
+
)
|
|
43
|
+
self._tools["jupyter_markdown"] = JupyterMarkdownTool(
|
|
44
|
+
self._contents_manager,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# 파일 도구
|
|
48
|
+
self._tools["file_read"] = FileReadTool(
|
|
49
|
+
self._contents_manager,
|
|
50
|
+
self._base_path,
|
|
51
|
+
)
|
|
52
|
+
self._tools["file_write"] = FileWriteTool(
|
|
53
|
+
self._contents_manager,
|
|
54
|
+
self._base_path,
|
|
55
|
+
)
|
|
56
|
+
self._tools["file_list"] = FileListTool(
|
|
57
|
+
self._contents_manager,
|
|
58
|
+
self._base_path,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# 셸 도구
|
|
62
|
+
self._tools["shell_execute"] = ShellExecuteTool(
|
|
63
|
+
working_dir=self._base_path,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# 검색 도구
|
|
67
|
+
self._tools["search_code"] = SearchCodeTool(
|
|
68
|
+
self._base_path,
|
|
69
|
+
)
|
|
70
|
+
web_search = WebSearchTool(api_key=tavily_api_key)
|
|
71
|
+
self._tools["web_search"] = web_search
|
|
72
|
+
|
|
73
|
+
def register(self, tool: BaseTool) -> None:
|
|
74
|
+
"""도구를 등록합니다."""
|
|
75
|
+
self._tools[tool.name] = tool
|
|
76
|
+
|
|
77
|
+
def unregister(self, name: str) -> None:
|
|
78
|
+
"""도구 등록을 해제합니다."""
|
|
79
|
+
if name in self._tools:
|
|
80
|
+
del self._tools[name]
|
|
81
|
+
|
|
82
|
+
def get(self, name: str) -> Optional[BaseTool]:
|
|
83
|
+
"""이름으로 도구를 가져옵니다."""
|
|
84
|
+
return self._tools.get(name)
|
|
85
|
+
|
|
86
|
+
def get_definitions(self) -> list[dict[str, Any]]:
|
|
87
|
+
"""LLM용 모든 도구 정의를 가져옵니다."""
|
|
88
|
+
return [tool.definition for tool in self._tools.values()]
|
|
89
|
+
|
|
90
|
+
def requires_approval(self, name: str, args: dict[str, Any]) -> bool:
|
|
91
|
+
"""
|
|
92
|
+
도구 실행에 HITL 승인이 필요한지 확인합니다.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
name: 도구 이름
|
|
96
|
+
args: 도구 인자
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
승인이 필요하면 True
|
|
100
|
+
"""
|
|
101
|
+
# 고위험 도구인지 확인
|
|
102
|
+
if name in self.HIGH_RISK_TOOLS:
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
# 도구의 위험 수준 확인
|
|
106
|
+
tool = self._tools.get(name)
|
|
107
|
+
if tool and tool.risk_level in ("high", "critical"):
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
# 인자 기반 추가 검사
|
|
111
|
+
if name == "jupyter_cell":
|
|
112
|
+
# EXECUTE 작업에 승인 필요
|
|
113
|
+
if args.get("operation", "").upper() == "EXECUTE":
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
async def execute(self, name: str, args: dict[str, Any]) -> ToolResult:
|
|
119
|
+
"""
|
|
120
|
+
도구를 실행합니다.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
name: 도구 이름
|
|
124
|
+
args: 도구 인자
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
도구 실행 결과 ToolResult
|
|
128
|
+
"""
|
|
129
|
+
tool = self._tools.get(name)
|
|
130
|
+
if not tool:
|
|
131
|
+
return ToolResult(success=False, error=f"알 수 없는 도구: {name}")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
return await tool.execute(**args)
|
|
135
|
+
except ToolError:
|
|
136
|
+
raise
|
|
137
|
+
except Exception as e:
|
|
138
|
+
raise ToolError(f"도구 실행 오류: {e}", tool_name=name, cause=e) from e
|
|
139
|
+
|
|
140
|
+
def set_notebook_context(self, path: str, model: dict) -> None:
|
|
141
|
+
"""Jupyter 도구에 노트북 컨텍스트를 설정합니다."""
|
|
142
|
+
jupyter_cell = self._tools.get("jupyter_cell")
|
|
143
|
+
if isinstance(jupyter_cell, JupyterCellTool):
|
|
144
|
+
jupyter_cell.set_notebook(path, model)
|
|
145
|
+
|
|
146
|
+
jupyter_md = self._tools.get("jupyter_markdown")
|
|
147
|
+
if isinstance(jupyter_md, JupyterMarkdownTool):
|
|
148
|
+
jupyter_md.set_notebook(model)
|
|
149
|
+
|
|
150
|
+
def set_tavily_api_key(self, api_key: str) -> None:
|
|
151
|
+
"""웹 검색을 위한 Tavily API 키를 설정합니다."""
|
|
152
|
+
web_search = self._tools.get("web_search")
|
|
153
|
+
if isinstance(web_search, WebSearchTool):
|
|
154
|
+
web_search.set_api_key(api_key)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def tool_names(self) -> list[str]:
|
|
158
|
+
"""등록된 도구 이름 목록을 가져옵니다."""
|
|
159
|
+
return list(self._tools.keys())
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""코드 및 웹 검색 도구."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .base import BaseTool, ToolResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SearchCodeTool(BaseTool):
|
|
15
|
+
"""파일에서 코드 검색 도구."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, base_path: str = "."):
|
|
18
|
+
self._base_path = Path(base_path)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self) -> str:
|
|
22
|
+
return "search_code"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def description(self) -> str:
|
|
26
|
+
return """Search for patterns in code files.
|
|
27
|
+
Uses regex patterns to find matches across files.
|
|
28
|
+
Returns matching lines with file paths and line numbers."""
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def parameters(self) -> dict[str, Any]:
|
|
32
|
+
return {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"pattern": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Regex pattern to search for",
|
|
38
|
+
},
|
|
39
|
+
"path": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Directory to search in (relative to workspace)",
|
|
42
|
+
"default": ".",
|
|
43
|
+
},
|
|
44
|
+
"file_pattern": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Glob pattern for files to search (e.g., '*.py')",
|
|
47
|
+
"default": "*",
|
|
48
|
+
},
|
|
49
|
+
"max_results": {
|
|
50
|
+
"type": "integer",
|
|
51
|
+
"description": "Maximum number of results to return",
|
|
52
|
+
"default": 50,
|
|
53
|
+
},
|
|
54
|
+
"case_sensitive": {
|
|
55
|
+
"type": "boolean",
|
|
56
|
+
"description": "Case-sensitive search",
|
|
57
|
+
"default": True,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
"required": ["pattern"],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def risk_level(self) -> str:
|
|
65
|
+
return "low"
|
|
66
|
+
|
|
67
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
68
|
+
"""코드 패턴을 검색합니다."""
|
|
69
|
+
pattern = kwargs.get("pattern", "")
|
|
70
|
+
path = kwargs.get("path", ".")
|
|
71
|
+
file_pattern = kwargs.get("file_pattern", "*")
|
|
72
|
+
max_results = kwargs.get("max_results", 50)
|
|
73
|
+
case_sensitive = kwargs.get("case_sensitive", True)
|
|
74
|
+
|
|
75
|
+
if not pattern:
|
|
76
|
+
return ToolResult(success=False, error="패턴이 필요합니다")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
80
|
+
regex = re.compile(pattern, flags)
|
|
81
|
+
except re.error as e:
|
|
82
|
+
return ToolResult(success=False, error=f"잘못된 정규식 패턴: {e}")
|
|
83
|
+
|
|
84
|
+
search_path = self._base_path / path
|
|
85
|
+
if not search_path.exists():
|
|
86
|
+
return ToolResult(success=False, error=f"경로를 찾을 수 없음: {path}")
|
|
87
|
+
|
|
88
|
+
results = []
|
|
89
|
+
try:
|
|
90
|
+
for file_path in search_path.rglob(file_pattern):
|
|
91
|
+
if not file_path.is_file():
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# 바이너리 파일과 숨김 디렉토리 건너뛰기
|
|
95
|
+
if any(part.startswith(".") for part in file_path.parts):
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
100
|
+
for line_num, line in enumerate(content.splitlines(), 1):
|
|
101
|
+
if regex.search(line):
|
|
102
|
+
rel_path = file_path.relative_to(self._base_path)
|
|
103
|
+
results.append(
|
|
104
|
+
{
|
|
105
|
+
"file": str(rel_path),
|
|
106
|
+
"line": line_num,
|
|
107
|
+
"content": line.strip(),
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if len(results) >= max_results:
|
|
112
|
+
break
|
|
113
|
+
except Exception:
|
|
114
|
+
# 읽을 수 없는 파일 건너뛰기
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
if len(results) >= max_results:
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
return ToolResult(
|
|
121
|
+
success=True,
|
|
122
|
+
data={"matches": results, "pattern": pattern},
|
|
123
|
+
metadata={
|
|
124
|
+
"count": len(results),
|
|
125
|
+
"truncated": len(results) >= max_results,
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
return ToolResult(success=False, error=f"검색 오류: {str(e)}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class WebSearchTool(BaseTool):
|
|
134
|
+
"""Tavily API를 사용한 웹 검색 도구."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
137
|
+
self._api_key = api_key
|
|
138
|
+
self._client = httpx.AsyncClient(timeout=30.0)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def name(self) -> str:
|
|
142
|
+
return "web_search"
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def description(self) -> str:
|
|
146
|
+
return """Search the web for information.
|
|
147
|
+
Returns relevant search results with titles, URLs, and snippets.
|
|
148
|
+
Useful for finding current information, documentation, or examples."""
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def parameters(self) -> dict[str, Any]:
|
|
152
|
+
return {
|
|
153
|
+
"type": "object",
|
|
154
|
+
"properties": {
|
|
155
|
+
"query": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"description": "The search query",
|
|
158
|
+
},
|
|
159
|
+
"max_results": {
|
|
160
|
+
"type": "integer",
|
|
161
|
+
"description": "Maximum number of results to return",
|
|
162
|
+
"default": 5,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
"required": ["query"],
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def risk_level(self) -> str:
|
|
170
|
+
return "low"
|
|
171
|
+
|
|
172
|
+
def set_api_key(self, api_key: str) -> None:
|
|
173
|
+
"""Tavily API 키를 설정합니다."""
|
|
174
|
+
self._api_key = api_key
|
|
175
|
+
|
|
176
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
177
|
+
"""웹 검색을 실행합니다."""
|
|
178
|
+
query = kwargs.get("query", "")
|
|
179
|
+
max_results = kwargs.get("max_results", 5)
|
|
180
|
+
|
|
181
|
+
if not query:
|
|
182
|
+
return ToolResult(success=False, error="쿼리가 필요합니다")
|
|
183
|
+
|
|
184
|
+
if not self._api_key:
|
|
185
|
+
return ToolResult(success=False, error="Tavily API 키가 설정되지 않음")
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
response = await self._client.post(
|
|
189
|
+
"https://api.tavily.com/search",
|
|
190
|
+
json={
|
|
191
|
+
"api_key": self._api_key,
|
|
192
|
+
"query": query,
|
|
193
|
+
"max_results": max_results,
|
|
194
|
+
"include_answer": True,
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
response.raise_for_status()
|
|
198
|
+
|
|
199
|
+
data = response.json()
|
|
200
|
+
results = []
|
|
201
|
+
|
|
202
|
+
for result in data.get("results", []):
|
|
203
|
+
results.append(
|
|
204
|
+
{
|
|
205
|
+
"title": result.get("title", ""),
|
|
206
|
+
"url": result.get("url", ""),
|
|
207
|
+
"snippet": result.get("content", ""),
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return ToolResult(
|
|
212
|
+
success=True,
|
|
213
|
+
data={
|
|
214
|
+
"results": results,
|
|
215
|
+
"answer": data.get("answer"),
|
|
216
|
+
"query": query,
|
|
217
|
+
},
|
|
218
|
+
metadata={"count": len(results)},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
except httpx.HTTPStatusError as e:
|
|
222
|
+
return ToolResult(
|
|
223
|
+
success=False, error=f"검색 API 오류: {e.response.status_code}"
|
|
224
|
+
)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
return ToolResult(success=False, error=f"검색 오류: {str(e)}")
|
|
227
|
+
|
|
228
|
+
async def close(self):
|
|
229
|
+
"""HTTP 클라이언트를 닫습니다."""
|
|
230
|
+
await self._client.aclose()
|