Skip to content

Commit 7dd9309

Browse files
authored
Merge pull request #10 from thedadams/event-streaming
feat: add event streaming
2 parents 98c1fcd + ef41741 commit 7dd9309

File tree

6 files changed

+178
-2
lines changed

6 files changed

+178
-2
lines changed

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,47 @@ print_output(out, err)
181181
wait()
182182
```
183183

184+
### `stream_exec_with_events(tool, opts)`
185+
186+
This function streams the execution of a tool and returns the output, error, event stream, and process wait function. The streams must be read from.
187+
188+
```python
189+
from gptscript.command import stream_exec_with_events
190+
from gptscript.tool import Tool
191+
192+
tool = Tool(
193+
json_response=True,
194+
instructions="""
195+
Create three short graphic artist descriptions and their muses.
196+
These should be descriptive and explain their point of view.
197+
Also come up with a made up name, they each should be from different
198+
backgrounds and approach art differently.
199+
the response should be in JSON and match the format:
200+
{
201+
artists: [{
202+
name: "name"
203+
description: "description"
204+
}]
205+
}
206+
""",
207+
)
208+
209+
def print_output(out, err, events):
210+
for event in events:
211+
print(event)
212+
213+
# Error stream has the debug info that is useful to see
214+
for line in err:
215+
print(line)
216+
217+
for line in out:
218+
print(line)
219+
220+
out, err, events, wait = stream_exec_with_events(tool)
221+
print_output(out, err, events)
222+
wait()
223+
```
224+
184225
### `stream_exec_file(tool_path, input="",opts)`
185226

186227
This function streams the execution of a tool from a file and returns the output, error, and process wait function. The input values are passed to the tool as args.
@@ -201,6 +242,29 @@ print_output(out, err)
201242
wait()
202243
```
203244

245+
### `stream_exec_file_with_events(tool_path, input="",opts)`
246+
247+
This function streams the execution of a tool from a file and returns the output, error, event stream, and process wait function. The input values are passed to the tool as args.
248+
249+
```python
250+
from gptscript.command import stream_exec_file_with_events
251+
252+
def print_output(out, err, events):
253+
for event in events:
254+
print(event)
255+
256+
# Error stream has the debug info that is useful to see
257+
for line in err:
258+
print(line)
259+
260+
for line in out:
261+
print(line)
262+
263+
out, err, events, wait = stream_exec_file_with_events("./init.gpt")
264+
print_output(out, err, events)
265+
wait()
266+
```
267+
204268
## Example Usage
205269

206270
```python

gptscript/command.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
import sys
33

4-
from gptscript.exec_utils import exec_cmd, stream_exec_cmd
4+
from gptscript.exec_utils import exec_cmd, stream_exec_cmd, stream_exec_cmd_with_events
55
from gptscript.tool import FreeForm, Tool
66

77
optToArg = {
@@ -57,6 +57,18 @@ def stream_exec(tool, opts={}):
5757
raise e
5858

5959

60+
def stream_exec_with_events(tool, opts={}):
61+
cmd = _get_command()
62+
args = toArgs(opts)
63+
args.append("-")
64+
try:
65+
tool_str = process_tools(tool)
66+
process, events = stream_exec_cmd_with_events(cmd, args, input=tool_str)
67+
return process.stdout, process.stderr, events, process.wait
68+
except Exception as e:
69+
raise e
70+
71+
6072
def exec_file(tool_path, input="", opts={}):
6173
cmd = _get_command()
6274
args = toArgs(opts)
@@ -88,6 +100,22 @@ def stream_exec_file(tool_path, input="", opts={}):
88100
raise e
89101

90102

103+
def stream_exec_file_with_events(tool_path, input="", opts={}):
104+
cmd = _get_command()
105+
args = toArgs(opts)
106+
107+
args.append(tool_path)
108+
109+
if input != "":
110+
args.append(input)
111+
try:
112+
process, events = stream_exec_cmd_with_events(cmd, args)
113+
114+
return process.stdout, process.stderr, events, process.wait
115+
except Exception as e:
116+
raise e
117+
118+
91119
def toArgs(opts):
92120
args = ["--quiet=false"]
93121
for opt, val in opts.items():

gptscript/exec_utils.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import subprocess
2+
import os
3+
import sys
4+
5+
if sys.platform == "win32":
6+
import msvcrt
7+
import win32api
8+
import win32con
29

310

411
def exec_cmd(cmd, args=[], input=None):
@@ -21,13 +28,55 @@ def exec_cmd(cmd, args=[], input=None):
2128

2229

2330
def stream_exec_cmd(cmd, args=[], input=None):
31+
return _stream_cmd(cmd, args, input)
32+
33+
34+
def stream_exec_cmd_with_events(cmd, args=[], input=None):
35+
events_out, events_in = os.pipe()
36+
close_fds = True
37+
fds = (events_in,)
38+
39+
if sys.platform == "win32":
40+
# Can't close fds on Windows
41+
close_fds = False
42+
# Can't pass fds on Windows
43+
fds = tuple()
44+
45+
# Convert the writable file descriptor to a handle
46+
w_handle = msvcrt.get_osfhandle(events_in)
47+
48+
# Duplicate the handle to make it inheritable
49+
proc_handle = win32api.GetCurrentProcess()
50+
dup_handle = win32api.DuplicateHandle(
51+
proc_handle, # Source process handle
52+
w_handle, # Source handle
53+
proc_handle, # Target process handle
54+
0, # Desired access (0 defaults to same as source)
55+
1, # Inherit handle
56+
win32con.DUPLICATE_SAME_ACCESS # Options
57+
)
58+
args = ["--events-stream-to=fd://" + str(int(dup_handle))] + args
59+
else:
60+
args = ["--events-stream-to=fd://" + str(events_in)] + args
61+
62+
proc = _stream_cmd(cmd, args, input, fds=fds, close_fds=close_fds)
63+
os.close(events_in)
64+
if sys.platform == "win32":
65+
win32api.CloseHandle(dup_handle)
66+
67+
return proc, os.fdopen(events_out, "r", encoding="utf-8")
68+
69+
70+
def _stream_cmd(cmd, args=[], input=None, fds=tuple(), close_fds=True):
2471
try:
2572
process = subprocess.Popen(
2673
[cmd] + args,
2774
stdin=subprocess.PIPE,
2875
stdout=subprocess.PIPE,
2976
stderr=subprocess.PIPE,
3077
text=True,
78+
pass_fds=fds,
79+
close_fds=close_fds,
3180
encoding="utf-8",
3281
)
3382

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies = [
1616
"requests==2.31.0",
1717
"tqdm==4.66.2",
1818
"urllib3==2.2.1",
19+
"pywin32==306 ; sys_platform == 'win32'",
1920
]
2021

2122
[project.urls]

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ urllib3==2.2.1
77
tox==4.14.1
88
wheel==0.42.0
99
setuptools==69.1.1
10+
pywin32==306; sys_platform == 'win32'

tests/test_gptscript.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
list_models,
55
list_tools,
66
exec,
7-
exec_file,
87
stream_exec,
8+
stream_exec_with_events,
99
stream_exec_file,
10+
stream_exec_file_with_events,
1011
)
1112
from gptscript.tool import Tool, FreeForm
1213

@@ -115,7 +116,39 @@ def test_stream_exec_complex_tool(complex_tool):
115116
def test_stream_exec_file():
116117
out, err, wait = stream_exec_file("./fixtures/test.gpt")
117118
resp = wait() # Wait for streaming to complete
119+
for line in out:
120+
print(line)
121+
for line in err:
122+
print(line)
118123
assert (
119124
out is not None or err is not None
120125
), "Expected some output or error from stream_exec_file"
121126
assert resp == 0, "Expected a successful response from stream_exec_file"
127+
128+
129+
def test_stream_exec_tool_with_events(simple_tool):
130+
out, err, events, wait = stream_exec_with_events(simple_tool)
131+
has_events = False
132+
for line in events:
133+
has_events = line != ""
134+
135+
assert has_events, "Expected some events from stream_exec_with_events"
136+
resp = wait() # Wait for streaming to complete
137+
assert (
138+
out is not None or err is not None
139+
), "Expected some output or error from stream_exec_file"
140+
assert resp == 0, "Expected a successful response from stream_exec_file"
141+
142+
143+
def test_stream_exec_file_with_events():
144+
out, err, events, wait = stream_exec_file_with_events("./fixtures/test.gpt")
145+
has_events = False
146+
for line in events:
147+
has_events = line != ""
148+
149+
assert has_events, "Expected some events from stream_exec_file_with_events"
150+
resp = wait() # Wait for streaming to complete
151+
assert (
152+
out is not None or err is not None
153+
), "Expected some output or error from stream_exec_file"
154+
assert resp == 0, "Expected a successful response from stream_exec_file"

0 commit comments

Comments
 (0)