9 Commits

6 changed files with 164 additions and 27 deletions
+1
View File
@@ -5,3 +5,4 @@ export OLLAMA_HOST_URL=""
export OPENAI_API_KEY="" export OPENAI_API_KEY=""
export XAI_API_KEY="" export XAI_API_KEY=""
export AMAZON_PROFILE_NAME="" export AMAZON_PROFILE_NAME=""
export DEEPSEEK_API_KEY=""
+4
View File
@@ -1,6 +1,10 @@
Release History Release History
=============== ===============
## 0.3.3 (2024-02-08)
- Improve openai provider by removing debug print statements.
## 0.3.2 (2024-01-27) ## 0.3.2 (2024-01-27)
- Improve Deepseek provider. - Improve Deepseek provider.
+1 -1
View File
@@ -88,7 +88,7 @@ First, authenticate your API keys by setting them in the environment variables:
$ export OPENAI_API_KEY="sk-..." $ export OPENAI_API_KEY="sk-..."
``` ```
This pattern allows you to keep your API keys private and out of your codebase. Other supported environment variables: `ANTHROPIC_API_KEY`, `XAI_API_KEY`, `GROQ_API_KEY`, and `GEMINI_API_KEY`. This pattern allows you to keep your API keys private and out of your codebase. Other supported environment variables: `ANTHROPIC_API_KEY`, `XAI_API_KEY`, `DEEPSEEK_API_KEY`, `GROQ_API_KEY`, and `GEMINI_API_KEY`.
Next, import Simplemind and start using it: Next, import Simplemind and start using it:
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "simplemind" name = "simplemind"
version = "0.3.2" version = "0.3.3"
description = "An experimental client for AI providers that intends to replace LangChain and LangGraph for most common use cases." description = "An experimental client for AI providers that intends to replace LangChain and LangGraph for most common use cases."
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
+117 -11
View File
@@ -1,5 +1,5 @@
from functools import cached_property from functools import cached_property
from typing import TYPE_CHECKING, Iterator, Type, TypeVar from typing import TYPE_CHECKING, Callable, Iterator, Type, TypeVar
import instructor import instructor
from pydantic import BaseModel from pydantic import BaseModel
@@ -7,6 +7,7 @@ from pydantic import BaseModel
from ..logging import logger from ..logging import logger
from ..settings import settings from ..settings import settings
from ._base import BaseProvider from ._base import BaseProvider
from ._base_tools import BaseTool
if TYPE_CHECKING: if TYPE_CHECKING:
from ..models import Conversation, Message from ..models import Conversation, Message
@@ -14,6 +15,78 @@ if TYPE_CHECKING:
T = TypeVar("T", bound=BaseModel) T = TypeVar("T", bound=BaseModel)
class GroqTool(BaseTool):
def get_response_schema(self):
assert self.is_executed, f"Tool {self.name} was not executed."
assert isinstance(
self.tool_id, str
), f"Expected str for `tool_id` got {self.tool_id!r}"
return {
"role": "tool",
"tool_call_id": self.tool_id,
"content": self.function_result,
}
@logger
def handle(self, response, messages) -> None:
"""Handle the tool execution result from an API response."""
tool_used = False
# Get the message from the response
assistant_message = response.choices[0].message
# Check if there's a tool call
if assistant_message.tool_calls:
tool_call = assistant_message.tool_calls[
0
] # Get the first tool call
if tool_call.function.name == self.name:
# Execute the function
import json
function_args = json.loads(tool_call.function.arguments)
self.function_result = str(self.raw_func(**function_args))
self.tool_id = tool_call.id
tool_used = True
# Add assistant's message with tool call
messages.append(
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": tool_call.id,
"type": "function",
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments,
},
}
],
}
)
if tool_used:
# Add tool response message
messages.append(self.get_response_schema())
def get_input_schema(self):
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": self.get_properties_schema(),
"required": self.required,
"additionalProperties": False,
},
},
}
class Groq(BaseProvider): class Groq(BaseProvider):
NAME = "groq" NAME = "groq"
DEFAULT_MODEL = "llama3-8b-8192" DEFAULT_MODEL = "llama3-8b-8192"
@@ -46,28 +119,56 @@ class Groq(BaseProvider):
def send_conversation( def send_conversation(
self, self,
conversation: "Conversation", conversation: "Conversation",
tools: list[Callable | BaseTool] | None = None,
**kwargs, **kwargs,
) -> "Message": ) -> "Message":
"""Send a conversation to the Groq API.""" """Send a conversation to the Groq API."""
from ..models import Message from ..models import Message
messages = [ # Format messages from conversation
{"role": msg.role, "content": msg.text} for msg in conversation.messages formatted_messages = [
{"role": msg.role, "content": msg.text}
for msg in conversation.messages
] ]
response = self.client.chat.completions.create( # Set up tools if provided
model=conversation.llm_model or self.DEFAULT_MODEL, converted_tools = self.make_tools(tools)
messages=messages, tools_config = (
**{**self.DEFAULT_KWARGS, **kwargs}, [t.get_input_schema() for t in converted_tools] if tools else None
) )
# Get the response content from the Groq response # Merge all kwargs
assistant_message = response.choices[0].message request_kwargs = {
**self.DEFAULT_KWARGS,
**kwargs,
"model": conversation.llm_model or self.DEFAULT_MODEL,
"messages": formatted_messages,
}
if tools_config:
request_kwargs["tools"] = tools_config
# Make initial API call
response = self.client.chat.completions.create(**request_kwargs)
# Handle tool responses if needed
while response.choices[0].message.tool_calls:
print(response)
# Handle each tool call
for tool in converted_tools:
tool.handle(response, formatted_messages)
if tool.is_executed():
# Make another API call with the updated messages
response = self.client.chat.completions.create(
**request_kwargs
)
tool.reset_result()
final_message = response.choices[0].message.content
# Create and return a properly formatted Message instance
return Message( return Message(
role="assistant", role="assistant",
text=assistant_message.content or "", text=final_message or "",
raw=response, raw=response,
llm_model=conversation.llm_model or self.DEFAULT_MODEL, llm_model=conversation.llm_model or self.DEFAULT_MODEL,
llm_provider=self.NAME, llm_provider=self.NAME,
@@ -136,3 +237,8 @@ class Groq(BaseProvider):
raise RuntimeError( raise RuntimeError(
f"Failed to generate streaming text with Groq API: {e}" f"Failed to generate streaming text with Groq API: {e}"
) from e ) from e
@cached_property
def tool(self) -> Type[BaseTool]:
"""The tool implementation for Groq."""
return GroqTool
+40 -14
View File
@@ -38,9 +38,7 @@ class OpenAITool(BaseTool):
# Check if there's a tool call # Check if there's a tool call
if assistant_message.tool_calls: if assistant_message.tool_calls:
tool_call = assistant_message.tool_calls[ tool_call = assistant_message.tool_calls[0] # Get the first tool call
0
] # Get the first tool call
if tool_call.function.name == self.name: if tool_call.function.name == self.name:
# Execute the function # Execute the function
import json import json
@@ -128,8 +126,7 @@ class OpenAI(BaseProvider):
# Format messages from conversation # Format messages from conversation
formatted_messages = [ formatted_messages = [
{"role": msg.role, "content": msg.text} {"role": msg.role, "content": msg.text} for msg in conversation.messages
for msg in conversation.messages
] ]
# Set up tools if provided # Set up tools if provided
@@ -154,15 +151,12 @@ class OpenAI(BaseProvider):
# Handle tool responses if needed # Handle tool responses if needed
while response.choices[0].message.tool_calls: while response.choices[0].message.tool_calls:
print(response)
# Handle each tool call # Handle each tool call
for tool in converted_tools: for tool in converted_tools:
tool.handle(response, formatted_messages) tool.handle(response, formatted_messages)
if tool.is_executed(): if tool.is_executed():
# Make another API call with the updated messages # Make another API call with the updated messages
response = self.client.chat.completions.create( response = self.client.chat.completions.create(**request_kwargs)
**request_kwargs
)
tool.reset_result() tool.reset_result()
final_message = response.choices[0].message.content final_message = response.choices[0].message.content
@@ -182,13 +176,21 @@ class OpenAI(BaseProvider):
response_model: Type[T], response_model: Type[T],
*, *,
llm_model: str | None = None, llm_model: str | None = None,
image_url: str | None = None,
**kwargs, **kwargs,
) -> T: ) -> T:
"""Get a structured response from the OpenAI API.""" """Get a structured response from the OpenAI API."""
# Ensure messages are provided in kwargs # Ensure messages are provided in kwargs
messages = [ messages = [
{"role": "user", "content": prompt}, {"role": "user", "content": [{"type": "text", "text": prompt}]},
] ]
"""Add an image (url or base64-encoded) to the message if provided."""
if image_url:
messages[0]["content"].append(
{"type": "image_url", "image_url": {"url": image_url}}
)
response = self.structured_client.chat.completions.create( response = self.structured_client.chat.completions.create(
messages=messages, messages=messages,
model=llm_model or self.DEFAULT_MODEL, model=llm_model or self.DEFAULT_MODEL,
@@ -199,12 +201,24 @@ class OpenAI(BaseProvider):
@logger @logger
def generate_text( def generate_text(
self, prompt: str, *, llm_model: str | None = None, **kwargs self,
prompt: str,
*,
llm_model: str | None = None,
image_url: str | None = None,
**kwargs,
): ):
"""Generate text using the OpenAI API.""" """Generate text using the OpenAI API."""
messages = [ messages = [
{"role": "user", "content": prompt}, {"role": "user", "content": [{"type": "text", "text": prompt}]},
] ]
"""Add an image (url or base64-encoded) to the message if provided."""
if image_url:
messages[0]["content"].append(
{"type": "image_url", "image_url": {"url": image_url}}
)
response = self.client.chat.completions.create( response = self.client.chat.completions.create(
messages=messages, messages=messages,
model=llm_model or self.DEFAULT_MODEL, model=llm_model or self.DEFAULT_MODEL,
@@ -214,15 +228,27 @@ class OpenAI(BaseProvider):
@logger @logger
def generate_stream_text( def generate_stream_text(
self, prompt: str, *, llm_model: str | None = None, **kwargs self,
prompt: str,
*,
llm_model: str | None = None,
image_url: str | None = None,
**kwargs,
) -> Iterator[str]: ) -> Iterator[str]:
"""Generate streaming text using the OpenAI API. """Generate streaming text using the OpenAI API.
Yields chunks of text as they are generated by the model. Yields chunks of text as they are generated by the model.
""" """
messages = [ messages = [
{"role": "user", "content": prompt}, {"role": "user", "content": [{"type": "text", "text": prompt}]},
] ]
"""Add an image (url or base64-encoded) to the message if provided."""
if image_url:
messages[0]["content"].append(
{"type": "image_url", "image_url": {"url": image_url}}
)
response = self.client.chat.completions.create( response = self.client.chat.completions.create(
messages=messages, messages=messages,
model=llm_model or self.DEFAULT_MODEL, model=llm_model or self.DEFAULT_MODEL,