-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathtext_chat.clj
More file actions
206 lines (189 loc) · 8.93 KB
/
Copy pathtext_chat.clj
File metadata and controls
206 lines (189 loc) · 8.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
(ns simulflow-examples.text-chat
{:clj-reload/no-unload true}
(:require
[clojure.core.async :as a]
[clojure.core.async.flow :as flow]
[simulflow.processors.llm-context-aggregator :as context]
[simulflow.processors.openai :as openai]
[simulflow.scenario-manager :as sm]
[simulflow.secrets :refer [secret]]
[simulflow.transport.text-in :as text-in]
[simulflow.transport.text-out :as text-out]
[simulflow.utils.core :as u]
[taoensso.telemere :as t]))
;; Set log level to reduce noise during chat
(t/set-min-level! :warn)
(defn text-chat-flow-config
"Text-based chat flow using stdin/stdout instead of voice I/O.
This example demonstrates how to interact with simulflow through text:
- User types messages instead of speaking
- LLM responses are streamed to console as they're generated
- Input is blocked while LLM is responding
- Clean prompt management handled by text-output processor
- Uses existing frame types: user-speech-start/transcription/user-speech-stop sequence
For a convenient CLI version, use: bin/chat
"
([] (text-chat-flow-config {}))
([{:keys [llm/context debug? extra-procs extra-conns]
:or {context {:messages
[{:role "system"
:content "You are a helpful AI assistant. Be concise and conversational.
You are communicating through text chat."}]
:tools
[{:type :function
:function
{:name "get_weather"
:handler (fn [{:keys [town]}]
(str "The weather in " town " is 17 degrees celsius"))
:description "Get the current weather of a location"
:parameters {:type :object
:required [:town]
:properties {:town {:type :string
:description "Town for which to retrieve the current weather"}}
:additionalProperties false}
:strict true}}
{:type :function
:function
{:name "quit_chat"
:handler (fn [_]
(println "\nGoodbye!")
(System/exit 0))
:description "Quit the chat session"
:parameters {:type :object
:properties {}
:additionalProperties false}
:strict true}}]}
debug? false}}]
{:procs
(u/deep-merge
{;; Read from stdin and emit user-speech-start/transcription/user-speech-stop sequence
:text-input {:proc text-in/text-input-process
:args {}}
;; Handle conversation context and user speech aggregation
:context-aggregator {:proc context/context-aggregator
:args {:llm/context context
:aggregator/debug? debug?}}
;; Generate LLM responses with streaming
:llm {:proc openai/openai-llm-process
:args {:openai/api-key (secret [:openai :api-key])
:llm/model :gpt-4.1-mini}}
;; Handle assistant message assembly for context
:assistant-context-assembler {:proc context/assistant-context-assembler
:args {:debug? debug?}}
;; Stream LLM output and manage prompts
:text-output {:proc text-out/text-output-process
:args {:text-out/response-prefix "Assistant: "
:text-out/response-suffix ""
:text-out/show-thinking debug?
:text-out/user-prompt "You: "
:text-out/manage-prompts true}}}
extra-procs)
:conns (concat
[;; Main conversation flow
[[:text-input :out] [:context-aggregator :in]]
[[:context-aggregator :out] [:llm :in]]
;; Stream LLM responses to output for clean formatting
[[:llm :out] [:text-output :in]]
;; Assemble assistant context for conversation history
[[:llm :out] [:assistant-context-assembler :in]]
[[:assistant-context-assembler :out] [:context-aggregator :in]]
;; System frame routing for input blocking/unblocking
;; LLM emits llm-full-response-start/end frames to :out, which are system frames
[[:llm :out] [:text-input :sys-in]]
;; System frames for lifecycle management
[[:text-input :sys-out] [:context-aggregator :sys-in]]]
extra-conns)}))
(defn scenario-example
"A scenario is a predefined, highly structured conversation. LLM performance
degrades when it has a big complex prompt to enact, so to ensure a consistent
output use scenarios that transition the LLM into a new scenario node with a clear
instruction for the current node."
[{:keys [initial-node scenario-config]}]
(let [flow (flow/create-flow
(text-chat-flow-config
{;; Don't add any context because the scenario will handle that
:llm/context {:messages []
:tools []}
:language :en
;; add gateway process for scenario to inject frames
:extra-procs {:scenario {:proc (flow/process #'sm/scenario-in-process)}}
:extra-conns [[[:scenario :sys-out] [:text-output :in]]
[[:scenario :sys-out] [:context-aggregator :sys-in]]]}))
s (when scenario-config
(sm/scenario-manager
{:flow flow
:flow-in-coord [:scenario :scenario-in] ;; scenario-manager will inject frames through this channel
:scenario-config (cond-> scenario-config
initial-node (assoc :initial-node initial-node))}))]
{:flow flow
:scenario s}))
(defn start-text-chat!
"Start a text-based chat session with the simulflow agent.
Usage:
(start-text-chat!) ; Regular chat
(start-text-chat! {:scenario? true}) ; Scenario example
Then type messages and press Enter. The assistant will respond with streaming text.
Input is blocked while the assistant is responding.
Type messages with 'quit' to exit via the quit_chat function.
CLI Alternative: Use bin/chat for a convenient command-line interface."
([] (start-text-chat! {}))
([{:keys [scenario?] :as config}]
(println "🤖 Starting Simulflow Text Chat...")
(println "💡 Type your messages and press Enter")
(println "⏸️ Input is blocked while assistant responds")
(println "🚪 Ask to 'quit chat' to exit")
(println "🔗 CLI version available at: bin/chat")
(println (apply str (repeat 50 "=")))
;; Create flow based on scenario choice
(let [{:keys [flow scenario]} (if scenario?
(scenario-example {:scenario-config scenario-example/config})
{:flow (flow/create-flow (text-chat-flow-config config)) :scenario nil})
{:keys [report-chan error-chan]} (flow/start flow)]
;; Start error/report monitoring in background
(future
(loop []
(when-let [[msg c] (a/alts!! [report-chan error-chan] :default nil)]
(when msg
(when (= c error-chan)
(println "\n❌ Error:" msg))
(recur)))))
;; Resume the flow to begin processing
(flow/resume flow)
(when scenario (sm/start scenario))
;; Return flow for manual control if needed
flow)))
(defn -main
"Main entry point for text chat demo"
[& args]
(let [debug? (some #(= % "--debug") args)
config (cond-> {}
debug? (assoc :debug? true)
(some #(= % "--scenario") args) (assoc :scenario? true))]
(when debug?
(t/set-min-level! :debug))
(start-text-chat! config)))
(comment
;; Interactive usage examples:
;; 1. Basic usage
(def chat (start-text-chat!))
;; Now type in the console:
;; You: Hello, how are you?
;; Assistant: Hello! I'm doing well, thank you for asking...
;; 2. With debug mode
(def debug-chat (start-text-chat! {:debug? true}))
;; 3. With custom system message
(def expert-chat (start-text-chat!
{:llm-context
{:messages
[{:role "system"
:content "You are a technical expert in Clojure programming. Be detailed and precise."}]}}))
;; 4. Stop the chat manually (or just ask to quit)
(flow/stop chat))
;; 5. Test the weather function
;; You: What's the weather like in Paris?
;; Assistant: I'll check the weather in Paris for you.
;; [Tool call executed]
;; The weather in Paris is 17 degrees celsius
;; 6. CLI usage (alternative to REPL)
;; From terminal: bin/chat
;; Or with debug: bin/chat --debug