Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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}
]
3 changes: 2 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 3 additions & 1 deletion lib/mcpixir.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
58 changes: 32 additions & 26 deletions lib/mcpixir/agents/mcpagent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
127 changes: 52 additions & 75 deletions lib/mcpixir/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
[
Expand All @@ -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
Expand All @@ -37,124 +37,101 @@ 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 """
Checks if LangChain integration is available.
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

@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

@doc """
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
end
1 change: 1 addition & 0 deletions lib/mcpixir/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions lib/mcpixir/connectors/http.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions lib/mcpixir/connectors/stdio.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -70,6 +72,7 @@ defmodule Mcpixir.Connectors.StdioConnector do
%{port: port} when is_port(port) -> Port.close(port)
_ -> nil
end

:ok
end

Expand Down
Loading
Loading