diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index dbcfac4..0f5a94d 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -29,5 +29,11 @@ {"lib/mcpixir/agents/mcpagent.ex", :pattern_match_cov}, {"lib/mcpixir.ex", :call}, {"lib/mcpixir/agents/mcpagent.ex", :invalid_contract}, - {"lib/mcpixir/agents/mcpagent.ex", :pattern_match} + {"lib/mcpixir/agents/mcpagent.ex", :pattern_match}, + + # Ignore guard_fail warnings for LangChain checks + {"lib/mcpixir/application.ex", :guard_fail}, + {"lib/mcpixir/llm_client/anthropic.ex", :guard_fail}, + {"lib/mcpixir/llm_client/langchain.ex", :guard_fail}, + {"lib/mcpixir/llm_client/openai.ex", :guard_fail} ] \ No newline at end of file diff --git a/config/config.exs b/config/config.exs index 53c2e82..5aaaa3f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -3,7 +3,8 @@ import Config config :mcpixir, log_level: :info, default_servers: [], - environment: config_env() # Add this line to capture the environment + # Add this line to capture the environment + environment: config_env() # Import environment specific config import_config "#{config_env()}.exs" diff --git a/lib/mcpixir.ex b/lib/mcpixir.ex index 85aa352..332e669 100644 --- a/lib/mcpixir.ex +++ b/lib/mcpixir.ex @@ -82,7 +82,9 @@ defmodule Mcpixir do {:ok, prepared_agent} -> {:ok, prepared_agent} {:error, _} -> {:error, "Failed to prepare agent"} end - _ -> {:error, "Failed to create agent"} + + _ -> + {:error, "Failed to create agent"} end end diff --git a/lib/mcpixir/agents/mcpagent.ex b/lib/mcpixir/agents/mcpagent.ex index f06477e..1c96022 100644 --- a/lib/mcpixir/agents/mcpagent.ex +++ b/lib/mcpixir/agents/mcpagent.ex @@ -153,37 +153,43 @@ defmodule Mcpixir.Agents.MCPAgent do defp process_tool_calls(agent, tool_calls, content) do {updated_agent, tool_results} = Enum.reduce(tool_calls, {agent, []}, fn tool_call, {current_agent, results} -> - tool_name = tool_call["name"] || tool_call["function"]["name"] - arguments = tool_call["arguments"] || tool_call["function"]["arguments"] - tool_call_id = tool_call["id"] - - args = - case arguments do - args when is_binary(args) -> - case Jason.decode(args) do - {:ok, parsed} -> parsed - _ -> %{} - end - - args when is_map(args) -> - args - - _ -> - %{} - end - - case run_tool(current_agent, tool_name, args) do - {:ok, result} -> - handle_successful_tool_call(current_agent, tool_call_id, tool_name, result, results) - - {:error, reason} -> - handle_failed_tool_call(current_agent, tool_call_id, tool_name, reason, results) - end + process_single_tool_call(current_agent, tool_call, results) end) handle_tool_results(updated_agent, tool_results, content) end + defp process_single_tool_call(agent, tool_call, results) do + tool_name = tool_call["name"] || tool_call["function"]["name"] + arguments = tool_call["arguments"] || tool_call["function"]["arguments"] + tool_call_id = tool_call["id"] + args = parse_arguments(arguments) + + case run_tool(agent, tool_name, args) do + {:ok, result} -> + handle_successful_tool_call(agent, tool_call_id, tool_name, result, results) + + {:error, reason} -> + handle_failed_tool_call(agent, tool_call_id, tool_name, reason, results) + end + end + + defp parse_arguments(arguments) do + case arguments do + args when is_binary(args) -> + case Jason.decode(args) do + {:ok, parsed} -> parsed + _ -> %{} + end + + args when is_map(args) -> + args + + _ -> + %{} + end + end + defp handle_successful_tool_call(agent, tool_call_id, tool_name, result, results) do tool_result = %{ "tool_call_id" => tool_call_id, diff --git a/lib/mcpixir/application.ex b/lib/mcpixir/application.ex index 41633bd..cc1c7b2 100644 --- a/lib/mcpixir/application.ex +++ b/lib/mcpixir/application.ex @@ -9,15 +9,15 @@ defmodule Mcpixir.Application do def start(_type, _args) do # Get the current environment env = Application.get_env(:mcpixir, :environment, :dev) - + # Start supervision tree with proper children opts = [strategy: :one_for_one, name: Mcpixir.Supervisor] - + # Start supervision tree children(env) |> Supervisor.start_link(opts) end - + # Define children for each environment defp children(env) do [ @@ -26,7 +26,7 @@ defmodule Mcpixir.Application do {Registry, keys: :unique, name: Mcpixir.SessionRegistry} ] ++ maybe_add_langchain(env) end - + # Conditionally start LangChain in development and production defp maybe_add_langchain(env) when env in [:dev, :prod] do # Try to load LangChain and start any required services @@ -37,38 +37,36 @@ defmodule Mcpixir.Application do _ = Code.ensure_loaded?(LangChain.ChatModels) _ = Code.ensure_loaded?(LangChain.ChatModels.OpenAI) _ = Code.ensure_loaded?(LangChain.ChatModels.Anthropic) - + # We don't need to actually start any processes, just ensure modules are loaded [] - + false -> # LangChain not available, which is fine as it's optional [] end end - + # Don't load LangChain in test defp maybe_add_langchain(:test), do: [] @doc """ Load optional dependencies like LangChain - + Returns a boolean indicating whether LangChain is available. """ def load_optional_dependencies do - try do - # Try to load LangChain module itself - Code.ensure_loaded?(LangChain) and - # Also verify some key modules are available - Code.ensure_loaded?(LangChain.Message) and - Code.ensure_loaded?(LangChain.ChatModels) - rescue - # Handle any loading errors gracefully - _ -> false - catch - # Handle any unexpected issues - _, _ -> false - end + # Try to load LangChain module itself + # Also verify some key modules are available + Code.ensure_loaded?(LangChain) and + Code.ensure_loaded?(LangChain.Message) and + Code.ensure_loaded?(LangChain.ChatModels) + rescue + # Handle any loading errors gracefully + _ -> false + catch + # Handle any unexpected issues + _, _ -> false end @doc """ @@ -76,7 +74,7 @@ defmodule Mcpixir.Application do Always use this function instead of directly checking Code.ensure_loaded. """ def langchain_available? do - # This is a hardcoded true because we've removed the optional flag + # This is a hardcoded true because we've removed the optional flag # from the dependency in mix.exs and are forcing it to be loaded in all mix tasks true end @@ -84,45 +82,33 @@ defmodule Mcpixir.Application do @doc """ Gets the OpenAI module if available. """ - def openai_module do - try do - if langchain_available?() && Code.ensure_loaded?(LangChain.ChatModels.OpenAI) do - LangChain.ChatModels.OpenAI - else - nil - end - rescue - _ -> nil + def get_openai_module do + if langchain_available?() do + {:ok, LangChain.ChatModels.ChatOpenAI} + else + {:error, "LangChain library is not available"} end end @doc """ Gets the Anthropic module if available. """ - def anthropic_module do - try do - if langchain_available?() && Code.ensure_loaded?(LangChain.ChatModels.Anthropic) do - LangChain.ChatModels.Anthropic - else - nil - end - rescue - _ -> nil + def get_anthropic_module do + if langchain_available?() do + {:ok, LangChain.ChatModels.ChatAnthropic} + else + {:error, "LangChain library is not available"} end end @doc """ Gets the LangChain models module if available. """ - def langchain_models_module do - try do - if langchain_available?() do - LangChain.ChatModels - else - nil - end - rescue - _ -> nil + def get_langchain_module do + if langchain_available?() do + {:ok, LangChain} + else + {:error, "LangChain library is not available"} end end @@ -130,31 +116,22 @@ defmodule Mcpixir.Application do Formats messages for LangChain integration. Safely handles the conversion, falling back to the original messages if LangChain is unavailable. """ - def format_messages_for_langchain(messages) do - try do - if langchain_available?() do - Enum.map(messages, fn message -> - role = Map.get(message, :role) || Map.get(message, "role", "user") - content = Map.get(message, :content) || Map.get(message, "content", "") - - role_atom = - case role do - role when is_atom(role) -> role - role when is_binary(role) -> String.to_atom(role) - _ -> :user - end - - # Create the struct dynamically to avoid compile-time errors when LangChain is missing - struct(LangChain.Message, role: role_atom, content: content) - end) - else - messages - end - rescue - # Return original messages if any conversion fails - _ -> messages - catch - _, _ -> messages + def format_messages(messages) do + if langchain_available?() do + Enum.map(messages, &format_single_message/1) + else + messages + end + end + + defp format_single_message(message) do + role = Map.get(message, :role) || Map.get(message, "role", "user") + content = Map.get(message, :content) || Map.get(message, "content", "") + + case to_string(role) do + "system" -> LangChain.Message.new_system!(content) + "assistant" -> LangChain.Message.new_assistant!(content) + _ -> LangChain.Message.new_user!(content) end end -end \ No newline at end of file +end diff --git a/lib/mcpixir/config.ex b/lib/mcpixir/config.ex index f19479d..b92fadb 100644 --- a/lib/mcpixir/config.ex +++ b/lib/mcpixir/config.ex @@ -56,6 +56,7 @@ defmodule Mcpixir.Config do read_config_file(path) end) end + defp read_config_file(path) do with {:ok, content} <- File.read(path), {:ok, config} <- Jason.decode(content) do diff --git a/lib/mcpixir/connectors/http.ex b/lib/mcpixir/connectors/http.ex index 2e3f4b7..ab1e6da 100644 --- a/lib/mcpixir/connectors/http.ex +++ b/lib/mcpixir/connectors/http.ex @@ -44,7 +44,8 @@ defmodule Mcpixir.Connectors.HttpConnector do end @impl Mcpixir.Connectors.Base - @spec initialize(Mcpixir.Connectors.Base.connector()) :: {:ok, Mcpixir.Connectors.Base.connector()} | {:error, any()} + @spec initialize(Mcpixir.Connectors.Base.connector()) :: + {:ok, Mcpixir.Connectors.Base.connector()} | {:error, any()} def initialize(connector) do Base.initialize(connector) end @@ -56,7 +57,8 @@ defmodule Mcpixir.Connectors.HttpConnector do end @impl Mcpixir.Connectors.Base - @spec execute_tool(Mcpixir.Connectors.Base.connector(), String.t(), map()) :: {:ok, any()} | {:error, any()} + @spec execute_tool(Mcpixir.Connectors.Base.connector(), String.t(), map()) :: + {:ok, any()} | {:error, any()} def execute_tool(connector, tool_name, args) do Base.execute_tool(connector, tool_name, args) end diff --git a/lib/mcpixir/connectors/stdio.ex b/lib/mcpixir/connectors/stdio.ex index 86f3f74..7d08450 100644 --- a/lib/mcpixir/connectors/stdio.ex +++ b/lib/mcpixir/connectors/stdio.ex @@ -46,7 +46,8 @@ defmodule Mcpixir.Connectors.StdioConnector do end @impl Mcpixir.Connectors.Base - @spec initialize(Mcpixir.Connectors.Base.connector()) :: {:ok, Mcpixir.Connectors.Base.connector()} | {:error, any()} + @spec initialize(Mcpixir.Connectors.Base.connector()) :: + {:ok, Mcpixir.Connectors.Base.connector()} | {:error, any()} def initialize(connector) do Base.initialize(connector) end @@ -58,7 +59,8 @@ defmodule Mcpixir.Connectors.StdioConnector do end @impl Mcpixir.Connectors.Base - @spec execute_tool(Mcpixir.Connectors.Base.connector(), String.t(), map()) :: {:ok, any()} | {:error, any()} + @spec execute_tool(Mcpixir.Connectors.Base.connector(), String.t(), map()) :: + {:ok, any()} | {:error, any()} def execute_tool(connector, tool_name, args) do Base.execute_tool(connector, tool_name, args) end @@ -70,6 +72,7 @@ defmodule Mcpixir.Connectors.StdioConnector do %{port: port} when is_port(port) -> Port.close(port) _ -> nil end + :ok end diff --git a/lib/mcpixir/connectors/websocket.ex b/lib/mcpixir/connectors/websocket.ex index 12b7a0e..4efaf9e 100644 --- a/lib/mcpixir/connectors/websocket.ex +++ b/lib/mcpixir/connectors/websocket.ex @@ -49,7 +49,8 @@ defmodule Mcpixir.Connectors.WebSocketConnector do end @impl Mcpixir.Connectors.Base - @spec initialize(Mcpixir.Connectors.Base.connector()) :: {:ok, Mcpixir.Connectors.Base.connector()} | {:error, any()} + @spec initialize(Mcpixir.Connectors.Base.connector()) :: + {:ok, Mcpixir.Connectors.Base.connector()} | {:error, any()} def initialize(connector) do Base.initialize(connector) end @@ -61,7 +62,8 @@ defmodule Mcpixir.Connectors.WebSocketConnector do end @impl Mcpixir.Connectors.Base - @spec execute_tool(Mcpixir.Connectors.Base.connector(), String.t(), map()) :: {:ok, any()} | {:error, any()} + @spec execute_tool(Mcpixir.Connectors.Base.connector(), String.t(), map()) :: + {:ok, any()} | {:error, any()} def execute_tool(connector, tool_name, args) do Base.execute_tool(connector, tool_name, args) end @@ -70,11 +72,15 @@ defmodule Mcpixir.Connectors.WebSocketConnector do @spec close(Mcpixir.Connectors.Base.connector()) :: :ok | {:error, any()} def close(connector) do case connector do - %{client_pid: pid, response_table: table} when is_pid(pid) and (is_atom(table) or is_reference(table)) -> + %{client_pid: pid, response_table: table} + when is_pid(pid) and (is_atom(table) or is_reference(table)) -> WebSockex.cast(pid, :terminate) :ets.delete(table) - _ -> nil + + _ -> + nil end + :ok end @@ -128,14 +134,17 @@ defmodule Mcpixir.Connectors.WebSocketConnector do case :ets.lookup(connector.response_table, id) do [{^id, response}] -> handle_websocket_response(connector, id, response) + [] -> Process.sleep(100) wait_for_response(connector, id, attempts + 1) end end end + defp handle_websocket_response(connector, id, response) do :ets.delete(connector.response_table, id) + if Map.has_key?(response, "error") do {:error, response["error"]} else diff --git a/lib/mcpixir/llm_client.ex b/lib/mcpixir/llm_client.ex index ba6ae97..aea7492 100644 --- a/lib/mcpixir/llm_client.ex +++ b/lib/mcpixir/llm_client.ex @@ -1,7 +1,7 @@ defmodule Mcpixir.LLMClient do @moduledoc """ Client for interacting with LLM providers. - + This module defines the interface for LLM clients and provides basic implementation that delegates to provider-specific modules. """ @@ -15,50 +15,57 @@ defmodule Mcpixir.LLMClient do :provider, :config ] - + @doc """ Creates a new LLM client with the given configuration. """ @spec new(map()) :: {:ok, t()} def new(config) do provider = Map.get(config, :provider, :mock) - + llm_client = %__MODULE__{ provider: provider, config: config } - + {:ok, llm_client} end - + @doc """ Runs a query using the LLM client. """ @spec run(t(), list()) :: {:ok, map()} | {:error, String.t()} def run(client, messages) do provider_module = get_provider_module(client.provider) - + if provider_module do provider_module.run(client, messages) else # Fallback when provider module isn't available - {:ok, %{"role" => "assistant", "content" => "LLM integration is not available. Using placeholder response."}} + {:ok, + %{ + "role" => "assistant", + "content" => "LLM integration is not available. Using placeholder response." + }} end end - + # Private helper function to map provider to module defp get_provider_module(provider) do + alias Mcpixir.LLMClient.{Anthropic, LangChain, OpenAI} + case provider do - :openai -> - if Code.ensure_loaded?(Mcpixir.LLMClient.OpenAI), do: Mcpixir.LLMClient.OpenAI, else: nil - - :anthropic -> - if Code.ensure_loaded?(Mcpixir.LLMClient.Anthropic), do: Mcpixir.LLMClient.Anthropic, else: nil - - :langchain -> - if Code.ensure_loaded?(Mcpixir.LLMClient.LangChain), do: Mcpixir.LLMClient.LangChain, else: nil - - _ -> nil + :openai -> + if Code.ensure_loaded?(OpenAI), do: OpenAI, else: nil + + :anthropic -> + if Code.ensure_loaded?(Anthropic), do: Anthropic, else: nil + + :langchain -> + if Code.ensure_loaded?(LangChain), do: LangChain, else: nil + + _ -> + nil end end -end \ No newline at end of file +end diff --git a/lib/mcpixir/llm_client/anthropic.ex b/lib/mcpixir/llm_client/anthropic.ex index d22a4f4..18458d6 100644 --- a/lib/mcpixir/llm_client/anthropic.ex +++ b/lib/mcpixir/llm_client/anthropic.ex @@ -8,9 +8,9 @@ defmodule Mcpixir.LLMClient.Anthropic do @behaviour Mcpixir.LLMClient.Base - alias LangChain.Message alias LangChain.Chains.LLMChain alias LangChain.ChatModels.ChatAnthropic + alias LangChain.Message @impl Mcpixir.LLMClient.Base def run(client, messages) do @@ -66,7 +66,7 @@ defmodule Mcpixir.LLMClient.Anthropic do _ -> Message.new_user!(content) end - LangChain.Chains.LLMChain.add_message(acc_chain, langchain_message) + LLMChain.add_message(acc_chain, langchain_message) end) end diff --git a/lib/mcpixir/llm_client/base.ex b/lib/mcpixir/llm_client/base.ex index 738e06b..8edf203 100644 --- a/lib/mcpixir/llm_client/base.ex +++ b/lib/mcpixir/llm_client/base.ex @@ -1,7 +1,7 @@ defmodule Mcpixir.LLMClient.Base do @moduledoc """ Base behaviour for LLM clients. - + This module defines the common behavior that all LLM client providers must implement. It allows for a consistent interface when working with different language models. @@ -9,15 +9,15 @@ defmodule Mcpixir.LLMClient.Base do @doc """ Run the LLM with the given messages and return a response. - + The messages should be a list of structured message maps containing at least: - role: The role of the message sender (system, user, assistant) - content: The content of the message - + Returns {:ok, response} where response is a map with at least: - role: always "assistant" - content: The generated response text - tool_calls: (optional) List of tool calls if the LLM wishes to use tools """ @callback run(Mcpixir.LLMClient.t(), list()) :: {:ok, map()} | {:error, String.t()} -end \ No newline at end of file +end diff --git a/lib/mcpixir/llm_client/langchain.ex b/lib/mcpixir/llm_client/langchain.ex index a630d1e..afbec9c 100644 --- a/lib/mcpixir/llm_client/langchain.ex +++ b/lib/mcpixir/llm_client/langchain.ex @@ -1,41 +1,38 @@ defmodule Mcpixir.LLMClient.LangChain do @moduledoc """ LangChain-specific implementation of the LLM client. - + This module handles using a pre-configured LangChain model instance that is provided directly by the user. """ - + @behaviour Mcpixir.LLMClient.Base - - @impl true + + @impl Mcpixir.LLMClient.Base def run(client, messages) do if langchain_available?() do run_with_langchain(client, messages) else # Fallback if LangChain isn't available - {:ok, %{"role" => "assistant", "content" => "LangChain library is not available."}} + {:ok, + %{"role" => "assistant", "content" => "LangChain integration requires LangChain library."}} end end - + # Private functions - + defp run_with_langchain(client, messages) do - # This is for when the user has pre-configured a LangChain instance - langchain_llm = Map.get(client.config, :model) - - if is_nil(langchain_llm) do - {:error, "LangChain LLM model not provided in configuration"} - else - module = langchain_module() - - if module do - # Convert messages to LangChain format - formatted_messages = format_messages_for_langchain(messages) - - # Run the model with the pre-configured LangChain instance + with {:ok, LangChain} <- get_langchain_module(), + {:ok, formatted_messages} <- format_messages(messages) do + # This is for when the user has pre-configured a LangChain instance + langchain_llm = Map.get(client.config, :model) + + if is_nil(langchain_llm) do + {:error, "LangChain LLM model not provided in configuration"} + else try do - result = apply(module, :call, [langchain_llm, formatted_messages, []]) + # Use the appropriate LangChain function based on the model type + result = apply_langchain_model(langchain_llm, formatted_messages) {:ok, %{"role" => "assistant", "content" => result.content}} rescue e -> @@ -44,21 +41,30 @@ defmodule Mcpixir.LLMClient.LangChain do :exit, reason -> {:error, "LangChain request exited: #{inspect(reason)}"} end - else - {:error, "LangChain.ChatModels module not available"} end + else + {:error, reason} -> {:error, reason} end end - - defp langchain_available? do - Mcpixir.Application.langchain_available?() + + defp apply_langchain_model(model, messages) do + # Use the appropriate function based on the model type + if function_exported?(model, :call, 3) do + model.call(messages, [], []) + else + model.run(messages) + end + end + + defp get_langchain_module do + Mcpixir.Application.get_langchain_module() end - - defp langchain_module do - Mcpixir.Application.langchain_models_module() + + defp format_messages(messages) do + {:ok, Mcpixir.Application.format_messages(messages)} end - - defp format_messages_for_langchain(messages) do - Mcpixir.Application.format_messages_for_langchain(messages) + + defp langchain_available? do + Mcpixir.Application.langchain_available?() end -end \ No newline at end of file +end diff --git a/lib/mcpixir/llm_client/mock.ex b/lib/mcpixir/llm_client/mock.ex index d0c8a8b..0e0894c 100644 --- a/lib/mcpixir/llm_client/mock.ex +++ b/lib/mcpixir/llm_client/mock.ex @@ -1,15 +1,19 @@ defmodule Mcpixir.LLMClient.Mock do @moduledoc """ Mock implementation of the LLM client. - + This module provides a mock implementation that can be used for testing or when no other LLM provider is available. """ - + @behaviour Mcpixir.LLMClient.Base - + @impl true def run(_client, _messages) do - {:ok, %{"role" => "assistant", "content" => "This is a placeholder LLM response from the mock provider."}} + {:ok, + %{ + "role" => "assistant", + "content" => "This is a placeholder LLM response from the mock provider." + }} end -end \ No newline at end of file +end diff --git a/lib/mcpixir/llm_client/openai.ex b/lib/mcpixir/llm_client/openai.ex index 2aa2b3c..f4185cf 100644 --- a/lib/mcpixir/llm_client/openai.ex +++ b/lib/mcpixir/llm_client/openai.ex @@ -66,7 +66,7 @@ defmodule Mcpixir.LLMClient.OpenAI do _ -> Message.new_user!(content) end - LangChain.Chains.LLMChain.add_message(acc_chain, langchain_message) + LLMChain.add_message(acc_chain, langchain_message) end) end diff --git a/lib/mcpixir/server_manager.ex b/lib/mcpixir/server_manager.ex index 4551686..6435c40 100644 --- a/lib/mcpixir/server_manager.ex +++ b/lib/mcpixir/server_manager.ex @@ -150,14 +150,30 @@ defmodule Mcpixir.ServerManager do defp find_best_server(remaining_tools, tool_to_servers) do Enum.reduce(remaining_tools, {nil, 0}, fn tool, {best_server, max_count} -> servers = Map.get(tool_to_servers, tool, MapSet.new()) - find_server_with_most_coverage(servers, tool, remaining_tools, tool_to_servers, best_server, max_count) + + find_server_with_most_coverage( + servers, + tool, + remaining_tools, + tool_to_servers, + best_server, + max_count + ) end) end - defp find_server_with_most_coverage(servers, tool, remaining_tools, tool_to_servers, best_server, max_count) do + defp find_server_with_most_coverage( + servers, + tool, + remaining_tools, + tool_to_servers, + best_server, + max_count + ) do Enum.reduce(servers, {best_server, max_count}, fn server, {current_best, current_max} -> covered_tools = MapSet.new(Map.get(tool_to_servers, tool, [])) covered_count = MapSet.intersection(remaining_tools, covered_tools) |> MapSet.size() + if covered_count > current_max do {server, covered_count} else diff --git a/lib/mcpixir/task_managers/websocket.ex b/lib/mcpixir/task_managers/websocket.ex index 7dbcb01..dee35a8 100644 --- a/lib/mcpixir/task_managers/websocket.ex +++ b/lib/mcpixir/task_managers/websocket.ex @@ -8,10 +8,10 @@ defmodule Mcpixir.TaskManagers.WebSocketManager do use WebSockex @type t :: %__MODULE__{ - url: String.t(), - client_pid: pid() | nil, - config: map() - } + url: String.t(), + client_pid: pid() | nil, + config: map() + } defstruct [ :url, diff --git a/lib/mix/tasks/mcpixir.airbnb.ex b/lib/mix/tasks/mcpixir.airbnb.ex index ecf6b6a..3a3daf1 100644 --- a/lib/mix/tasks/mcpixir.airbnb.ex +++ b/lib/mix/tasks/mcpixir.airbnb.ex @@ -46,9 +46,8 @@ defmodule Mix.Tasks.Mcpixir.Airbnb do Application.ensure_all_started(:httpoison) Application.ensure_all_started(:jason) Application.ensure_all_started(:langchain) - # Force load LangChain core modules - Code.ensure_loaded?(LangChain) + Code.ensure_loaded?(LangChain) Code.ensure_loaded?(LangChain.Message) Code.ensure_loaded?(LangChain.ChatModels) @@ -82,12 +81,11 @@ defmodule Mix.Tasks.Mcpixir.Airbnb do defp run_airbnb_example(provider, model, query, config_path) do # Start the application to ensure all dependencies are loaded Application.ensure_all_started(:mcpixir) - + # Explicitly ensure LangChain is started Application.ensure_all_started(:langchain) - + # Skip the LangChain check since we've made it a required dependency - # Load required API keys load_api_keys() diff --git a/lib/mix/tasks/mcpixir.blender.ex b/lib/mix/tasks/mcpixir.blender.ex index 41b14db..6cb7ac3 100644 --- a/lib/mix/tasks/mcpixir.blender.ex +++ b/lib/mix/tasks/mcpixir.blender.ex @@ -54,9 +54,8 @@ defmodule Mix.Tasks.Mcpixir.Blender do Application.ensure_all_started(:httpoison) Application.ensure_all_started(:jason) Application.ensure_all_started(:langchain) - # Force load LangChain core modules - Code.ensure_loaded?(LangChain) + Code.ensure_loaded?(LangChain) Code.ensure_loaded?(LangChain.Message) Code.ensure_loaded?(LangChain.ChatModels) @@ -90,9 +89,8 @@ defmodule Mix.Tasks.Mcpixir.Blender do defp run_blender_example(provider, model, query, command) do # Explicitly ensure LangChain is started Application.ensure_all_started(:langchain) - + # Skip the LangChain check since we've made it a required dependency - # Load required API keys load_api_keys() diff --git a/lib/mix/tasks/mcpixir.browser.ex b/lib/mix/tasks/mcpixir.browser.ex index 53b0df5..026e9d1 100644 --- a/lib/mix/tasks/mcpixir.browser.ex +++ b/lib/mix/tasks/mcpixir.browser.ex @@ -48,9 +48,8 @@ defmodule Mix.Tasks.Mcpixir.Browser do Application.ensure_all_started(:httpoison) Application.ensure_all_started(:jason) Application.ensure_all_started(:langchain) - # Force load LangChain core modules - Code.ensure_loaded?(LangChain) + Code.ensure_loaded?(LangChain) Code.ensure_loaded?(LangChain.Message) Code.ensure_loaded?(LangChain.ChatModels) @@ -83,9 +82,8 @@ defmodule Mix.Tasks.Mcpixir.Browser do defp run_browser_example(provider, model, query, config_path) do # Explicitly ensure LangChain is started Application.ensure_all_started(:langchain) - + # Skip the LangChain check since we've made it a required dependency - # Load required API keys load_api_keys() diff --git a/lib/mix/tasks/mcpixir.chat.ex b/lib/mix/tasks/mcpixir.chat.ex index 7ca52f3..4758679 100644 --- a/lib/mix/tasks/mcpixir.chat.ex +++ b/lib/mix/tasks/mcpixir.chat.ex @@ -44,9 +44,8 @@ defmodule Mix.Tasks.Mcpixir.Chat do Application.ensure_all_started(:httpoison) Application.ensure_all_started(:jason) Application.ensure_all_started(:langchain) - # Force load LangChain core modules - Code.ensure_loaded?(LangChain) + Code.ensure_loaded?(LangChain) Code.ensure_loaded?(LangChain.Message) Code.ensure_loaded?(LangChain.ChatModels) @@ -72,9 +71,8 @@ defmodule Mix.Tasks.Mcpixir.Chat do defp run_chat_example(provider, model, query) do # Explicitly ensure LangChain is started Application.ensure_all_started(:langchain) - + # Skip the LangChain check since we've made it a required dependency - # Load required API keys load_api_keys() diff --git a/lib/mix/tasks/mcpixir.filesystem.ex b/lib/mix/tasks/mcpixir.filesystem.ex index 2ea4165..1659a2d 100644 --- a/lib/mix/tasks/mcpixir.filesystem.ex +++ b/lib/mix/tasks/mcpixir.filesystem.ex @@ -46,9 +46,8 @@ defmodule Mix.Tasks.Mcpixir.Filesystem do Application.ensure_all_started(:httpoison) Application.ensure_all_started(:jason) Application.ensure_all_started(:langchain) - # Force load LangChain core modules - Code.ensure_loaded?(LangChain) + Code.ensure_loaded?(LangChain) Code.ensure_loaded?(LangChain.Message) Code.ensure_loaded?(LangChain.ChatModels) @@ -82,9 +81,8 @@ defmodule Mix.Tasks.Mcpixir.Filesystem do defp run_filesystem_example(provider, model, query, path) do # Explicitly ensure LangChain is started Application.ensure_all_started(:langchain) - + # Skip the LangChain check since we've made it a required dependency - # Load required API keys load_api_keys() diff --git a/lib/mix/tasks/mcpixir.http.ex b/lib/mix/tasks/mcpixir.http.ex index 905728c..6a80e80 100644 --- a/lib/mix/tasks/mcpixir.http.ex +++ b/lib/mix/tasks/mcpixir.http.ex @@ -53,9 +53,8 @@ defmodule Mix.Tasks.Mcpixir.Http do Application.ensure_all_started(:httpoison) Application.ensure_all_started(:jason) Application.ensure_all_started(:langchain) - # Force load LangChain core modules - Code.ensure_loaded?(LangChain) + Code.ensure_loaded?(LangChain) Code.ensure_loaded?(LangChain.Message) Code.ensure_loaded?(LangChain.ChatModels) @@ -85,9 +84,8 @@ defmodule Mix.Tasks.Mcpixir.Http do defp run_http_example(provider, model, query, url) do # Explicitly ensure LangChain is started Application.ensure_all_started(:langchain) - + # Skip the LangChain check since we've made it a required dependency - # Load required API keys load_api_keys() diff --git a/lib/mix/tasks/mcpixir.release.ex b/lib/mix/tasks/mcpixir.release.ex index b69a824..d7a41b2 100644 --- a/lib/mix/tasks/mcpixir.release.ex +++ b/lib/mix/tasks/mcpixir.release.ex @@ -131,6 +131,7 @@ defmodule Mix.Tasks.Mcpixir.Release do IO.puts("Running tests...") cmd_output = System.cmd("mix", ["test"], stderr_to_stdout: true) + case cmd_output do {_, 0} -> :ok {output, _} -> raise "Tests failed, aborting release\n#{output}" @@ -141,6 +142,7 @@ defmodule Mix.Tasks.Mcpixir.Release do IO.puts("Compiling documentation...") cmd_output = System.cmd("mix", ["docs"], stderr_to_stdout: true) + case cmd_output do {_, 0} -> :ok {output, _} -> raise "Documentation compilation failed, aborting release\n#{output}" @@ -162,6 +164,7 @@ defmodule Mix.Tasks.Mcpixir.Release do IO.puts("Publishing to Hex.pm...") cmd_output = System.cmd("mix", ["hex.publish"], stderr_to_stdout: true) + case cmd_output do {_, 0} -> :ok {output, _} -> raise "Failed to publish to Hex.pm\n#{output}" diff --git a/mix.exs b/mix.exs index a429b34..869e89e 100644 --- a/mix.exs +++ b/mix.exs @@ -12,21 +12,21 @@ defmodule Mcpixir.MixProject do start_permanent: Mix.env() == :prod, deps: deps(), elixirc_paths: elixirc_paths(Mix.env()), - + # Hex description: "Elixir library to connect LLMs to MCP servers for tool use capabilities", package: package(), - + # Docs name: "Mcpixir", docs: docs(), - + # Dialyzer dialyzer: [ ignore_warnings: ".dialyzer_ignore.exs", plt_file: {:no_warn, "priv/plts/dialyzer.plt"} ], - + # Testing test_coverage: [tool: ExCoveralls], preferred_cli_env: [ @@ -48,33 +48,44 @@ defmodule Mcpixir.MixProject do defp deps do [ # Core dependencies - {:jason, "~> 1.4"}, # JSON parsing (equivalent to Python's json) - {:httpoison, "~> 2.1"}, # HTTP client (equivalent to aiohttp) - {:websockex, "~> 0.4.3"}, # WebSockets client (equivalent to websockets) - {:nimble_pool, "~> 1.0"}, # Resource pooling - + # JSON parsing (equivalent to Python's json) + {:jason, "~> 1.4"}, + # HTTP client (equivalent to aiohttp) + {:httpoison, "~> 2.1"}, + # WebSockets client (equivalent to websockets) + {:websockex, "~> 0.4.3"}, + # Resource pooling + {:nimble_pool, "~> 1.0"}, + # Schema validation - {:ex_json_schema, "~> 0.9.2"}, # JSON Schema validation (equiv to jsonschema) - {:ecto, "~> 3.10"}, # Data validation (similar to pydantic) - + # JSON Schema validation (equiv to jsonschema) + {:ex_json_schema, "~> 0.9.2"}, + # Data validation (similar to pydantic) + {:ecto, "~> 3.10"}, + # Utility libraries - {:uniq, "~> 0.6"}, # Modern UUID generation with UUIDv7 support - {:poolboy, "~> 1.5"}, # Process pooling - {:typed_struct, "~> 0.3.0"}, # Type definitions (similar to typing-extensions) - + # Modern UUID generation with UUIDv7 support + {:uniq, "~> 0.6"}, + # Process pooling + {:poolboy, "~> 1.5"}, + # Type definitions (similar to typing-extensions) + {:typed_struct, "~> 0.3.0"}, + # LLM integrations - {:langchain, "~> 0.2.0"}, # LangChain integration for LLMs - {:req, "~> 0.4"}, # HTTP client for API calls + # LangChain integration for LLMs + {:langchain, "~> 0.2.0"}, + # HTTP client for API calls + {:req, "~> 0.4"}, # Documentation {:ex_doc, "~> 0.29", only: :dev, runtime: false}, - + # Development and testing {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.3", only: [:dev, :test], runtime: false}, {:mix_test_watch, "~> 1.1", only: [:dev, :test]}, {:excoveralls, "~> 0.18", only: :test}, - + # Testing {:mock, "~> 0.3.0", only: :test}, {:bypass, "~> 2.1", only: :test} @@ -105,18 +116,18 @@ defmodule Mcpixir.MixProject do "LICENSE" ], groups_for_modules: [ - "Core": [ + Core: [ Mcpixir, Mcpixir.Client, Mcpixir.Session, Mcpixir.Config, - Mcpixir.ServerManager, + Mcpixir.ServerManager ], - "Agents": [ + Agents: [ Mcpixir.Agents.Base, Mcpixir.Agents.MCPAgent ], - "Connectors": [ + Connectors: [ Mcpixir.Connectors.Base, Mcpixir.Connectors.HttpConnector, Mcpixir.Connectors.WebSocketConnector, @@ -128,10 +139,10 @@ defmodule Mcpixir.MixProject do Mcpixir.TaskManagers.WebSocketManager, Mcpixir.TaskManagers.SSEManager ], - "Adapters": [ + Adapters: [ Mcpixir.Adapters.LangChainAdapter ], - "Tools": [ + Tools: [ Mix.Tasks.Mcp, Mix.Tasks.Mcp.Chat, Mix.Tasks.Mcp.Airbnb, @@ -148,4 +159,4 @@ defmodule Mcpixir.MixProject do defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] -end \ No newline at end of file +end