Skip to content

Commit 44192e2

Browse files
authored
Merge pull request #1346 from OpenInterpreter/development
Development Update
2 parents c044179 + 098e4da commit 44192e2

File tree

9 files changed

+176
-58
lines changed

9 files changed

+176
-58
lines changed

docs/server/usage.mdx

Lines changed: 119 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
---
2-
title: Server Usage
3-
---
1+
# Server Usage Guide
42

53
## Starting the Server
64

@@ -27,15 +25,29 @@ async_interpreter.server.run(port=8000) # Default port is 8000, but you can cus
2725
Connect to the WebSocket server at `ws://localhost:8000/`.
2826

2927
### Message Format
30-
Messages must follow the LMC format with start and end flags. For detailed specifications, see the [LMC messages documentation](https://docs.openinterpreter.com/protocols/lmc-messages).
28+
The server uses an extended message format that allows for rich, multi-part messages. Here's the basic structure:
3129

32-
Basic message structure:
3330
```json
34-
{"role": "user", "type": "message", "start": true}
31+
{"role": "user", "start": true}
3532
{"role": "user", "type": "message", "content": "Your message here"}
36-
{"role": "user", "type": "message", "end": true}
33+
{"role": "user", "end": true}
3734
```
3835

36+
### Multi-part Messages
37+
You can send complex messages with multiple components:
38+
39+
1. Start with `{"role": "user", "start": true}`
40+
2. Add various types of content (message, file, image, etc.)
41+
3. End with `{"role": "user", "end": true}`
42+
43+
### Content Types
44+
You can include various types of content in your messages:
45+
46+
- Text messages: `{"role": "user", "type": "message", "content": "Your text here"}`
47+
- File paths: `{"role": "user", "type": "file", "content": "path/to/file"}`
48+
- Images: `{"role": "user", "type": "image", "format": "path", "content": "path/to/photo"}`
49+
- Audio: `{"role": "user", "type": "audio", "format": "wav", "content": "path/to/audio.wav"}`
50+
3951
### Control Commands
4052
To control the server's behavior, send the following commands:
4153

@@ -51,7 +63,7 @@ To control the server's behavior, send the following commands:
5163
```
5264
This executes a generated code block and allows the agent to proceed.
5365

54-
**Important**: If `auto_run` is set to `False`, the agent will pause after generating code blocks. You must send the "go" command to continue execution.
66+
**Note**: If `auto_run` is set to `False`, the agent will pause after generating code blocks. You must send the "go" command to continue execution.
5567

5668
### Completion Status
5769
The server indicates completion with the following message:
@@ -67,8 +79,46 @@ If an error occurs, the server will send an error message in the following forma
6779
```
6880
Your client should be prepared to handle these error messages appropriately.
6981

70-
### Example WebSocket Interaction
71-
Here's a simple example demonstrating the WebSocket interaction:
82+
## Code Execution Review
83+
84+
After code blocks are executed, you'll receive a review message:
85+
86+
```json
87+
{
88+
"role": "assistant",
89+
"type": "review",
90+
"content": "Review of the executed code, including safety assessment and potential irreversible actions."
91+
}
92+
```
93+
94+
This review provides important information about the safety and potential impact of the executed code. Pay close attention to these messages, especially when dealing with operations that might have significant effects on your system.
95+
96+
The `content` field of the review message may have two possible formats:
97+
98+
1. If the code is deemed completely safe, the content will be exactly `"<SAFE>"`.
99+
2. Otherwise, it will contain an explanation of why the code might be unsafe or have irreversible effects.
100+
101+
Example of a safe code review:
102+
```json
103+
{
104+
"role": "assistant",
105+
"type": "review",
106+
"content": "<SAFE>"
107+
}
108+
```
109+
110+
Example of a potentially unsafe code review:
111+
```json
112+
{
113+
"role": "assistant",
114+
"type": "review",
115+
"content": "This code performs file deletion operations which are irreversible. Please review carefully before proceeding."
116+
}
117+
```
118+
119+
## Example WebSocket Interaction
120+
121+
Here's an example demonstrating the WebSocket interaction:
72122

73123
```python
74124
import websockets
@@ -77,21 +127,25 @@ import asyncio
77127

78128
async def websocket_interaction():
79129
async with websockets.connect("ws://localhost:8000/") as websocket:
80-
# Send a message
81-
await websocket.send(json.dumps({"role": "user", "type": "message", "start": True}))
82-
await websocket.send(json.dumps({"role": "user", "type": "message", "content": "What's 2 + 2?"}))
83-
await websocket.send(json.dumps({"role": "user", "type": "message", "end": True}))
130+
# Send a multi-part user message
131+
await websocket.send(json.dumps({"role": "user", "start": True}))
132+
await websocket.send(json.dumps({"role": "user", "type": "message", "content": "Analyze this image:"}))
133+
await websocket.send(json.dumps({"role": "user", "type": "image", "format": "path", "content": "path/to/image.jpg"}))
134+
await websocket.send(json.dumps({"role": "user", "end": True}))
84135

85136
# Receive and process messages
86137
while True:
87138
message = await websocket.recv()
88139
data = json.loads(message)
89140

90141
if data.get("type") == "message":
91-
print(data.get("content", ""), end="", flush=True)
142+
print(f"Assistant: {data.get('content', '')}")
143+
elif data.get("type") == "review":
144+
print(f"Code Review: {data.get('content')}")
92145
elif data.get("type") == "error":
93146
print(f"Error: {data.get('content')}")
94147
elif data == {"role": "assistant", "type": "status", "content": "complete"}:
148+
print("Interaction complete")
95149
break
96150

97151
asyncio.run(websocket_interaction())
@@ -100,7 +154,7 @@ asyncio.run(websocket_interaction())
100154
## HTTP API
101155

102156
### Modifying Settings
103-
To change server settings, send a POST request to `http://localhost:8000/settings`. The payload should conform to [the interpreter object's settings](https://docs.openinterpreter.com/settings/all-settings).
157+
To change server settings, send a POST request to `http://localhost:8000/settings`. The payload should conform to the interpreter object's settings.
104158

105159
Example:
106160
```python
@@ -122,9 +176,56 @@ Example:
122176
```python
123177
response = requests.get("http://localhost:8000/settings/custom_instructions")
124178
print(response.json())
125-
# Output: {"custom_instructions": "You only write react."}
179+
# Output: {"custom_instructions": "You only write Python code."}
180+
```
181+
182+
## OpenAI-Compatible Endpoint
183+
184+
The server provides an OpenAI-compatible endpoint at `/openai`. This allows you to use the server with any tool or library that's designed to work with the OpenAI API.
185+
186+
### Chat Completions Endpoint
187+
188+
The chat completions endpoint is available at:
189+
190+
```
191+
[server_url]/openai/chat/completions
192+
```
193+
194+
To use this endpoint, set the `api_base` in your OpenAI client or configuration to `[server_url]/openai`. For example:
195+
196+
```python
197+
import openai
198+
199+
openai.api_base = "http://localhost:8000/openai" # Replace with your server URL if different
200+
openai.api_key = "dummy" # The key is not used but required by the OpenAI library
201+
202+
response = openai.ChatCompletion.create(
203+
model="gpt-3.5-turbo", # This model name is ignored, but required
204+
messages=[
205+
{"role": "system", "content": "You are a helpful assistant."},
206+
{"role": "user", "content": "What's the capital of France?"}
207+
]
208+
)
209+
210+
print(response.choices[0].message['content'])
126211
```
127212

213+
Note that only the chat completions endpoint (`/chat/completions`) is implemented. Other OpenAI API endpoints are not available.
214+
215+
When using this endpoint:
216+
- The `model` parameter is required but ignored.
217+
- The `api_key` is required by the OpenAI library but not used by the server.
218+
219+
## Best Practices
220+
221+
1. Always handle the "complete" status message to ensure your client knows when the server has finished processing.
222+
2. If `auto_run` is set to `False`, remember to send the "go" command to execute code blocks and continue the interaction.
223+
3. Implement proper error handling in your client to manage potential connection issues, unexpected server responses, or server-sent error messages.
224+
4. Use the AsyncInterpreter class when working with the server in Python to ensure compatibility with asynchronous operations.
225+
5. Pay attention to the code execution review messages for important safety and operational information.
226+
6. Utilize the multi-part user message structure for complex inputs, including file paths and images.
227+
7. When sending file paths or image paths, ensure they are accessible to the server.
228+
128229
## Advanced Usage: Accessing the FastAPI App Directly
129230

130231
The FastAPI app is exposed at `async_interpreter.server.app`. This allows you to add custom routes or host the app using Uvicorn directly.
@@ -147,25 +248,4 @@ if __name__ == "__main__":
147248
uvicorn.run(app, host="0.0.0.0", port=8000)
148249
```
149250

150-
## Using Docker
151-
152-
You can also run the server using Docker. First, build the Docker image from the root of the repository:
153-
154-
```bash
155-
docker build -t open-interpreter .
156-
```
157-
158-
Then, run the container:
159-
160-
```bash
161-
docker run -p 8000:8000 open-interpreter
162-
```
163-
164-
This will expose the server on port 8000 of your host machine.
165-
166-
## Best Practices
167-
1. Always handle the "complete" status message to ensure your client knows when the server has finished processing.
168-
2. If `auto_run` is set to `False`, remember to send the "go" command to execute code blocks and continue the interaction.
169-
3. Implement proper error handling in your client to manage potential connection issues, unexpected server responses, or server-sent error messages.
170-
4. Use the AsyncInterpreter class when working with the server in Python to ensure compatibility with asynchronous operations.
171-
5. When deploying in production, consider using the Docker container for easier setup and consistent environment across different machines.
251+
This guide covers all aspects of using the server, including the WebSocket API, HTTP API, OpenAI-compatible endpoint, code execution review, and various features. It provides clear explanations and examples for users to understand how to interact with the server effectively.

interpreter/core/async_core.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,11 @@ def accumulate(self, chunk):
137137
# We don't do anything with these.
138138
pass
139139

140-
elif "start" in chunk:
140+
elif (
141+
"start" in chunk
142+
or chunk["type"] != self.messages[-1]["type"]
143+
or chunk.get("format") != self.messages[-1].get("format")
144+
):
141145
chunk_copy = (
142146
chunk.copy()
143147
) # So we don't modify the original chunk, which feels wrong.
@@ -277,10 +281,14 @@ async def receive_input():
277281
try:
278282
data = await websocket.receive()
279283

280-
print("Received:", data)
284+
if False:
285+
print("Received:", data)
281286

282-
if data.get("type") == "websocket.receive" and "text" in data:
283-
data = json.loads(data["text"])
287+
if data.get("type") == "websocket.receive":
288+
if "text" in data:
289+
data = json.loads(data["text"])
290+
elif "bytes" in data:
291+
data = data["bytes"]
284292
await async_interpreter.input(data)
285293
elif data.get("type") == "websocket.disconnect":
286294
print("Disconnecting.")
@@ -363,12 +371,8 @@ async def send_output():
363371
# TODO
364372
@router.post("/")
365373
async def post_input(payload: Dict[str, Any]):
366-
# This doesn't work, but something like this should exist
367-
query = payload.get("query")
368-
if not query:
369-
return {"error": "Query is required."}, 400
370374
try:
371-
async_interpreter.input.put(query)
375+
async_interpreter.input(payload)
372376
return {"status": "success"}
373377
except Exception as e:
374378
return {"error": str(e)}, 500

interpreter/core/core.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,11 @@ def _respond_and_store(self):
303303

304304
# Utility function
305305
def is_active_line_chunk(chunk):
306-
return "format" in chunk and chunk["format"] == "active_line"
306+
if "format" in chunk and chunk["format"] == "active_line":
307+
return True
308+
if chunk["type"] == "review":
309+
return True
310+
return False
307311

308312
last_flag_base = None
309313

interpreter/core/llm/llm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ def load(self):
306306
if self._is_loaded:
307307
return
308308

309+
self._is_loaded = True
310+
309311
if self.model.startswith("ollama/"):
310312
model_name = self.model.replace("ollama/", "")
311313
try:
@@ -371,8 +373,6 @@ def load(self):
371373
except:
372374
pass
373375

374-
self._is_loaded = True
375-
376376

377377
def fixed_litellm_completions(**params):
378378
"""

interpreter/core/llm/run_function_calling_llm.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,30 @@ def run_function_calling_llm(llm, request_params):
4040
accumulated_deltas = {}
4141
language = None
4242
code = ""
43+
function_call_detected = False
4344

4445
for chunk in llm.completions(**request_params):
4546
if "choices" not in chunk or len(chunk["choices"]) == 0:
4647
# This happens sometimes
4748
continue
4849

4950
delta = chunk["choices"][0]["delta"]
50-
5151
# Accumulate deltas
5252
accumulated_deltas = merge_deltas(accumulated_deltas, delta)
5353

5454
if "content" in delta and delta["content"]:
55-
yield {"type": "message", "content": delta["content"]}
55+
if function_call_detected:
56+
# More content after a code block? This is a code review by a judge layer.
57+
yield {"type": "review", "content": delta["content"]}
58+
else:
59+
yield {"type": "message", "content": delta["content"]}
5660

5761
if (
5862
accumulated_deltas.get("function_call")
5963
and "arguments" in accumulated_deltas["function_call"]
6064
and accumulated_deltas["function_call"]["arguments"]
6165
):
66+
function_call_detected = True
6267
if (
6368
"name" in accumulated_deltas["function_call"]
6469
and accumulated_deltas["function_call"]["name"] == "execute"

interpreter/core/respond.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import os
33
import re
4+
import time
45
import traceback
56

67
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
@@ -181,8 +182,8 @@ def respond(interpreter):
181182

182183
if code.replace("\n", "").replace(" ", "").startswith("{language:"):
183184
try:
184-
code = code.replace("language: ", "'language': ").replace(
185-
"code: ", "'code': "
185+
code = code.replace("language: ", '"language": ').replace(
186+
"code: ", '"code": '
186187
)
187188
code_dict = json.loads(code)
188189
if set(code_dict.keys()) == {"language", "code"}:
@@ -197,9 +198,19 @@ def respond(interpreter):
197198
except:
198199
pass
199200

200-
if language == "text" or language == "markdown":
201+
if (
202+
language == "text"
203+
or language == "markdown"
204+
or language == "plaintext"
205+
):
201206
# It does this sometimes just to take notes. Let it, it's useful.
202207
# In the future we should probably not detect this behavior as code at all.
208+
real_content = interpreter.messages[-1]["content"]
209+
interpreter.messages[-1] = {
210+
"role": "assistant",
211+
"type": "message",
212+
"content": f"```\n{real_content}\n```",
213+
}
203214
continue
204215

205216
# Is this language enabled/supported?

interpreter/terminal_interface/terminal_interface.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ def terminal_interface(interpreter, message):
243243
if "content" in chunk:
244244
active_block.code += chunk["content"]
245245

246+
if chunk["type"] == "review" and chunk.get("content"):
247+
# Specialized models can emit a code review.
248+
print(chunk.get("content"), end="", flush=True)
249+
246250
# Execution notice
247251
if chunk["type"] == "confirmation":
248252
if not interpreter.auto_run:

interpreter/terminal_interface/validate_llm_settings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ def validate_llm_settings(interpreter):
7272
"""
7373
7474
**Tip:** To save this key for later, run one of the following and then restart your terminal.
75-
MacOS: `echo '\\nexport OPENAI_API_KEY=your_api_key' >> ~/.zshrc`
76-
Linux: `echo '\\nexport OPENAI_API_KEY=your_api_key' >> ~/.bashrc`
75+
MacOS: `echo 'export OPENAI_API_KEY=your_api_key' >> ~/.zshrc`
76+
Linux: `echo 'export OPENAI_API_KEY=your_api_key' >> ~/.bashrc`
7777
Windows: `setx OPENAI_API_KEY your_api_key`
7878
7979
---"""

0 commit comments

Comments
 (0)