hdsp-jupyter-extension 2.0.7__py3-none-any.whl → 2.0.8__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 (78) hide show
  1. agent_server/core/embedding_service.py +67 -46
  2. agent_server/core/rag_manager.py +31 -17
  3. agent_server/core/retriever.py +13 -8
  4. agent_server/core/vllm_embedding_service.py +243 -0
  5. agent_server/langchain/agent.py +8 -0
  6. agent_server/langchain/custom_middleware.py +58 -31
  7. agent_server/langchain/hitl_config.py +6 -1
  8. agent_server/langchain/logging_utils.py +53 -14
  9. agent_server/langchain/prompts.py +47 -16
  10. agent_server/langchain/tools/__init__.py +13 -0
  11. agent_server/langchain/tools/file_tools.py +285 -7
  12. agent_server/langchain/tools/file_utils.py +334 -0
  13. agent_server/langchain/tools/lsp_tools.py +264 -0
  14. agent_server/main.py +7 -0
  15. agent_server/routers/langchain_agent.py +115 -19
  16. agent_server/routers/rag.py +8 -3
  17. hdsp_agent_core/models/rag.py +15 -1
  18. hdsp_agent_core/services/rag_service.py +6 -1
  19. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  20. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +3 -2
  21. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js +160 -3
  22. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js.map +1 -0
  23. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js +1759 -221
  24. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +1 -0
  25. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js +14 -12
  26. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
  27. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
  28. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
  29. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +209 -2
  30. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
  31. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +212 -3
  32. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
  33. {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/METADATA +1 -1
  34. {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/RECORD +66 -63
  35. jupyter_ext/__init__.py +18 -0
  36. jupyter_ext/_version.py +1 -1
  37. jupyter_ext/handlers.py +176 -1
  38. jupyter_ext/labextension/build_log.json +1 -1
  39. jupyter_ext/labextension/package.json +3 -2
  40. jupyter_ext/labextension/static/{frontend_styles_index_js.4770ec0fb2d173b6deb4.js → frontend_styles_index_js.8740a527757068814573.js} +160 -3
  41. jupyter_ext/labextension/static/frontend_styles_index_js.8740a527757068814573.js.map +1 -0
  42. jupyter_ext/labextension/static/{lib_index_js.29cf4312af19e86f82af.js → lib_index_js.e4ff4b5779b5e049f84c.js} +1759 -221
  43. jupyter_ext/labextension/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +1 -0
  44. jupyter_ext/labextension/static/{remoteEntry.61343eb4cf0577e74b50.js → remoteEntry.020cdb0b864cfaa4e41e.js} +14 -12
  45. jupyter_ext/labextension/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
  46. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
  47. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
  48. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +209 -2
  49. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
  50. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +212 -3
  51. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
  52. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
  53. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
  54. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js.map +0 -1
  55. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
  56. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
  57. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
  58. jupyter_ext/labextension/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
  59. jupyter_ext/labextension/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
  60. jupyter_ext/labextension/static/remoteEntry.61343eb4cf0577e74b50.js.map +0 -1
  61. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
  62. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
  63. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
  64. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  65. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  66. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js +0 -0
  67. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js.map +0 -0
  68. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js +0 -0
  69. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js.map +0 -0
  70. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  71. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js +0 -0
  72. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js.map +0 -0
  73. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
  74. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js.map +0 -0
  75. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
  76. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
  77. {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/WHEEL +0 -0
  78. {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/licenses/LICENSE +0 -0
@@ -78,6 +78,20 @@ def parse_json_tool_call(text) -> Optional[Dict[str, Any]]:
78
78
  return None
79
79
 
80
80
 
81
+ def normalize_tool_name(tool_name: str) -> str:
82
+ """Normalize tool name to match registered tool names.
83
+
84
+ Rules:
85
+ - write_todos_tool → write_todos (TodoListMiddleware exception)
86
+ - other tools without _tool suffix → add _tool suffix
87
+ """
88
+ if tool_name == "write_todos_tool":
89
+ return "write_todos"
90
+ if not tool_name.endswith("_tool") and tool_name != "write_todos":
91
+ return f"{tool_name}_tool"
92
+ return tool_name
93
+
94
+
81
95
  def create_tool_call_message(tool_name: str, arguments: Dict[str, Any]) -> AIMessage:
82
96
  """Create AIMessage with tool_calls from parsed JSON.
83
97
 
@@ -88,9 +102,7 @@ def create_tool_call_message(tool_name: str, arguments: Dict[str, Any]) -> AIMes
88
102
  Returns:
89
103
  AIMessage with properly formatted tool_calls
90
104
  """
91
- # Normalize tool name
92
- if not tool_name.endswith("_tool"):
93
- tool_name = f"{tool_name}_tool"
105
+ tool_name = normalize_tool_name(tool_name)
94
106
 
95
107
  return AIMessage(
96
108
  content="",
@@ -363,70 +375,70 @@ def create_limit_tool_calls_middleware(wrap_model_call):
363
375
 
364
376
  def _get_string_params_from_tools(tools) -> Dict[str, set]:
365
377
  """Extract string parameter names from tool schemas.
366
-
378
+
367
379
  Analyzes each tool's Pydantic args_schema to determine which parameters
368
380
  should be strings (not arrays).
369
-
381
+
370
382
  Args:
371
383
  tools: List of LangChain tools
372
-
384
+
373
385
  Returns:
374
386
  Dict mapping tool names to sets of string parameter names
375
387
  """
376
388
  from typing import get_args, get_origin
377
-
389
+
378
390
  tool_string_params: Dict[str, set] = {}
379
-
391
+
380
392
  for tool in tools:
381
- tool_name = getattr(tool, 'name', None)
393
+ tool_name = getattr(tool, "name", None)
382
394
  if not tool_name:
383
395
  continue
384
-
385
- args_schema = getattr(tool, 'args_schema', None)
396
+
397
+ args_schema = getattr(tool, "args_schema", None)
386
398
  if not args_schema:
387
399
  continue
388
-
400
+
389
401
  string_params = set()
390
-
402
+
391
403
  # Get field annotations from Pydantic model
392
404
  try:
393
- annotations = getattr(args_schema, '__annotations__', {})
405
+ annotations = getattr(args_schema, "__annotations__", {})
394
406
  for field_name, field_type in annotations.items():
395
407
  origin = get_origin(field_type)
396
-
408
+
397
409
  # Check if it's a simple str type
398
410
  if field_type is str:
399
411
  string_params.add(field_name)
400
412
  # Check if it's Optional[str] (Union[str, None])
401
- elif origin is type(None) or str(origin) == 'typing.Union':
413
+ elif origin is type(None) or str(origin) == "typing.Union":
402
414
  args = get_args(field_type)
403
415
  if str in args:
404
416
  string_params.add(field_name)
405
417
  except Exception as e:
406
418
  logger.debug("Failed to analyze schema for tool %s: %s", tool_name, e)
407
-
419
+
408
420
  if string_params:
409
421
  tool_string_params[tool_name] = string_params
410
422
  logger.debug("Tool %s string params: %s", tool_name, string_params)
411
-
423
+
412
424
  return tool_string_params
413
425
 
414
426
 
415
427
  def create_normalize_tool_args_middleware(wrap_model_call, tools=None):
416
428
  """Create middleware to normalize tool call arguments.
417
-
429
+
418
430
  Gemini sometimes returns tool call arguments with list values instead of strings.
419
431
  This middleware converts list arguments to strings ONLY for parameters that
420
432
  are defined as str in the tool's Pydantic schema.
421
-
433
+
422
434
  Args:
423
435
  wrap_model_call: LangChain's wrap_model_call decorator
424
436
  tools: Optional list of tools to analyze for type information
425
-
437
+
426
438
  Returns:
427
439
  Middleware function
428
440
  """
429
-
441
+
430
442
  # Build tool -> string params mapping from tool schemas
431
443
  tool_string_params: Dict[str, set] = {}
432
444
  if tools:
@@ -436,25 +448,37 @@ def create_normalize_tool_args_middleware(wrap_model_call, tools=None):
436
448
  len(tool_string_params),
437
449
  {k: list(v) for k, v in tool_string_params.items()},
438
450
  )
439
-
451
+
440
452
  @wrap_model_call
441
453
  @_with_middleware_logging("normalize_tool_args")
442
454
  def normalize_tool_args(request, handler):
443
455
  response = handler(request)
444
-
456
+
445
457
  if hasattr(response, "result"):
446
458
  result = response.result
447
459
  messages = result if isinstance(result, list) else [result]
448
-
460
+
449
461
  for msg in messages:
450
462
  if isinstance(msg, AIMessage) and hasattr(msg, "tool_calls"):
451
463
  tool_calls = msg.tool_calls
452
464
  if tool_calls:
453
465
  for tool_call in tool_calls:
454
466
  tool_name = tool_call.get("name", "")
467
+ # Normalize tool name (e.g., write_todos_tool → write_todos)
468
+ normalized_name = normalize_tool_name(tool_name)
469
+ if normalized_name != tool_name:
470
+ logger.info(
471
+ "Normalized tool name: %s → %s",
472
+ tool_name,
473
+ normalized_name,
474
+ )
475
+ tool_call["name"] = normalized_name
476
+ tool_name = normalized_name
455
477
  string_params = tool_string_params.get(tool_name, set())
456
-
457
- if "args" in tool_call and isinstance(tool_call["args"], dict):
478
+
479
+ if "args" in tool_call and isinstance(
480
+ tool_call["args"], dict
481
+ ):
458
482
  args = tool_call["args"]
459
483
  # Normalize list arguments to strings for str-typed params
460
484
  for key, value in args.items():
@@ -464,9 +488,12 @@ def create_normalize_tool_args_middleware(wrap_model_call, tools=None):
464
488
  for part in value:
465
489
  if isinstance(part, str):
466
490
  text_parts.append(part)
467
- elif isinstance(part, dict) and part.get("type") == "text":
491
+ elif (
492
+ isinstance(part, dict)
493
+ and part.get("type") == "text"
494
+ ):
468
495
  text_parts.append(part.get("text", ""))
469
-
496
+
470
497
  if text_parts:
471
498
  normalized_value = "\n".join(text_parts)
472
499
  logger.info(
@@ -476,9 +503,9 @@ def create_normalize_tool_args_middleware(wrap_model_call, tools=None):
476
503
  tool_name,
477
504
  )
478
505
  args[key] = normalized_value
479
-
506
+
480
507
  return response
481
-
508
+
482
509
  return normalize_tool_args
483
510
 
484
511
 
@@ -59,7 +59,12 @@ def get_hitl_interrupt_config() -> Dict[str, Any]:
59
59
  # File write requires approval
60
60
  "write_file_tool": {
61
61
  "allowed_decisions": ["approve", "edit", "reject"],
62
- "description": "⚠️ File write requires approval",
62
+ "description": "File write requires approval",
63
+ },
64
+ # File edit requires approval (string replacement with diff preview)
65
+ "edit_file_tool": {
66
+ "allowed_decisions": ["approve", "edit", "reject"],
67
+ "description": "File edit requires approval",
63
68
  },
64
69
  # Final answer doesn't need approval
65
70
  "final_answer_tool": False,
@@ -115,8 +115,28 @@ def _with_middleware_logging(name: str):
115
115
 
116
116
 
117
117
  class LLMTraceLogger(BaseCallbackHandler):
118
- """Log prompts, responses, tool calls, and tool messages."""
119
-
118
+ """Log prompts, responses, tool calls, and tool messages.
119
+
120
+ Only logs newly added messages to avoid duplicate logging of conversation history.
121
+ Uses content hash of first message (usually system prompt) to identify conversation threads.
122
+ """
123
+
124
+ def __init__(self):
125
+ super().__init__()
126
+ # Track last logged message count per conversation thread
127
+ # Key: hash of first message content, Value: message count
128
+ self._last_message_counts: Dict[str, int] = {}
129
+
130
+ def _get_conversation_key(self, batch) -> str:
131
+ """Get a stable key for the conversation based on first message content."""
132
+ if not batch:
133
+ return "empty"
134
+ first_msg = batch[0]
135
+ content = getattr(first_msg, "content", "")
136
+ # Use hash of first 200 chars of first message (usually system prompt)
137
+ content_preview = str(content)[:200] if content else ""
138
+ return str(hash(content_preview))
139
+
120
140
  def _normalize_batches(self, messages):
121
141
  if not messages:
122
142
  return []
@@ -125,19 +145,38 @@ class LLMTraceLogger(BaseCallbackHandler):
125
145
  return [messages]
126
146
 
127
147
  def _log_prompt_batches(self, title: str, messages) -> None:
148
+ """Log only new messages that haven't been logged before."""
128
149
  for batch_idx, batch in enumerate(self._normalize_batches(messages)):
129
- header = f"{title} (batch={batch_idx}, messages={len(batch)})"
130
- logger.info("%s", _format_messages_block(header, batch))
131
-
132
- tool_messages = [
133
- msg
134
- for msg in batch
135
- if getattr(msg, "type", "") == "tool"
136
- or msg.__class__.__name__ == "ToolMessage"
137
- ]
138
- if tool_messages:
139
- tool_header = f"{title} TOOL MESSAGES (batch={batch_idx})"
140
- logger.info("%s", _format_messages_block(tool_header, tool_messages))
150
+ # Get stable conversation key based on first message
151
+ conv_key = self._get_conversation_key(batch)
152
+ batch_key = f"{conv_key}_{batch_idx}"
153
+ last_count = self._last_message_counts.get(batch_key, 0)
154
+
155
+ # Only log new messages
156
+ new_messages = batch[last_count:]
157
+ if not new_messages:
158
+ logger.debug(
159
+ "Skipping duplicate log for batch %d (already logged %d messages)",
160
+ batch_idx,
161
+ last_count,
162
+ )
163
+ continue
164
+
165
+ # Update count
166
+ self._last_message_counts[batch_key] = len(batch)
167
+
168
+ # Log with offset info
169
+ header = f"{title} (batch={batch_idx}, new={len(new_messages)}, total={len(batch)})"
170
+
171
+ # Format new messages with correct indices
172
+ lines = [LOG_SEPARATOR, header, LOG_SEPARATOR]
173
+ for idx, message in enumerate(new_messages, start=last_count):
174
+ lines.append(f"[{idx}] {message.__class__.__name__}")
175
+ lines.append(_pretty_json(_serialize_message(message)))
176
+ if idx < len(batch) - 1:
177
+ lines.append(LOG_SUBSECTION)
178
+ lines.append(LOG_SEPARATOR)
179
+ logger.info("%s", "\n".join(lines))
141
180
 
142
181
  def on_chat_model_start(self, serialized, messages, **kwargs) -> None:
143
182
  if not messages:
@@ -8,7 +8,14 @@ and middleware-specific prompts.
8
8
  DEFAULT_SYSTEM_PROMPT = """You are an expert Python data scientist and Jupyter notebook assistant.
9
9
  Your role is to help users with data analysis, visualization, and Python coding tasks in Jupyter notebooks. You can use only Korean
10
10
 
11
- ## ⚠️ CRITICAL RULE: NEVER produce an empty response
11
+ # Core Behavior
12
+ Be concise and direct. Answer in fewer than 4 lines unless the user asks for detail.
13
+ After working on a file, just stop - don't explain what you did unless asked.
14
+ Avoid unnecessary introductions or conclusions.
15
+
16
+ ## Task Management
17
+ Use write_todos for complex multi-step tasks (3+ steps). Mark tasks in_progress before starting, completed immediately after finishing.
18
+ For simple 1-2 step tasks, just do them directly without todos.
12
19
 
13
20
  You MUST ALWAYS call a tool in every response. After any tool result, you MUST:
14
21
  1. Check your todo list - are there pending or in_progress items?
@@ -24,8 +31,6 @@ You MUST ALWAYS call a tool in every response. After any tool result, you MUST:
24
31
  }
25
32
  4. If ALL todos are completed → call final_answer_tool with a summary
26
33
 
27
- NEVER end your turn without calling a tool. NEVER produce an empty response.
28
-
29
34
  ## 🔴 MANDATORY: Resource Check Before Data Hanlding
30
35
  **ALWAYS call check_resource_tool FIRST** when the task involves:
31
36
  - Loading files: .csv, .parquet, .json, .xlsx, .pickle, .h5, .feather
@@ -45,11 +50,27 @@ NEVER end your turn without calling a tool. NEVER produce an empty response.
45
50
  - Ending without calling final_answer_tool
46
51
  - Leaving todos in "in_progress" or "pending" state without continuing
47
52
 
48
- ## 🚫 execute_command_tool Rules
49
- **NEVER run long-running commands** with execute_command_tool (e.g., servers, daemons, watch processes).
50
- - ✅ Allowed: Quick commands like `ls`, `cat`, `grep`, `git status`
51
- - Forbidden: `jupyter lab`, `npm start`, `python app.py`, `watch`, background processes
52
- - For long tasks: Use jupyter_cell_tool instead or inform the user to run manually
53
+ ## 📖 File Reading Best Practices
54
+ **CRITICAL**: When exploring codebases or reading files, use pagination to prevent context overflow.
55
+
56
+ **Pattern for codebase exploration:**
57
+ 1. First scan: `read_file_tool(path, limit=100)` - See file structure and key sections
58
+ 2. Targeted read: `read_file_tool(path, offset=100, limit=200)` - Read specific sections if needed
59
+ 3. Full read: Only read without limit when necessary for immediate editing
60
+
61
+ **When to paginate (use offset/limit):**
62
+ - Reading any file >500 lines
63
+ - Exploring unfamiliar codebases (always start with limit=100)
64
+ - Reading multiple files in sequence
65
+ - Any research or investigation task
66
+
67
+ **When full read is OK:**
68
+ - Small files (<500 lines)
69
+ - Files you need to edit immediately after reading
70
+ - After confirming file size with first scan
71
+
72
+ ## 🔧 Code Development
73
+ For code generation/refactoring, use LSP tools (diagnostics_tool, references_tool) to check errors and find symbol usages. Use multiedit_file_tool for multiple changes in one file.
53
74
  """
54
75
 
55
76
  JSON_TOOL_SCHEMA = """You MUST respond with ONLY valid JSON matching this schema:
@@ -63,7 +84,7 @@ Available tools:
63
84
  - markdown_tool: Add markdown cell. Arguments: {"content": "<markdown>"}
64
85
  - final_answer_tool: Complete task. Arguments: {"answer": "<summary>"}
65
86
  - write_todos: Update task list. Arguments: {"todos": [{"content": "...", "status": "pending|in_progress|completed"}]}
66
- - read_file_tool: Read file. Arguments: {"path": "<file_path>"}
87
+ - read_file_tool: Read file with pagination. Arguments: {"path": "<file_path>", "offset": 0, "limit": 500}
67
88
  - write_file_tool: Write file. Arguments: {"path": "<path>", "content": "<content>", "overwrite": false}
68
89
  - list_files_tool: List directory. Arguments: {"path": ".", "recursive": false}
69
90
  - search_workspace_tool: Search files. Arguments: {"pattern": "<regex>", "file_types": ["py"], "path": "."}
@@ -75,27 +96,32 @@ Output ONLY the JSON object, no markdown, no explanation."""
75
96
 
76
97
  TODO_LIST_SYSTEM_PROMPT = """
77
98
  ## CRITICAL WORKFLOW RULES - MUST FOLLOW:
78
- 1. NEVER stop after calling write_todos - ALWAYS make another tool call immediately
79
- 2. write_todos is ONLY for tracking progress - it does NOT complete any work
80
- 3. After EVERY write_todos call, you MUST call another tool (jupyter_cell_tool, markdown_tool, or final_answer_tool)
99
+ - NEVER stop after calling write_todos - ALWAYS make another tool call immediately
100
+ - For simple 1-2 step tasks, just do them directly without todos.
101
+
102
+ ## 🔴 NEW USER MESSAGE = FRESH START:
103
+ - When user sends a NEW message, treat it as a COMPLETELY NEW TASK
104
+ - IGNORE any previous todo completion history - start fresh
105
+ - Do NOT assume any work was already done based on past conversations
106
+ - Create a NEW todo list for the new request, even if similar items existed before
107
+ - "다음 단계 제시" from a previous task is NOT completed for the new task
81
108
 
82
109
  ## Todo List Management:
83
110
  - Before complex tasks, use write_todos to create a task list
84
111
  - Update todos as you complete each step (mark 'in_progress' → 'completed')
85
- - Each todo item should be specific and descriptive (30-60 characters)
112
+ - Each todo item should be specific and descriptive
86
113
  - All todo items must be written in Korean
87
114
  - ALWAYS include "다음 단계 제시" as the LAST item
88
115
 
89
116
  ## Task Completion Flow:
90
117
  1. When current task is done → mark it 'completed' with write_todos
91
- 2. IMMEDIATELY call the next tool (jupyter_cell_tool for code, markdown_tool for text)
92
- 3. For "다음 단계 제시" → mark completed, then call final_answer_tool with suggestions
93
- 4. NEVER end your turn after write_todos - you MUST continue with actual work
118
+ 2. For "다음 단계 제시" mark completed, then call final_answer_tool with suggestions
94
119
 
95
120
  ## FORBIDDEN PATTERNS:
96
121
  ❌ Calling write_todos and then stopping
97
122
  ❌ Updating todo status without doing the actual work
98
123
  ❌ Ending turn without calling final_answer_tool when all tasks are done
124
+ ❌ Marking a todo as 'completed' without actually executing it in THIS conversation
99
125
  """
100
126
 
101
127
  TODO_LIST_TOOL_DESCRIPTION = """Update the task list for tracking progress.
@@ -116,4 +142,9 @@ NON_HITL_TOOLS = {
116
142
  "search_notebook_cells_tool",
117
143
  "search_notebook_cells",
118
144
  "write_todos",
145
+ # LSP tools (read-only)
146
+ "diagnostics_tool",
147
+ "diagnostics",
148
+ "references_tool",
149
+ "references",
119
150
  }
@@ -7,15 +7,20 @@ Tools available:
7
7
  - final_answer: Complete the task
8
8
  - read_file: Read file content
9
9
  - write_file: Write file content
10
+ - edit_file: Edit file with string replacement
10
11
  - list_files: List directory contents
11
12
  - search_workspace: Search files in workspace
12
13
  - search_notebook_cells: Search cells in notebooks
13
14
  - execute_command_tool: Run shell commands (client-executed)
14
15
  - check_resource_tool: Check resources before data processing (client-executed)
16
+ - diagnostics_tool: Get LSP diagnostics (errors, warnings)
17
+ - references_tool: Find symbol references via LSP
15
18
  """
16
19
 
17
20
  from agent_server.langchain.tools.file_tools import (
21
+ edit_file_tool,
18
22
  list_files_tool,
23
+ multiedit_file_tool,
19
24
  read_file_tool,
20
25
  write_file_tool,
21
26
  )
@@ -24,6 +29,10 @@ from agent_server.langchain.tools.jupyter_tools import (
24
29
  jupyter_cell_tool,
25
30
  markdown_tool,
26
31
  )
32
+ from agent_server.langchain.tools.lsp_tools import (
33
+ diagnostics_tool,
34
+ references_tool,
35
+ )
27
36
  from agent_server.langchain.tools.resource_tools import check_resource_tool
28
37
  from agent_server.langchain.tools.search_tools import (
29
38
  search_notebook_cells_tool,
@@ -37,9 +46,13 @@ __all__ = [
37
46
  "final_answer_tool",
38
47
  "read_file_tool",
39
48
  "write_file_tool",
49
+ "edit_file_tool",
50
+ "multiedit_file_tool",
40
51
  "list_files_tool",
41
52
  "search_workspace_tool",
42
53
  "search_notebook_cells_tool",
43
54
  "execute_command_tool",
44
55
  "check_resource_tool",
56
+ "diagnostics_tool",
57
+ "references_tool",
45
58
  ]