Skip to content

Commit 7b2c271

Browse files
committed
Merge branch 'release-1.0' into audio-classification-with-no-mic
2 parents 4f24110 + f1d346d commit 7b2c271

File tree

13 files changed

+1413
-55
lines changed

13 files changed

+1413
-55
lines changed
Lines changed: 91 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,109 @@
1-
# Cloud LLM brick
1+
# Cloud LLM Brick
22

3-
This directory contains the implementation of the Cloud LLM brick, which provides an interface to interact with cloud-based Large Language Models (LLMs) through their REST API.
3+
The Cloud LLM Brick provides a seamless interface to interact with cloud-based Large Language Models (LLMs) such as OpenAI's GPT, Anthropic's Claude, and Google's Gemini. It abstracts the complexity of REST APIs, enabling you to send prompts, receive responses, and maintain conversational context within your Arduino projects.
44

55
## Overview
66

7-
The Cloud LLM brick allows users to send prompts to a specified LLM service and receive generated responses.
8-
It can be configured to work with a curated set of LLM providers that offer RESTful APIs, notably: ChatGPT, Claude and Gemini.
7+
This Brick acts as a gateway to powerful AI models hosted in the cloud. It is designed to handle the nuances of network communication, authentication, and session management. Whether you need a simple one-off answer or a continuous conversation with memory, the Cloud LLM Brick provides a unified API for different providers.
8+
9+
## Features
10+
11+
- **Multi-Provider Support**: Compatible with major LLM providers including Anthropic (Claude), OpenAI (GPT), and Google (Gemini).
12+
- **Conversational Memory**: Built-in support for windowed history, allowing the AI to remember context from previous exchanges.
13+
- **Streaming Responses**: Receive text chunks in real-time as they are generated, ideal for responsive user interfaces.
14+
- **Configurable Behavior**: Customize system prompts, temperature (creativity), and request timeouts.
15+
- **Simple API**: Unified `chat` and `chat_stream` methods regardless of the underlying model provider.
916

1017
## Prerequisites
1118

12-
Before using the Cloud LLM brick, ensure you have the following:
13-
- An account with a cloud-based LLM service (e.g., OpenAI, Cohere, etc.).
14-
- API access credentials (API key or token) for the LLM service.
15-
- Network connectivity to access the LLM service endpoint.
19+
- **Internet Connection**: The board must be connected to the internet to reach the LLM provider's API.
20+
- **API Key**: A valid API key for the chosen service (e.g., OpenAI API Key, Anthropic API Key).
21+
- **Python Dependencies**: The Brick relies on LangChain integration packages (`langchain-anthropic`, `langchain-openai`, `langchain-google-genai`).
1622

17-
## Features
23+
## Code Example and Usage
24+
25+
### Basic Conversation
1826

19-
- Send prompts to a cloud-based LLM service.
20-
- Receive and process responses from the LLM.
21-
- Supports both one-shot requests and memory for follow-up questions and answers.
22-
- Supports a curated set of LLM providers.
27+
This example initializes the Brick with an OpenAI model and performs a simple chat interaction.
2328

24-
## Code example and usage
25-
Here is a basic example of how to use the Cloud LLM brick:
29+
**Note:** The API key is not hardcoded. It is retrieved automatically from the **Brick Configuration** in App Lab.
2630

2731
```python
28-
from arduino.app_bricks.cloud_llm import CloudLLM
32+
import os
33+
from arduino.app_bricks.cloud_llm import CloudLLM, CloudModel
2934
from arduino.app_utils import App
3035

31-
llm = CloudLLM(api_key="your_api_key_here")
36+
# Initialize the Brick (API key is loaded from configuration)
37+
llm = CloudLLM(
38+
model=CloudModel.OPENAI_GPT,
39+
system_prompt="You are a helpful assistant for an IoT device."
40+
)
3241

33-
App.start_bricks()
42+
def simple_chat():
43+
# Send a prompt and print the response
44+
response = llm.chat("What is the capital of Italy?")
45+
print(f"AI: {response}")
3446

35-
response = llm.chat("What is the capital of France?")
36-
print(response)
47+
# Run the application
48+
App.run(simple_chat)
49+
```
50+
51+
### Streaming with Memory
52+
53+
This example demonstrates how to enable conversational memory and process the response as a stream of tokens.
54+
55+
```python
56+
from arduino.app_bricks.cloud_llm import CloudLLM, CloudModel
57+
from arduino.app_utils import App
3758

38-
App.stop_bricks()
59+
# Initialize with memory enabled (keeps last 10 messages)
60+
# API Key is retrieved automatically from Brick Configuration
61+
llm = CloudLLM(
62+
model=CloudModel.ANTHROPIC_CLAUDE
63+
).with_memory(max_messages=10)
64+
65+
def chat_loop():
66+
while True:
67+
user_input = input("You: ")
68+
if user_input.lower() in ["exit", "quit"]:
69+
break
70+
71+
print("AI: ", end="", flush=True)
72+
73+
# Stream the response token by token
74+
for token in llm.chat_stream(user_input):
75+
print(token, end="", flush=True)
76+
print() # Newline after response
77+
78+
App.run(chat_loop)
3979
```
80+
81+
## Configuration
82+
83+
The Brick is initialized with the following parameters:
84+
85+
| Parameter | Type | Default | Description |
86+
| :-------------- | :-------------------- | :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
87+
| `api_key` | `str` | `os.getenv("API_KEY")` | The authentication key for the LLM provider. **Recommended:** Set this via the **Brick Configuration** menu in App Lab instead of code. |
88+
| `model` | `str` \| `CloudModel` | `CloudModel.ANTHROPIC_CLAUDE` | The specific model to use. Accepts a `CloudModel` enum or its string value. |
89+
| `system_prompt` | `str` | `""` | A base instruction that defines the AI's behavior and persona. |
90+
| `temperature` | `float` | `0.7` | Controls randomness. `0.0` is deterministic, `1.0` is creative. |
91+
| `timeout` | `int` | `30` | Maximum time (in seconds) to wait for a response. |
92+
93+
### Supported Models
94+
95+
You can select a model using the `CloudModel` enum or by passing the corresponding raw string identifier.
96+
97+
| Enum Constant | Raw String ID | Provider Documentation |
98+
| :---------------------------- | :------------------------- | :-------------------------------------------------------------------------- |
99+
| `CloudModel.ANTHROPIC_CLAUDE` | `claude-3-7-sonnet-latest` | [Anthropic Models](https://docs.anthropic.com/en/docs/about-claude/models) |
100+
| `CloudModel.OPENAI_GPT` | `gpt-4o-mini` | [OpenAI Models](https://platform.openai.com/docs/models) |
101+
| `CloudModel.GOOGLE_GEMINI` | `gemini-2.5-flash` | [Google Gemini Models](https://ai.google.dev/gemini-api/docs/models/gemini) |
102+
103+
## Methods
104+
105+
- **`chat(message)`**: Sends a message and returns the complete response string. Blocks until generation is finished.
106+
- **`chat_stream(message)`**: Returns a generator yielding response tokens as they arrive.
107+
- **`stop_stream()`**: Interrupts an active streaming generation.
108+
- **`with_memory(max_messages)`**: Enables history tracking. `max_messages` defines the context window size.
109+
- **`clear_memory()`**: Resets the conversation history.
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
id: arduino:cloud_llm
22
name: Cloud LLM
33
description: "Cloud LLM Brick enables seamless integration with cloud-based Large Language Models (LLMs) for advanced AI capabilities in your Arduino projects."
4-
disabled: true
5-
64
variables:
7-
- API_KEY
5+
- name: API_KEY
6+
description: API Key for the cloud-based LLM service

src/arduino/app_bricks/cloud_llm/cloud_llm.py

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from langchain_core.messages import SystemMessage
1313
from langchain_core.output_parsers import StrOutputParser
1414
from langchain_core.chat_history import InMemoryChatMessageHistory
15+
from langsmith import uuid7
1516

1617
from arduino.app_utils import Logger, brick
1718

@@ -30,9 +31,11 @@ class AlreadyGenerating(Exception):
3031

3132
@brick
3233
class CloudLLM:
33-
"""A simplified, opinionated wrapper for common LangChain conversational patterns.
34+
"""A Brick for interacting with cloud-based Large Language Models (LLMs).
3435
35-
This class provides a single interface to manage stateless chat and chat with memory.
36+
This class wraps LangChain functionality to provide a simplified, unified interface
37+
for chatting with models like Claude, GPT, and Gemini. It supports both synchronous
38+
'one-shot' responses and streaming output, with optional conversational memory.
3639
"""
3740

3841
def __init__(
@@ -43,18 +46,24 @@ def __init__(
4346
temperature: Optional[float] = 0.7,
4447
timeout: int = 30,
4548
):
46-
"""Initializes the CloudLLM brick with the given configuration.
49+
"""Initializes the CloudLLM brick with the specified provider and configuration.
4750
4851
Args:
49-
api_key: The API key for the LLM service.
50-
model: The model identifier as per LangChain specification (e.g., "anthropic:claude-3-sonnet-20240229")
51-
or by using a CloudModels enum (e.g. CloudModels.OPENAI_GPT). Defaults to CloudModel.ANTHROPIC_CLAUDE.
52-
system_prompt: The global system-level instruction for the AI.
53-
temperature: The sampling temperature for response generation. Defaults to 0.7.
54-
timeout: The maximum time to wait for a response from the LLM service, in seconds. Defaults to 30 seconds.
52+
api_key (str): The API access key for the target LLM service. Defaults to the
53+
'API_KEY' environment variable.
54+
model (Union[str, CloudModel]): The model identifier. Accepts a `CloudModel`
55+
enum member (e.g., `CloudModel.OPENAI_GPT`) or its corresponding raw string
56+
value (e.g., `'gpt-4o-mini'`). Defaults to `CloudModel.ANTHROPIC_CLAUDE`.
57+
system_prompt (str): A system-level instruction that defines the AI's persona
58+
and constraints (e.g., "You are a helpful assistant"). Defaults to empty.
59+
temperature (Optional[float]): The sampling temperature between 0.0 and 1.0.
60+
Higher values make output more random/creative; lower values make it more
61+
deterministic. Defaults to 0.7.
62+
timeout (int): The maximum duration in seconds to wait for a response before
63+
timing out. Defaults to 30.
5564
5665
Raises:
57-
ValueError: If the API key is missing.
66+
ValueError: If `api_key` is not provided (empty string).
5867
"""
5968
if api_key == "":
6069
raise ValueError("API key is required to initialize CloudLLM brick.")
@@ -79,7 +88,7 @@ def __init__(
7988
timeout=self._timeout,
8089
)
8190
self._parser = StrOutputParser()
82-
self._history_cfg = {"configurable": {"session_id": "default_session"}}
91+
self._history_cfg = {"configurable": {"session_id": uuid7()}}
8392

8493
core_chain = self._prompt | self._model | self._parser
8594
self._chain = RunnableWithMessageHistory(
@@ -98,34 +107,34 @@ def __init__(
98107
def with_memory(self, max_messages: int = DEFAULT_MEMORY) -> "CloudLLM":
99108
"""Enables conversational memory for this instance.
100109
101-
This allows the chatbot to remember previous user and AI messages.
102-
Calling this modifies the instance to be stateful.
110+
Configures the Brick to retain a window of previous messages, allowing the
111+
AI to maintain context across multiple interactions.
103112
104113
Args:
105-
max_messages: The total number of past messages (user + AI) to
106-
keep in the conversation window. Set to 0 to disable memory.
114+
max_messages (int): The maximum number of messages (user + AI) to keep
115+
in history. Older messages are discarded. Set to 0 to disable memory.
116+
Defaults to 10.
107117
108118
Returns:
109-
self: The current CloudLLM instance for method chaining.
119+
CloudLLM: The current instance, allowing for method chaining.
110120
"""
111121
self._max_messages = max_messages
112122

113123
return self
114124

115125
def chat(self, message: str) -> str:
116-
"""Sends a single message to the AI and gets a complete response synchronously.
126+
"""Sends a message to the AI and blocks until the complete response is received.
117127
118-
This is the primary way to interact. It automatically handles memory
119-
based on how the instance was configured.
128+
This method automatically manages conversation history if memory is enabled.
120129
121130
Args:
122-
message: The user's message.
131+
message (str): The input text prompt from the user.
123132
124133
Returns:
125-
The AI's complete response as a string.
134+
str: The complete text response generated by the AI.
126135
127136
Raises:
128-
RuntimeError: If the chat model is not initialized or if text generation fails.
137+
RuntimeError: If the internal chain is not initialized or if the API request fails.
129138
"""
130139
if self._chain is None:
131140
raise RuntimeError("CloudLLM brick is not started. Please call start() before generating text.")
@@ -136,19 +145,20 @@ def chat(self, message: str) -> str:
136145
raise RuntimeError(f"Response generation failed: {e}")
137146

138147
def chat_stream(self, message: str) -> Iterator[str]:
139-
"""Sends a single message to the AI and streams the response as a synchronous generator.
148+
"""Sends a message to the AI and yields response tokens as they are generated.
140149
141-
Use this to get tokens as they are generated, perfect for a streaming UI.
150+
This allows for processing or displaying the response in real-time (streaming).
151+
The generation can be interrupted by calling `stop_stream()`.
142152
143153
Args:
144-
message: The user's message.
154+
message (str): The input text prompt from the user.
145155
146156
Yields:
147-
str: Chunks of the AI's response as they become available.
157+
str: Chunks of text (tokens) from the AI response.
148158
149159
Raises:
150-
RuntimeError: If the chat model is not initialized or if text generation fails.
151-
AlreadyGenerating: If the chat model is already streaming a response.
160+
RuntimeError: If the internal chain is not initialized or if the API request fails.
161+
AlreadyGenerating: If a streaming session is already active.
152162
"""
153163
if self._chain is None:
154164
raise RuntimeError("CloudLLM brick is not started. Please call start() before generating text.")
@@ -167,18 +177,33 @@ def chat_stream(self, message: str) -> Iterator[str]:
167177
self._keep_streaming.clear()
168178

169179
def stop_stream(self) -> None:
170-
"""Signals the LLM to stop generating a response."""
180+
"""Signals the active streaming generation to stop.
181+
182+
This sets an internal flag that causes the `chat_stream` iterator to break
183+
early. It has no effect if no stream is currently running.
184+
"""
171185
self._keep_streaming.clear()
172186

173187
def clear_memory(self) -> None:
174-
"""Clears the conversational memory.
188+
"""Clears the conversational memory history.
175189
176-
This only has an effect if with_memory() has been called.
190+
Resets the stored context. This is useful for starting a new conversation
191+
topic without previous context interfering. Only applies if memory is enabled.
177192
"""
178193
if self._history:
179194
self._history.clear()
180195

181196
def _get_session_history(self, session_id: str) -> WindowedChatMessageHistory:
197+
"""Retrieves or creates the chat history for a given session.
198+
199+
Internal callback used by LangChain's `RunnableWithMessageHistory`.
200+
201+
Args:
202+
session_id (str): The unique identifier for the session.
203+
204+
Returns:
205+
WindowedChatMessageHistory: The history object managing the message window.
206+
"""
182207
if self._max_messages == 0:
183208
self._history = InMemoryChatMessageHistory()
184209
if self._history is None:
@@ -187,6 +212,21 @@ def _get_session_history(self, session_id: str) -> WindowedChatMessageHistory:
187212

188213

189214
def model_factory(model_name: CloudModel, **kwargs) -> BaseChatModel:
215+
"""Factory function to instantiate the specific LangChain chat model.
216+
217+
This function maps the supported `CloudModel` enum values to their respective
218+
LangChain implementations.
219+
220+
Args:
221+
model_name (CloudModel): The enum or string identifier for the model.
222+
**kwargs: Additional arguments passed to the model constructor (e.g., api_key, temperature).
223+
224+
Returns:
225+
BaseChatModel: An instance of a LangChain chat model wrapper.
226+
227+
Raises:
228+
ValueError: If `model_name` does not match one of the supported `CloudModel` options.
229+
"""
190230
if model_name == CloudModel.ANTHROPIC_CLAUDE:
191231
from langchain_anthropic import ChatAnthropic
192232

0 commit comments

Comments
 (0)