77import readline
88from pathlib import Path
99from typing import TYPE_CHECKING , Literal , cast
10- from typing_extensions import override
10+ from typing_extensions import Any , override
1111
1212from openai import AsyncOpenAI , pydantic_function_tool
1313from openai .lib .streaming .chat import ChatCompletionStreamState
2020from rich .console import Console
2121
2222import duron
23+ from duron import Deferred , Signal , SignalInterrupt , Stream , StreamWriter
2324from duron .codec import Codec
2425from duron .contrib .storage import FileLogStorage
2526
2627if TYPE_CHECKING :
27- from typing import Any
28-
2928 from duron .codec import JSONValue
3029 from duron .typing import TypeHint
3130
@@ -51,73 +50,83 @@ def decode_json(self, encoded: JSONValue, expected_type: TypeHint[Any]) -> objec
5150 return cast ("object" , TypeAdapter (expected_type ).validate_python (encoded ))
5251
5352
54- @duron .op
55- async def do_input () -> str : # noqa: RUF029
56- try :
57- return input ("> " ) # noqa: ASYNC250
58- except EOFError :
59- os ._exit (0 )
60- except KeyboardInterrupt :
61- os ._exit (1 )
62-
63-
6453@duron .fn (codec = PydanticCodec ())
65- async def agent_fn (ctx : duron .Context ) -> None :
66- console = Console ()
54+ async def agent_fn (
55+ ctx : duron .Context ,
56+ input_ : Stream [str ] = Deferred ,
57+ signal : Signal [None ] = Deferred ,
58+ output : StreamWriter [tuple [str , str ]] = Deferred ,
59+ ) -> None :
6760 history : list [ChatCompletionMessageParam ] = [
6861 {
6962 "role" : "system" ,
7063 "content" : "You are a helpful assistant!" ,
7164 },
7265 ]
73- while True :
74- msg = await ctx .run (do_input )
75- history .append ({
76- "role" : "user" ,
77- "content" : msg ,
78- })
79- console .print ("[bold cyan] USER[/bold cyan]" , msg )
66+ async with input_ as inp :
8067 while True :
81- result = await completion (
82- ctx ,
83- messages = history ,
84- )
85- if result .choices [0 ].message .content :
86- console .print (
87- "[bold red]ASSISTANT[/bold red] " , result .choices [0 ].message .content
88- )
68+ msgs : list [str ] = [msgs async for _ , msgs in inp .next_nowait (ctx )]
69+ if not msgs :
70+ _ , m = await inp .next ()
71+ msgs = [m ]
72+
8973 history .append ({
90- "role" : "assistant" ,
91- "content" : result .choices [0 ].message .content ,
92- "tool_calls" : [
93- {
94- "id" : toolcall .id ,
95- "type" : "function" ,
96- "function" : {
97- "name" : toolcall .function .name ,
98- "arguments" : toolcall .function .arguments ,
99- },
100- }
101- for toolcall in result .choices [0 ].message .tool_calls or []
102- if toolcall .type == "function"
103- ],
74+ "role" : "user" ,
75+ "content" : "\n " .join (msgs ),
10476 })
105- if not result .choices [0 ].message .tool_calls :
106- break
107-
108- tasks : list [asyncio .Task [tuple [str , str ]]] = []
109- for tool_call in result .choices [0 ].message .tool_calls :
110- console .print ("[bold yellow] CALL[/bold yellow]" , tool_call .id )
111- console .print (tool_call .model_dump_json ())
112- tasks .append (asyncio .create_task (ctx .run (call_tool , None , tool_call )))
113- for id_ , tool_result in await asyncio .gather (* tasks ):
114- console .print ("[bold cyan] TOOL[/bold cyan]" , id_ )
115- console .print (tool_result )
116- history .append ({
117- "role" : "tool" ,
118- "tool_call_id" : id_ ,
119- "content" : tool_result ,
120- })
77+ await output .send (("user" , "\n " .join (msgs )))
78+ while True :
79+ try :
80+ async with signal :
81+ result = await completion (
82+ ctx ,
83+ messages = history ,
84+ )
85+ if result .choices [0 ].message .content :
86+ await output .send ((
87+ "assistant" ,
88+ result .choices [0 ].message .content ,
89+ ))
90+ history .append ({
91+ "role" : "assistant" ,
92+ "content" : result .choices [0 ].message .content ,
93+ "tool_calls" : [
94+ {
95+ "id" : toolcall .id ,
96+ "type" : "function" ,
97+ "function" : {
98+ "name" : toolcall .function .name ,
99+ "arguments" : toolcall .function .arguments ,
100+ },
101+ }
102+ for toolcall in result .choices [0 ].message .tool_calls
103+ or []
104+ if toolcall .type == "function"
105+ ],
106+ })
107+ if not result .choices [0 ].message .tool_calls :
108+ break
109+
110+ tasks : list [asyncio .Task [tuple [str , str ]]] = []
111+ for tool_call in result .choices [0 ].message .tool_calls :
112+ await output .send (("call" , tool_call .model_dump_json ()))
113+ tasks .append (
114+ asyncio .create_task (ctx .run (call_tool , None , tool_call ))
115+ )
116+ for id_ , tool_result in await asyncio .gather (* tasks ):
117+ await output .send (("tool" , tool_result ))
118+ history .append ({
119+ "role" : "tool" ,
120+ "tool_call_id" : id_ ,
121+ "content" : tool_result ,
122+ })
123+ except SignalInterrupt :
124+ await output .send (("assistant" , "[Interrupted]" ))
125+ history .append ({
126+ "role" : "assistant" ,
127+ "content" : "[Interrupted]" ,
128+ })
129+ break
121130
122131
123132@duron .op
@@ -149,8 +158,45 @@ async def main() -> None:
149158
150159 log_storage = FileLogStorage (Path ("logs" ) / f"{ args .session_id } .jsonl" )
151160 async with agent_fn .invoke (log_storage ) as job :
161+ input_stream : StreamWriter [str ] = job .open_stream ("input_" , "w" )
162+ signal_stream : StreamWriter [None ] = job .open_stream ("signal" , "w" )
163+ stream : Stream [tuple [str , str ]] = job .open_stream ("output" , "r" )
164+
165+ async def reader () -> None :
166+ console = Console ()
167+ async for role , result in stream :
168+ match role :
169+ case "user" :
170+ console .print ("[bold cyan] USER[/bold cyan]" , result )
171+ case "assistant" :
172+ console .print ("[bold red]ASSISTANT[/bold red] " , result )
173+ case "tool" :
174+ console .print ("[bold cyan] TOOL[/bold cyan]" , result )
175+ case "call" :
176+ console .print ("[bold yellow] CALL[/bold yellow]" , result )
177+ case _:
178+ console .print ("[bold magenta] ???[/bold magenta]" , result )
179+
180+ async def writer () -> None :
181+ try :
182+ while True :
183+ await asyncio .sleep (0 )
184+ m = await asyncio .to_thread (input , "> " )
185+ if m .strip ():
186+ if m == "!" :
187+ await signal_stream .send (None )
188+ else :
189+ await input_stream .send (m )
190+ except EOFError :
191+ os ._exit (0 )
192+ except KeyboardInterrupt :
193+ os ._exit (1 )
194+
195+ bg = [asyncio .create_task (reader ()), asyncio .create_task (writer ())]
152196 await job .start ()
153197 await job .wait ()
198+ for t in bg :
199+ await t
154200
155201
156202async def completion (
0 commit comments