defmodule SnakeBridge.Discovery do @moduledoc """ Discovery and introspection for Python libraries. This module provides functions to discover Python library schemas, either through automatic introspection or from cached descriptors. """ alias SnakeBridge.Discovery.Introspector @doc """ Discover a Python module's schema. ## Options * `:depth` - Discovery depth for submodules (default: 2) * `:config_hash` - Config hash for cache validation * `:use_cache` - Whether to use cached results (default: true) ## Example {:ok, schema} = SnakeBridge.Discovery.discover("dspy", depth: 3) """ @spec discover(String.t(), keyword()) :: {:ok, map()} | {:error, term()} def discover(module_path, opts \\ []) do Introspector.discover(Introspector, module_path, opts) end @doc """ Convert discovered schema to SnakeBridge config. """ @spec schema_to_config(map(), keyword()) :: SnakeBridge.Config.t() def schema_to_config(schema, opts \\ []) do python_module = Keyword.get(opts, :python_module, "unknown") %SnakeBridge.Config{ python_module: python_module, version: Map.get(schema, "library_version", "unknown"), classes: convert_classes(Map.get(schema, "classes", %{})), functions: convert_functions(Map.get(schema, "functions", %{})) } end defp convert_classes(classes_map) do Enum.map(classes_map, fn {_name, descriptor} -> python_path = Map.get(descriptor, :python_path) || Map.get(descriptor, "python_path") %{ python_path: python_path, elixir_module: python_path_to_module(python_path), constructor: %{args: %{}, session_aware: true}, methods: convert_methods(Map.get(descriptor, :methods) || Map.get(descriptor, "methods") || []) } end) end defp convert_methods(methods) when is_list(methods) do Enum.map(methods, fn method -> name = Map.get(method, :name) || Map.get(method, "name") %{ name: name, elixir_name: elixir_name_from_python(name), streaming: Map.get(method, :supports_streaming) || Map.get(method, "supports_streaming") || false } end) end defp convert_functions(functions_map) do Enum.map(functions_map, fn {_name, descriptor} -> python_path = Map.get(descriptor, :python_path) || Map.get(descriptor, "python_path") name = Map.get(descriptor, :name) || Map.get(descriptor, "name") %{ name: name, python_path: python_path, elixir_name: elixir_name_from_python(name), args: %{} } end) end defp python_path_to_module(python_path) when is_binary(python_path) do python_path |> String.split(".") |> Enum.map(&String.capitalize/1) |> Enum.join(".") |> String.to_atom() end defp elixir_name_from_python("__call__"), do: :call defp elixir_name_from_python("__init__"), do: :new defp elixir_name_from_python(name), do: String.to_atom(name) end