# Tools Module

The following is a live and updated 


## Directory Structure:

Mode:          signatures — code skeleton (functions/classes/methods, no bodies)
Files:         39
Directories:   5
File types:    {'.py': 39}
Excluded:       (Make it clear but concise)
Chars (raw):   303,533
Output chars:  23,638
Updated:       
============================================================


Code Context  [mode: signatures  —  signatures — code skeleton (functions/classes/methods, no bodies)]
Scanned: ai/tools
Files: 39  |  Directories: 5


Here is the code for the module I want you to review.
A directory tree is shown first, followed by the code.

tools/
├── agent_tool.py
├── arg_models/
│   ├── browser_args.py
│   ├── db_args.py
│   ├── fs_args.py
│   ├── math_args.py
│   ├── memory_args.py
│   ├── shell_args.py
│   ├── text_args.py
│   ├── web_args.py
├── browser_sessions.py
├── executor.py
├── external_mcp.py
├── guardrails.py
├── handle_tool_calls.py
├── implementations/
│   ├── _summarize_helper.py
│   ├── browser.py
│   ├── code.py
│   ├── database.py
│   ├── filesystem.py
│   ├── math.py
│   ├── memory.py
│   ├── news.py
│   ├── personal_tables.py
│   ├── questionnaire.py
│   ├── seo.py
│   ├── shell.py
│   ├── text.py
│   ├── travel.py
│   ├── picklists_tools.py
│   ├── datasets_tools.py
│   ├── web.py
├── lifecycle.py
├── logger.py
├── models.py
├── output_models/
│   ├── seo.py
├── registry.py
├── streaming.py
├── tests/
│   ├── mimic_model_tests.py
├── tools_db.py



---
Filepath: ai/tools/agent_tool.py  [python]

  async def execute_agent_tool(tool_def: ToolDefinition, args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def register_agent_as_tool(prompt_id: str, tool_name: str, description: str, input_schema: dict[str, Any] | None = None, max_calls_per_conversation: int = 5, cost_cap_per_call: float = 1.0, timeout_seconds: float = 300.0) -> ToolDefinition


---
Filepath: ai/tools/logger.py  [python]

  class ToolExecutionLogger:
      async def log_started(self, ctx: ToolContext, tool_def: ToolDefinition, arguments: dict[str, Any]) -> str
      async def log_completed(self, row_id: str, result: ToolResult, execution_events: list[dict[str, Any]] | None = None) -> None
      async def log_error(self, row_id: str, result: ToolResult, execution_events: list[dict[str, Any]] | None = None) -> None
      async def link_message(self, row_id: str, message_id: str) -> None
      async def backfill_message_id(self, call_id: str, conversation_id: str, message_id: str) -> None
      async def _update_row(self, row_id: str, data: dict[str, Any]) -> None
      def _truncate_arguments(arguments: dict[str, Any]) -> dict[str, Any]
      def _serialize_output(output: Any) -> tuple[str | None, str]
      def _aggregate_usage(result: ToolResult) -> tuple[int, int, float]


---
Filepath: ai/tools/executor.py  [python]

  class ToolExecutor:
      def __init__(self, registry: ToolRegistryV2, guardrails: GuardrailEngine | None = None, execution_logger: ToolExecutionLogger | None = None, lifecycle: ToolLifecycleManager | None = None)
      def build_context(*, call_id: str, tool_name: str, iteration: int = 0, recursion_depth: int = 0, cost_budget_remaining: float | None = None, calls_remaining: int | None = None) -> ToolContext
      async def execute(self, tool_name: str, arguments: dict[str, Any], ctx: ToolContext) -> tuple[dict[str, Any], ToolResult]
      async def execute_batch(self, tool_calls: list[dict[str, Any]], ctx_base: ToolContext) -> tuple[list[dict[str, Any]], list[ToolResult]]
      async def _dispatch(self, tool_def: ToolDefinition, args: dict[str, Any], ctx: ToolContext, stream: ToolStreamManager) -> ToolResult
      async def _execute_local(self, tool_def: ToolDefinition, args: dict[str, Any], ctx: ToolContext, stream: ToolStreamManager) -> ToolResult
      async def _execute_external_mcp(self, tool_def: ToolDefinition, args: dict[str, Any], ctx: ToolContext) -> ToolResult
      async def _execute_agent(self, tool_def: ToolDefinition, args: dict[str, Any], ctx: ToolContext) -> ToolResult
      def _unknown_tool_result(tool_name: str, call_id: str, started_at: float) -> ToolResult


---
Filepath: ai/tools/guardrails.py  [python]

  class _CallRecord:
  class GuardrailEngine:
      def __init__(self) -> None
      async def check(self, tool_name: str, arguments: dict, ctx: ToolContext, tool_def: ToolDefinition) -> GuardrailResult
      def record_call(self, tool_name: str, arguments: dict, ctx: ToolContext) -> None
      def clear_conversation(self, conversation_id: str) -> None
      def _check_duplicate(self, tool_name: str, arguments: dict, ctx: ToolContext) -> GuardrailResult
      def _check_rate_limit(self, tool_name: str, ctx: ToolContext, tool_def: ToolDefinition) -> GuardrailResult
      def _check_conversation_limit(self, tool_name: str, ctx: ToolContext, tool_def: ToolDefinition) -> GuardrailResult
      def _check_cost_budget(self, ctx: ToolContext, tool_def: ToolDefinition) -> GuardrailResult
      def _check_loop_detection(self, tool_name: str, arguments: dict, ctx: ToolContext) -> GuardrailResult
      def _check_recursion_depth(self, ctx: ToolContext, tool_def: ToolDefinition) -> GuardrailResult
      def _hash_args(arguments: dict) -> str
      def _similarity(hash_a: str, hash_b: str) -> float


---
Filepath: ai/tools/tools_db.py  [python]

  class ToolsManager(ToolsBase):
      def __new__(cls, *args, **kwargs)
      def __init__(self)


---
Filepath: ai/tools/registry.py  [python]

  class ToolRegistryV2:
      def __init__(self) -> None
      def get_instance(cls) -> ToolRegistryV2
      async def load_from_database(self) -> int
      def load_from_database_sync(self) -> int
      def _load_rows(self, rows: list[dict[str, Any]]) -> int
      def load_from_definitions(self, definitions: list[ToolDefinition]) -> int
      def register_local(self, name: str, func: Callable[..., Awaitable[Any]], description: str = '', category: str | None = None, tags: list[str] | None = None, **overrides: Any) -> ToolDefinition
      def register(self, tool_def: ToolDefinition) -> None
      def unregister(self, name: str) -> bool
      def get(self, name: str) -> ToolDefinition | None
      def get_provider_tools(self, tool_names: list[str], provider: str) -> list[dict[str, Any]]
      def list_tools(self, category: str | None = None, tags: list[str] | None = None, tool_type: ToolType | None = None, active_only: bool = True) -> list[ToolDefinition]
      def list_tool_names(self) -> list[str]
      def loaded(self) -> bool
      def count(self) -> int
      async def _fetch_tools_async() -> list[dict[str, Any]]
      def _fetch_tools_via_orm_sync() -> list[dict[str, Any]]
      def _row_to_definition(row: dict[str, Any]) -> ToolDefinition
      def _resolve_callable(function_path: str) -> Callable[..., Awaitable[Any]]
      def _pydantic_to_param_dict(model_cls: type[BaseModel]) -> dict[str, Any]
      async def register_mcp_server(self, server_url: str, server_name: str, mcp_client: Any | None = None) -> list[str]


---
Filepath: ai/tools/external_mcp.py  [python]

  class ExternalMCPClient:
      def __init__(self, timeout: float = 120.0)
      async def discover_tools(self, server_url: str, auth: dict[str, Any] | None = None) -> list[ToolDefinition]
      async def call_tool(self, tool_def: ToolDefinition, args: dict[str, Any], ctx: ToolContext) -> ToolResult
      async def _send(self, server_url: str, payload: dict[str, Any], auth: dict[str, Any] | None = None) -> dict[str, Any]
      def _build_request(self, method: str, params: dict[str, Any]) -> dict[str, Any]
      def _strip_namespace(name: str) -> str
      def _schema_to_params(input_schema: dict[str, Any]) -> dict[str, Any]


---
Filepath: ai/tools/streaming.py  [python]

  class ToolStreamEvent(BaseModel):
  class ToolStreamManager:
      def __init__(self, emitter: Emitter | None, call_id: str, tool_name: str)
      async def emit(self, event: ToolStreamEvent) -> None
      async def started(self, message: str = 'Starting...', arguments: dict[str, Any] | None = None) -> None
      async def completed(self, message: str = 'Done', result: ToolResult | None = None) -> None
      async def error(self, message: str, error_type: str = 'execution') -> None
      async def progress(self, message: str, data: dict[str, Any] | None = None) -> None
      async def step(self, step_name: str, message: str, data: dict[str, Any] | None = None) -> None
      async def result_preview(self, preview: str) -> None
      def get_events_for_persistence(self) -> list[dict[str, Any]]


---
Filepath: ai/tools/handle_tool_calls.py  [python]

  def get_executor() -> ToolExecutor
  async def initialize_tool_system() -> int
  def initialize_tool_system_sync() -> int
  async def handle_tool_calls_v2(tool_calls_raw: list[dict[str, Any]], *, iteration: int, recursion_depth: int = 0, cost_budget_remaining: float | None = None) -> tuple[list[dict[str, Any]], list['TokenUsage']]
  async def cleanup_conversation(conversation_id: str) -> None


---
Filepath: ai/tools/browser_sessions.py  [python]

  class BrowserSession:
      async def close(self) -> None
  class BrowserSessionManager:
      def __init__(self) -> None
      def _ensure_eviction_task(self) -> None
      async def _eviction_loop(self) -> None
      async def create(self) -> BrowserSession
      async def get(self, session_id: str) -> BrowserSession | None
      async def close(self, session_id: str) -> None
      async def close_all(self) -> None
      async def evict_stale(self) -> None
      def active_count(self) -> int
      def session_ids(self) -> list[str]
  def get_browser_session_manager() -> BrowserSessionManager
  def _shutdown_browser_sessions() -> None


---
Filepath: ai/tools/models.py  [python]

  class ToolType(StrEnum):
  class ToolError(BaseModel):
      def to_agent_message(self) -> str
  class ToolResult(BaseModel):
      def compute_duration(self) -> None
      def to_tool_result_content(self) -> dict[str, Any]
  class GuardrailResult(BaseModel):
      def to_tool_result_content(self, call_id: str = '', tool_name: str = '') -> dict[str, Any]
  class ToolContext(BaseModel):
      def user_id(self) -> str
      def conversation_id(self) -> str
      def request_id(self) -> str
      def emitter(self) -> Emitter | None
      def api_keys(self) -> dict[str, str]
      def project_id(self) -> str | None
      def organization_id(self) -> str | None
  class ToolDefinition(BaseModel):
      def _build_json_schema(self, *, strip_openai_unsupported: bool = False) -> dict[str, Any]
      def _process_nested(schema: dict[str, Any] | str, strip_unsupported: bool) -> dict[str, Any]
      def to_mcp_format(self) -> dict[str, Any]
      def to_openai_format(self) -> dict[str, Any]
      def to_openai_responses_format(self) -> dict[str, Any]
      def to_google_format(self) -> dict[str, Any]
      def to_anthropic_format(self) -> dict[str, Any]
      def get_provider_format(self, provider: str) -> dict[str, Any]
      def format_user_message(self, arguments: dict[str, Any]) -> str
  class CxToolCallRecord(BaseModel):
  def _build(param_dict: dict[str, Any] | str) -> dict[str, Any]


---
Filepath: ai/tools/lifecycle.py  [python]

  class ToolLifecycleManager:
      def __init__(self) -> None
      def get_instance(cls) -> ToolLifecycleManager
      def register_cleanup(self, conversation_id: str, cleanup_fn: CleanupFn) -> None
      def touch(self, conversation_id: str) -> None
      async def cleanup_conversation(self, conversation_id: str) -> int
      async def cleanup_idle(self) -> list[str]
      async def cleanup_all(self) -> int
      def sweep_running(self) -> bool
      def start_background_sweep(self) -> None
      def stop_background_sweep(self) -> None
      async def _sweep_loop(self) -> None
      def active_conversations(self) -> int
      def pending_cleanups(self) -> int


---
Filepath: ai/tools/tests/mimic_model_tests.py  [python]

  def _estimate_cost_table(content: str) -> None
  def _build_ctx(tool_name: str, stream: bool = True) -> ToolContext
  def _get_tool_map() -> dict[str, Any]
  def _yline(char: str = '═', width: int = 80) -> str
  def _cline(char: str = '═', width: int = 80) -> str
  def _rline(char: str = '═', width: int = 80) -> str
  def _gline(char: str = '═', width: int = 80) -> str
  async def run_tool(tool_name: str, args: dict[str, Any], *, stream: bool = True, print_result: bool = True, use_db: bool | None = None) -> dict[str, Any]
  async def test_math_calculate()
  async def test_text_analyze()
  async def test_text_regex_extract()
  async def test_web_search()
  async def test_web_read()
  async def test_research_web()
  async def test_db_query()
  async def test_db_schema()
  async def test_memory_store()
  async def test_memory_recall()
  async def test_memory_search()
  async def test_fs_write_then_read()
  async def test_fs_list()
  async def test_fs_search()
  async def test_shell_execute()
  async def test_shell_python()
  async def test_browser_navigate()
  async def test_browser_screenshot()
  async def test_userlist_create_simple()
  async def test_userlist_get_all()
  async def test_seo_check_meta_titles()
  async def test_seo_check_meta_descriptions()
  async def test_code_store_html()
  async def test_code_fetch_code()
  async def test_code_fetch_tree()
  async def test_news_get_headlines()
  async def test_usertable_create()
  async def test_usertable_create_advanced()
  async def test_usertable_get_all()
  async def test_usertable_get_metadata()
  async def test_usertable_get_fields()
  async def test_usertable_get_data()
  async def test_usertable_search_data()
  async def test_usertable_add_rows()
  async def test_usertable_update_row()
  async def test_usertable_delete_row()
  async def test_interaction_ask()
  async def test_travel_get_location()
  async def test_travel_get_weather()
  async def test_travel_get_restaurants()
  async def test_travel_get_activities()
  async def test_travel_get_events()
  async def test_travel_create_summary()
  async def test_db_insert()
  async def test_db_update()
  async def test_memory_update()
  async def test_memory_forget()
  async def test_fs_mkdir()
  async def test_browser_click()
  async def test_browser_type_text()
  async def test_browser_full_session()
  async def test_seo_check_meta_tags_batch()
  async def test_seo_get_keyword_data()
  async def test_code_execute_python()
  async def test_code_execute_python_with_markers()
  async def test_userlist_create()
  async def test_userlist_get_details()
  async def test_userlist_update_item()
  async def test_userlist_batch_update()
  async def _run_interactive() -> None
  def _verify_tool_map_alignment() -> None
  def _parse(result: dict) -> dict
  def _session(result: dict) -> str
  def _ok(result: dict) -> bool


---
Filepath: ai/tools/implementations/shell.py  [python]

  def _is_blocked(command: str) -> bool
  def _workspace_dir(ctx: ToolContext, working_dir: str = '.') -> str
  async def shell_execute(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def shell_python(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: matrx_ai/tools/implementations/picklists_tools.py  [python]

  async def userlist_create(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def userlist_create_simple(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  def _make_serializable(obj: Any) -> Any
  async def userlist_get_all(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def userlist_get_details(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def userlist_update_item(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def userlist_batch_update(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/travel.py  [python]

  async def travel_get_location(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def travel_get_weather(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def travel_get_restaurants(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def travel_get_activities(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def travel_get_events(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def travel_create_summary(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/_summarize_helper.py  [python]

  async def summarize_content(content: str, instructions: str, ctx: ToolContext, model_id: str = 'gemini-2.5-flash-preview-05-20') -> tuple[str, list[TokenUsage]]


---
Filepath: ai/tools/implementations/personal_tables.py  [python]

  def _run_query(query_name: str, params: dict[str, Any]) -> list[dict]
  def _run_batch(query_name: str, batch_params: list[dict], batch_size: int = 50) -> list[dict]
  async def usertable_get_all(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def usertable_get_metadata(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def usertable_get_fields(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def usertable_get_data(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def usertable_search_data(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def usertable_add_rows(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def usertable_update_row(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def usertable_delete_row(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def usertable_create_advanced(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/web.py  [python]

  async def web_search(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def web_read(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def research_web(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def _search_with_query(query: str) -> tuple[str, dict[str, Any] | None]


---
Filepath: ai/tools/implementations/memory.py  [python]

  def _scope_id(ctx: ToolContext, scope: str) -> str | None
  async def memory_store(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def memory_recall(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def memory_search(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def memory_update(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def memory_forget(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/text.py  [python]

  async def text_analyze(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def text_regex_extract(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/code.py  [python]

  def _workspace_dir(ctx: ToolContext) -> Path
  async def code_execute_python(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def code_store_html(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  def _resolve_project_root(project_root: str) -> str | None
  async def code_fetch_code(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def code_fetch_tree(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/questionnaire.py  [python]

  async def interaction_ask(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/seo.py  [python]

  async def seo_check_meta_titles(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def seo_check_meta_descriptions(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def seo_check_meta_tags_batch(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def seo_get_keyword_data(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/filesystem.py  [python]

  def _resolve_path(relative: str, ctx: ToolContext) -> Path
  async def fs_read(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def fs_write(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def fs_list(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def fs_search(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def fs_mkdir(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: matrx_ai/tools/implementations/datasets_tools.py  [python]

  async def usertable_create(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/math.py  [python]

  async def math_calculate(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/browser.py  [python]

  def _mgr()
  async def browser_navigate(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def browser_click(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def browser_type_text(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def browser_screenshot(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def browser_close(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def browser_select_option(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def browser_wait_for(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def browser_get_element(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def browser_scroll(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/database.py  [python]

  def _get_async_supabase()
  def _is_blocked_table(table: str) -> bool
  def _is_read_only(table: str) -> bool
  def _is_safe_select(query: str) -> bool
  async def db_query(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def db_insert(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def db_update(args: dict[str, Any], ctx: ToolContext) -> ToolResult
  async def db_schema(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/implementations/news.py  [python]

  async def news_get_headlines(args: dict[str, Any], ctx: ToolContext) -> ToolResult


---
Filepath: ai/tools/arg_models/shell_args.py  [python]

  class ShellExecuteArgs(BaseModel):
  class ShellPythonArgs(BaseModel):


---
Filepath: ai/tools/arg_models/browser_args.py  [python]

  class BrowserNavigateArgs(BaseModel):
  class BrowserClickArgs(BaseModel):
  class BrowserTypeArgs(BaseModel):
  class BrowserScreenshotArgs(BaseModel):
  class BrowserSelectOptionArgs(BaseModel):
  class BrowserWaitForArgs(BaseModel):
  class BrowserGetElementArgs(BaseModel):
  class BrowserScrollArgs(BaseModel):
  class BrowserCloseArgs(BaseModel):


---
Filepath: ai/tools/arg_models/math_args.py  [python]

  class CalculateArgs(BaseModel):


---
Filepath: ai/tools/arg_models/memory_args.py  [python]

  class MemoryStoreArgs(BaseModel):
  class MemoryRecallArgs(BaseModel):
  class MemorySearchArgs(BaseModel):
  class MemoryUpdateArgs(BaseModel):
  class MemoryForgetArgs(BaseModel):


---
Filepath: ai/tools/arg_models/text_args.py  [python]

  class TextAnalyzeArgs(BaseModel):
  class RegexExtractArgs(BaseModel):


---
Filepath: ai/tools/arg_models/web_args.py  [python]

  class WebSearchArgs(BaseModel):
  class WebReadArgs(BaseModel):
  class WebResearchArgs(BaseModel):


---
Filepath: ai/tools/arg_models/db_args.py  [python]

  class DbQueryArgs(BaseModel):
  class DbInsertArgs(BaseModel):
  class DbUpdateArgs(BaseModel):
  class DbSchemaArgs(BaseModel):


---
Filepath: ai/tools/arg_models/fs_args.py  [python]

  class FsReadArgs(BaseModel):
  class FsWriteArgs(BaseModel):
  class FsListArgs(BaseModel):
  class FsSearchArgs(BaseModel):
  class FsMkdirArgs(BaseModel):


---
Filepath: ai/tools/output_models/seo.py  [python]

  class MonthlySearchItem(BaseModel):
  class SeoKeywordDataItem(BaseModel):
  class SeoKeywordDataOutput(BaseModel):
  def normalize_keyword_item(raw: dict[str, Any]) -> SeoKeywordDataItem

Please review carefully and let me know if anything needs to be improved.