darsoarafa commited on
Commit
a4bc1a7
·
verified ·
1 Parent(s): 04ee41f

Upload 5 files

Browse files
Files changed (5) hide show
  1. chat_app.html +434 -0
  2. chat_app.ts +90 -0
  3. favicon.ico +0 -0
  4. pre-requirements.txt +1 -0
  5. start.py +521 -0
chat_app.html ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DS1-App</title>
7
+
8
+ <script src="//cdnjs.cloudflare.com/ajax/libs/annyang/2.6.1/annyang.min.js"></script>
9
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
10
+ <script>
11
+ "use strict";
12
+ var mode_pesanan = false;
13
+ var mode_tanya = false;
14
+ var bicara=annyang
15
+ // first we make sure annyang started succesfully
16
+ if (bicara) {
17
+ // define the functions our commands will run.
18
+ var hello = function(text) {
19
+ $("#prompt-input").slideDown("slow");
20
+ scrollTo("#prompt-input");
21
+ if (mode_tanya==false) {
22
+ $("#prompt-input").val(""+text)
23
+ mode_tanya = true
24
+ } else {
25
+ let prev= $("#prompt-input").val()
26
+ $("#prompt-input").val(prev+" "+text)
27
+ }
28
+ //console.log($("#prompt-input").val())
29
+ $("#AppTitle").val("Tanya")
30
+ };
31
+
32
+ var pesanan = function(text) {
33
+ $("#prompt-input").slideDown("slow");
34
+ scrollTo("#prompt-input");
35
+ if (mode_pesanan==false) {
36
+ $("#prompt-input").val("@ "+text)
37
+ mode_pesanan = true
38
+ } else {
39
+ let prev= $("#prompt-input").val()
40
+ $("#prompt-input").val(prev+" "+text)
41
+ }
42
+ //console.log($("#prompt-input").val())
43
+ $("#AppTitle").val("Pesanan")
44
+ };
45
+
46
+ var warna = function() {
47
+ $("#warna").slideDown("slow");
48
+ scrollTo("#section_warna");
49
+ };
50
+ var warna_minuman = function() {
51
+ $("#warna_minuman").slideDown("slow");
52
+ scrollTo("#section_warna_minuman");
53
+ };
54
+ var rasa = function() {
55
+ $("#rasa").slideDown("slow");
56
+ scrollTo("#section_rasa");
57
+ };
58
+ var aroma = function() {
59
+ $("#aroma").slideDown("slow");
60
+ scrollTo("#section_aroma");
61
+ };
62
+ var tutup = function() {
63
+ $("#hello1").hide();
64
+ $("#section_hello").hide();
65
+ $("#section_warna").hide();
66
+ $("#section_rasa").hide();
67
+ $("#section_aroma").hide();
68
+ scrollTo("#section_hello");
69
+ };
70
+
71
+ var submitButton = function() {
72
+ $("#button-input").slideDown("slow");
73
+ scrollTo("#button-input");
74
+ $("#button-input").click()
75
+ console.log("LANJUT")
76
+ mode_pesanan = false
77
+ mode_tanya = false
78
+ $("#AppTitle").val("-")
79
+ };
80
+
81
+ var getStarted = function() {
82
+ window.location.href = 'https://arafaasia.com';
83
+ }
84
+
85
+ // define our commands.
86
+ // * The key is the phrase you want your users to say.
87
+ // * The value is the action to do.
88
+ // You can pass a function, a function name (as a string), or write your function as part of the commands object.
89
+ var commands = {
90
+ 'halo *teman': hello,
91
+ 'tanya *teman': hello,
92
+ 'pesanan *teman': pesanan,
93
+ 'lanjut': submitButton,
94
+ 'masuk': submitButton,
95
+
96
+ };
97
+
98
+ // OPTIONAL: activate debug mode for detailed logging in the console
99
+ bicara.debug();
100
+ bicara.addCommands(commands);
101
+ bicara.setLanguage('id-ID'); // en
102
+ // Start listening. You can call this here, or attach this call to an event, button, etc.
103
+ bicara.start();
104
+ } else {
105
+ $(document).ready(function() {
106
+ $('#unsupported').fadeIn('fast');
107
+ });
108
+ }
109
+
110
+ var scrollTo = function(identifier, speed=1000) {
111
+ //console.log(identifier, speed)
112
+ $(identifier).show();
113
+ $('html, body').animate({
114
+ // scrollTop: $(identifier).offset().top
115
+ }, speed || 1000);
116
+ }
117
+ </script>
118
+
119
+ <style>
120
+ .table {
121
+ display:table;
122
+
123
+ height:100%;
124
+ border:1px solid #000;
125
+ }
126
+ .row2 {
127
+ display:table-row;
128
+ height:100%;
129
+ }
130
+ .cell1, .cell2, .cell3 {
131
+ display:table-cell;
132
+ width:33%;
133
+ height:auto;
134
+ border:1px solid #CCC;
135
+ }
136
+ #section_warna {
137
+ background-color: #ADAD03;
138
+ color: #fff;
139
+ }
140
+ #section_rasa {
141
+ background-color: #03AD77;
142
+ color: #fff;
143
+ }
144
+ #section_aroma {
145
+ background-color: #6503AD;
146
+ color: #fff;
147
+ }
148
+ </style>
149
+ <style>
150
+ figure {
151
+ display: flex inline;
152
+ justify-content: space-between;
153
+ border: 1px #cccccc solid;
154
+ padding: 4px;
155
+ margin: auto;
156
+ }
157
+
158
+ figcaption {
159
+ background-color: #ADAD03;
160
+ color: white;
161
+ font-style: italic;
162
+ padding: 0px;
163
+ text-align: center;
164
+ font-size: 14px;
165
+ }
166
+
167
+
168
+ .container {
169
+ background: white;
170
+ margin: 0 auto;
171
+ padding: 5%;
172
+ width: 90%;
173
+ }
174
+ .img_ds1 {
175
+ width: 80%;
176
+ height: auto
177
+ horizontal-align: center;
178
+ }
179
+ .img_ds {
180
+ width: 100%;
181
+ height: 100%; // auto
182
+ vertical-align: middle;
183
+ }
184
+
185
+ .pics_in_a_row {
186
+ display: flex;
187
+ }
188
+
189
+ .img1 { flex: 1.5; }
190
+ .img2 { flex: 1.5; }
191
+ .img3 { flex: 1.5; }
192
+
193
+ .pics_in_a_row {
194
+ margin: 25px 0;
195
+ }
196
+
197
+ .pics_in_a_row > div:not(:last-child) {
198
+ margin-right: 2%;
199
+ }
200
+
201
+ </style>
202
+
203
+
204
+
205
+
206
+
207
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
208
+ <style>
209
+ main {
210
+ max-width: 700px;
211
+ }
212
+ #conversation .user::before {
213
+ content: 'Anda: ';
214
+ font-weight: bold;
215
+ display: block;
216
+ }
217
+ #conversation .model::before {
218
+ content: 'AI: ';
219
+ font-weight: bold;
220
+ display: block;
221
+ }
222
+ #spinner {
223
+ opacity: 0;
224
+ transition: opacity 500ms ease-in;
225
+ width: 30px;
226
+ height: 30px;
227
+ border: 3px solid #222;
228
+ border-bottom-color: transparent;
229
+ border-radius: 50%;
230
+ animation: rotation 1s linear infinite;
231
+ }
232
+ @keyframes rotation {
233
+ 0% { transform: rotate(0deg); }
234
+ 100% { transform: rotate(360deg); }
235
+ }
236
+ #spinner.active {
237
+ opacity: 1;
238
+ }
239
+ </style>
240
+
241
+
242
+
243
+
244
+ <style>
245
+ {box-sizing: border-box;}
246
+
247
+ /* Button used to open the contact form - fixed at the bottom of the page */
248
+ .open-button {
249
+ background-color: #555;
250
+ color: white;
251
+ padding: 10px 10px;
252
+ border: none;
253
+ cursor: pointer;
254
+ opacity: 0.8;
255
+ position: fixed;
256
+ bottom: 23px;
257
+ right: 30%;
258
+ width: 280px;
259
+ }
260
+
261
+ /* The popup form - hidden by default */
262
+ .form-popup {
263
+ display: none;
264
+ position: fixed;
265
+ bottom: 0;
266
+ right: 30%;
267
+ border: 3px solid #f1f1f1;
268
+ z-index: 9;
269
+ }
270
+
271
+ /* Add styles to the form container */
272
+ .form-container {
273
+ max-width: 300px;
274
+ padding: 10px;
275
+ background-color: white;
276
+ font-size: 12px;
277
+ }
278
+
279
+ /* Full-width input fields */
280
+ .form-container input[type=text], .form-container input[type=password] {
281
+ width: 100%;
282
+ padding: 2px;
283
+ margin: 1px 0 2px 0;
284
+ border: none;
285
+ background: #f1f1f1;
286
+ }
287
+
288
+ /* When the inputs get focus, do something */
289
+ .form-container input[type=text]:focus, .form-container input[type=password]:focus {
290
+ background-color: #ddd;
291
+ outline: none;
292
+ }
293
+
294
+ /* Set a style for the submit/login button */
295
+ .form-container .btn {
296
+ background-color: #04AA6D;
297
+ color: white;
298
+ padding: 10px 10px;
299
+ border: none;
300
+ cursor: pointer;
301
+ width: 100%;
302
+ margin-bottom:10px;
303
+ opacity: 0.8;
304
+ }
305
+
306
+ /* Add a red background color to the cancel button */
307
+ .form-container .cancel {
308
+ background-color: red;
309
+ }
310
+ .form-container .umum {
311
+ background-color: grey;
312
+ }
313
+ /* Add some hover effects to buttons */
314
+ .form-container .btn:hover, .open-button:hover {
315
+ opacity: 1;
316
+ }
317
+ </style>
318
+ <script>
319
+ function openForm() {
320
+ document.getElementById("myForm").style.display = "block";
321
+ }
322
+
323
+ function closeForm() {
324
+ document.getElementById("myForm").style.display = "none";
325
+ }
326
+ function closeAiForm(id) {
327
+ document.getElementById("myaiForm"+id).style.display = "none";
328
+ document.getElementById("myaiForm2"+id).style.display = "block";
329
+ }
330
+ function openAiForm(id) {
331
+ document.getElementById("myaiForm"+id).style.display = "block";
332
+ document.getElementById("myaiForm2"+id).style.display = "none";
333
+ }
334
+ function abcde(id) {
335
+ alert("abcde:"+id);
336
+ }
337
+ </script>
338
+ </head>
339
+ <body>
340
+ <main class="border rounded mx-auto my-5 p-4">
341
+ <h1 id="AppTitle">Tanya Pesanan</h1>
342
+ <p>Ucapkan: pesanan * | tanya * (masuk/lanjut)</p>
343
+ <div id="conversation" class="px-2"></div>
344
+ <div class="d-flex justify-content-center mb-3">
345
+ <div id="spinner"></div>
346
+ </div>
347
+ <form method="post">
348
+ <input id="prompt-input" name="prompt" class="form-control"/>
349
+ <div class="d-flex justify-content-end">
350
+ <button id="button-input" class="btn btn-primary mt-2">Send</button>
351
+ </div>
352
+ </form>
353
+ <div id="error" class="d-none text-danger">
354
+ Erro occurred, check the browser developer console for more information.
355
+ </div>
356
+ </main>
357
+
358
+ <!--
359
+ <button class="open-button" onclick="openForm()">Open Form</button>
360
+
361
+ <div class="form-popup" id="myForm">
362
+ <form action="" class="form-container">
363
+ <h3>Pesanan</h3>
364
+
365
+
366
+
367
+ <table>
368
+ <tr>
369
+ <td><label for="ds_salesOrderId"><b>SO#</b></label><input type="text" placeholder="SO#" name="ds_salesOrderId" required></td>
370
+ <td><label for="ds_salesDate"><b>Date</b></label><input type="text" placeholder="Date" name="ds_salesDate" required></td>
371
+ </tr>
372
+ </table>
373
+
374
+ <label for="ds_customerName"><b></b></label><input type="text" placeholder="Cust" name="ds_customerName" required>
375
+ <label for="ds_customerAddress"><b></b></label><input type="text" placeholder="Addr" name="ds_customerAddress" required>
376
+
377
+ <table>
378
+ <tr><th>Prod</th><th>Qty</th><th>Prc</th><th>Rp</th></tr>
379
+ <tr>
380
+ <td><input type="text" placeholder="-" name="ds_productName1"></td>
381
+ <td><input type="text" placeholder="0" name="ds_quantity1"></td>
382
+ <td><input type="text" placeholder="0" name="ds_unitPrice1"></td>
383
+ <td><input type="text" placeholder="0" name="ds_itemAmt1"></td>
384
+ </tr>
385
+ <tr>
386
+ <td><input type="text" placeholder="-" name="ds_productName2"></td>
387
+ <td><input type="text" placeholder="0" name="ds_quantity2"></td>
388
+ <td><input type="text" placeholder="0" name="ds_unitPrice2"></td>
389
+ <td><input type="text" placeholder="0" name="ds_itemAmt2"></td>
390
+ </tr>
391
+ <tr>
392
+ <td><input type="text" placeholder="-" name="ds_productName3"></td>
393
+ <td><input type="text" placeholder="0" name="ds_quantity3"></td>
394
+ <td><input type="text" placeholder="0" name="ds_unitPrice3"></td>
395
+ <td><input type="text" placeholder="0" name="ds_itemAmt3"></td>
396
+ </tr>
397
+ </table>
398
+
399
+ <table>
400
+ <tr>
401
+ <td><label for="ds_shippingCost"><b>Ongkir</b></label><input type="text" placeholder="Ongkir" name="ds_shippingCost" required></td>
402
+ <td><label for="ds_totalAmount"><b>Total</b></label><input type="text" placeholder="Total" name="ds_totalAmount" required></td>
403
+ </tr>
404
+ </table>
405
+
406
+
407
+ <button type="submit" class="btn">Submit</button>
408
+ <button type="button" class="btn cancel" onclick="closeForm()">Close</button>
409
+ </form>
410
+ </div>
411
+ -->
412
+ </body>
413
+ </html>
414
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/5.6.3/typescript.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
415
+ <script type="module">
416
+ // to let me write TypeScript, without adding the burden of npm we do a dirty, non-production-ready hack
417
+ // and transpile the TypeScript code in the browser
418
+ // this is (arguably) A neat demo trick, but not suitable for production!
419
+ async function loadTs() {
420
+ const response = await fetch('/chat_app.ts');
421
+ const tsCode = await response.text();
422
+ const jsCode = window.ts.transpile(tsCode, { target: "es2015" });
423
+ let script = document.createElement('script');
424
+ script.type = 'module';
425
+ script.text = jsCode;
426
+ document.body.appendChild(script);
427
+ }
428
+
429
+ loadTs().catch((e) => {
430
+ console.error(e);
431
+ document.getElementById('error').classList.remove('d-none');
432
+ document.getElementById('spinner').classList.remove('active');
433
+ });
434
+ </script>
chat_app.ts ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // BIG FAT WARNING: to avoid the complexity of npm, this typescript is compiled in the browser
2
+ // there's currently no static type checking
3
+
4
+ import { marked } from 'https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.0/lib/marked.esm.js'
5
+ const convElement = document.getElementById('conversation')
6
+
7
+ const promptInput = document.getElementById('prompt-input') as HTMLInputElement
8
+ const spinner = document.getElementById('spinner')
9
+
10
+ // stream the response and render messages as each chunk is received
11
+ // data is sent as newline-delimited JSON
12
+ async function onFetchResponse(response: Response): Promise<void> {
13
+ let text = ''
14
+ let decoder = new TextDecoder()
15
+ if (response.ok) {
16
+ const reader = response.body.getReader()
17
+ while (true) {
18
+ const {done, value} = await reader.read()
19
+ if (done) {
20
+ break
21
+ }
22
+ text += decoder.decode(value)
23
+ addMessages(text)
24
+ spinner.classList.remove('active')
25
+ }
26
+ addMessages(text)
27
+ promptInput.disabled = false
28
+ promptInput.focus()
29
+ } else {
30
+ const text = await response.text()
31
+ console.error(`Unexpected response: ${response.status}`, {response, text})
32
+ throw new Error(`Unexpected response: ${response.status}`)
33
+ }
34
+ }
35
+
36
+ // The format of messages, this matches pydantic-ai both for brevity and understanding
37
+ // in production, you might not want to keep this format all the way to the frontend
38
+ interface Message {
39
+ role: string
40
+ content: string
41
+ timestamp: string
42
+ }
43
+
44
+ // take raw response text and render messages into the `#conversation` element
45
+ // Message timestamp is assumed to be a unique identifier of a message, and is used to deduplicate
46
+ // hence you can send data about the same message multiple times, and it will be updated
47
+ // instead of creating a new message elements
48
+ function addMessages(responseText: string) {
49
+ const lines = responseText.split('\n')
50
+ const messages: Message[] = lines.filter(line => line.length > 1).map(j => JSON.parse(j))
51
+ for (const message of messages) {
52
+ // we use the timestamp as a crude element id
53
+ const {timestamp, role, content} = message
54
+ const id = `msg-${timestamp}`
55
+ let msgDiv = document.getElementById(id)
56
+ if (!msgDiv) {
57
+ msgDiv = document.createElement('div')
58
+ msgDiv.id = id
59
+ msgDiv.title = `${role} at ${timestamp}`
60
+ msgDiv.classList.add('border-top', 'pt-2', role)
61
+ convElement.appendChild(msgDiv)
62
+ }
63
+ msgDiv.innerHTML = marked.parse(content)
64
+ }
65
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
66
+ }
67
+
68
+ function onError(error: any) {
69
+ console.error(error)
70
+ //document.getElementById('error').classList.remove('d-none')
71
+ //document.getElementById('spinner').classList.remove('active')
72
+ }
73
+
74
+ async function onSubmit(e: SubmitEvent): Promise<void> {
75
+ e.preventDefault()
76
+ spinner.classList.add('active')
77
+ const body = new FormData(e.target as HTMLFormElement)
78
+
79
+ promptInput.value = ''
80
+ promptInput.disabled = true
81
+
82
+ const response = await fetch('/chat/', {method: 'POST', body})
83
+ await onFetchResponse(response)
84
+ }
85
+
86
+ // call onSubmit when the form is submitted (e.g. user clicks the send button or hits Enter)
87
+ document.querySelector('form').addEventListener('submit', (e) => onSubmit(e).catch(onError))
88
+
89
+ // load messages on page load
90
+ fetch('/chat/').then(onFetchResponse).catch(onError)
favicon.ico ADDED
pre-requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ pip==21.1.1
start.py ADDED
@@ -0,0 +1,521 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations as _annotations
2
+ import os
3
+ import asyncio
4
+ import json
5
+ import sqlite3
6
+ import datetime
7
+ import fastapi
8
+ import logfire
9
+ import time
10
+ from collections.abc import AsyncIterator
11
+ from concurrent.futures.thread import ThreadPoolExecutor
12
+ from contextlib import asynccontextmanager
13
+ from dataclasses import dataclass
14
+ from datetime import datetime, timezone, date
15
+ from functools import partial
16
+ from pathlib import Path
17
+ from typing import Annotated, Any, Callable, Literal, TypeVar
18
+
19
+ from pydantic import BaseModel, Field, ValidationError, model_validator
20
+ from typing import List, Optional, Dict
21
+
22
+ from fastapi import Depends, Request
23
+ from fastapi.responses import FileResponse, Response, StreamingResponse
24
+ from typing_extensions import LiteralString, ParamSpec, TypedDict
25
+
26
+ from pydantic_ai import Agent
27
+ from pydantic_ai.exceptions import UnexpectedModelBehavior
28
+ from pydantic_ai.messages import (
29
+ ModelMessage,
30
+ ModelMessagesTypeAdapter,
31
+ ModelRequest,
32
+ ModelResponse,
33
+ TextPart,
34
+ UserPromptPart,
35
+ )
36
+ from pydantic_ai.models.openai import OpenAIModel
37
+
38
+ model = OpenAIModel(
39
+ 'gemma-2-2b-it',
40
+ base_url='http://localhost:1234/v1',
41
+ api_key='your-local-api-key',
42
+ )
43
+ model11 = OpenAIModel(
44
+ 'mistral-7b-instruct-v0.3',
45
+ base_url='http://localhost:1234/v1',
46
+ api_key='your-local-api-key',
47
+ )
48
+
49
+ #DeepSeek Key: sk-694660c67c3947e4853019473f30240d https://api.deepseek.com
50
+ # Model Pydantic untuk Sales Order
51
+ class Item(BaseModel):
52
+ product_code: Optional[str] = None #str = Field(..., min_length=1, max_length=20)
53
+ description: str = Field(..., min_length=1, max_length=100)
54
+ quantity: int = Field(..., ge=1)
55
+ unit_price: float = Field(..., ge=0)
56
+
57
+ class SalesOrder1(BaseModel):
58
+ so_number: Optional[str] = None
59
+ customer_name: str = Field(..., min_length=1, max_length=100)
60
+ items: List[Item] = Field(..., min_items=1)
61
+ payment_terms: Optional[str] = None
62
+ shipping_address: Optional[str] = None
63
+ #order_date: datetime = None
64
+
65
+ # 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured
66
+ logfire.configure(send_to_logfire='if-token-present')
67
+
68
+ class product(BaseModel):
69
+ ds_productCode: Optional[str] = "" #str = Field(..., min_length=1, max_length=20)
70
+ ds_productName: str = Field(..., min_length=1, max_length=100)
71
+ ds_quantity: int = Field(..., ge=1)
72
+ ds_unitPrice: float = Field(..., ge=0)
73
+ ds_itemAmt: Optional[float] = 0
74
+
75
+ class SalesOrder(BaseModel):
76
+ ds_salesOrderId: Optional[str] = ""
77
+ ds_salesDate: Optional[str] = ""
78
+ ds_customerName: str = Field(..., min_length=1, max_length=100)
79
+ ds_customerAddress: Optional[str] = ""
80
+ ds_items: List[product] = Field(..., min_items=1)
81
+ ds_shippingCost:Optional[float] = ""
82
+ ds_shippingAddress: Optional[str] = ""
83
+ ds_totalAmount:Optional[float] = ""
84
+ #ds_paymentTerms: Optional[str] = ""
85
+ os.environ['GEMINI_API_KEY'] = 'AIzaSyAsVIHsPIIfDBTb2K6VNdNlMt05t8x3mtE'
86
+ #agent= Agent('gemini-1.5-flash', result_type=SalesOrder)
87
+
88
+ # # Create a system prompt to guide the model
89
+ SYSTEM_PROMPT = """
90
+ You are a helper that extracts SalesOrder information from text and formats it as a SalesOrder class.
91
+ """
92
+ #agent3 = Agent(model=ollama_model, result_type=PetList, retries=3, system_prompt=SYSTEM_PROMPT)
93
+ #agent3 = Agent(model=ollama_model, retries=3, system_prompt=SYSTEM_PROMPT)
94
+
95
+
96
+
97
+ agent = Agent('gemini-1.5-flash') # OK-1
98
+ #agent = Agent(model) # OK-2
99
+
100
+
101
+
102
+ #agent = Agent(model, result_type=SalesOrder) # belum bisa untuk local
103
+ #agent = Agent(model, system_prompt=SYSTEM_PROMPT) # belum bisa untuk local
104
+ #agent= Agent('gemini-1.5-flash', system_prompt=SYSTEM_PROMPT) # ERR
105
+ THIS_DIR = Path(__file__).parent
106
+
107
+
108
+ @asynccontextmanager
109
+ async def lifespan(_app: fastapi.FastAPI):
110
+ async with Database.connect() as db:
111
+ yield {'db': db}
112
+
113
+
114
+ app = fastapi.FastAPI(lifespan=lifespan)
115
+ logfire.instrument_fastapi(app)
116
+
117
+
118
+ @app.get('/')
119
+ async def index() -> FileResponse:
120
+ return FileResponse((THIS_DIR / 'chat_app.html'), media_type='text/html')
121
+
122
+
123
+ @app.get('/chat_app.ts')
124
+ async def main_ts() -> FileResponse:
125
+ """Get the raw typescript code, it's compiled in the browser, forgive me."""
126
+ return FileResponse((THIS_DIR / 'chat_app.ts'), media_type='text/plain')
127
+
128
+
129
+ async def get_db(request: Request) -> Database:
130
+ return request.state.db
131
+
132
+ @app.get('/chat/')
133
+ async def get_chat(database: Database = Depends(get_db)) -> Response:
134
+ msgs = await database.get_messages()
135
+ return Response(
136
+ b'\n'.join(json.dumps(to_chat_message(m)).encode('utf-8') for m in msgs),
137
+ media_type='text/plain',
138
+ )
139
+
140
+
141
+
142
+ class ChatMessage(TypedDict):
143
+ """Format of messages sent to the browser."""
144
+
145
+ role: Literal['user', 'model']
146
+ timestamp: str
147
+ content: str
148
+
149
+
150
+ def to_chat_message(m: ModelMessage) -> ChatMessage:
151
+ first_part = m.parts[0]
152
+ if isinstance(m, ModelRequest):
153
+ if isinstance(first_part, UserPromptPart):
154
+ return {
155
+ 'role': 'user',
156
+ 'timestamp': first_part.timestamp.isoformat(),
157
+ 'content': first_part.content,
158
+ }
159
+ elif isinstance(m, ModelResponse):
160
+ if isinstance(first_part, TextPart):
161
+ return {
162
+ 'role': 'model',
163
+ 'timestamp': m.timestamp.isoformat(),
164
+ 'content': first_part.content,
165
+ }
166
+ raise UnexpectedModelBehavior(f'Unexpected message type for chat app: {m}')
167
+
168
+ def to_ds_message(m: ModelMessage) -> SalesOrder:
169
+ first_part = m.parts[0]
170
+ if isinstance(m, ModelRequest):
171
+ if isinstance(first_part, UserPromptPart):
172
+ return {
173
+ 'role': 'user',
174
+ 'timestamp': first_part.timestamp.isoformat(),
175
+ 'content': first_part.content,
176
+ }
177
+ elif isinstance(m, ModelResponse):
178
+ if isinstance(first_part, TextPart):
179
+ return {
180
+ 'role': 'model',
181
+ 'timestamp': m.timestamp.isoformat(),
182
+ 'content': first_part.content,
183
+ }
184
+ raise UnexpectedModelBehavior(f'Unexpected message type for chat app: {m}')
185
+
186
+ @app.post('/chat/')
187
+ async def post_chat(
188
+ prompt: Annotated[str, fastapi.Form()], database: Database = Depends(get_db)
189
+ ) -> StreamingResponse:
190
+ async def stream_messages():
191
+ """Streams new line delimited JSON `Message`s to the client."""
192
+ # stream the user prompt so that can be displayed straight away
193
+ yield (
194
+ json.dumps(
195
+ {
196
+ 'role': 'user',
197
+ 'timestamp': datetime.now(tz=timezone.utc).isoformat(),
198
+ 'content': prompt,
199
+ }
200
+ ).encode('utf-8')
201
+ + b'\n'
202
+ )
203
+ # get the chat history so far to pass as context to the agent
204
+ messages = await database.get_messages()
205
+ # run the agent with the user prompt and the chat history
206
+ async with agent.run_stream(prompt, message_history=messages) as result:
207
+ async for text in result.stream(debounce_by=0.01):
208
+ # text here is a `str` and the frontend wants
209
+ # JSON encoded ModelResponse, so we create one
210
+ m = ModelResponse.from_text(content=text, timestamp=result.timestamp())
211
+ yield json.dumps(to_chat_message(m)).encode('utf-8') + b'\n'
212
+
213
+ # add new messages (e.g. the user prompt and the agent response in this case) to the database
214
+ #print("---",result.new_messages_json(),"---")
215
+ #print("***",prompt,"***")
216
+ await database.add_messages(result.new_messages_json())
217
+ async def ds_messages(prompt1):
218
+ #Nama pembeli: Bu Lurah, alamat Bekasi Barat, hari ini membeli Teh Putih dua kaleng harga 110000 per kaleng, juga membeli Teh Hitam 3 kaleng, harga per kaleng 60000. Ongkos kirim ke Bekasi Barat sebesar 36 ribu
219
+ #nama pembeli Mas Anang alamat di Jalan Cisitu no.5 Bandung, membeli Chocobar 5 batang harga 15 ribu per batang, dan membeli Rice Cracker 4 buah harga 20 ribu per buah, ongkos kirim ke Jalan Cisitu no.5 sebesar 7 ribu rupiah
220
+ try:
221
+ prompt2=f"Ekstrak data Sales Order dari teks: {prompt1}. Format output yang diinginkan hanya berupa JSON saja sesuai class SalesOrder. Tidak usah ada penjelasan lain. Sekali lagi: Output hanya JSON saja. Hari ini adalah tanggal {date.today()}. untuk Nomor Sales Order pasangkan dengan key ds_salesOrderId, untuk Tanggal Sales pasangkan dengan key ds_salesDate, untuk Nama Customer pasangkan dengan Key ds_customerName, untuk Alamat Customer pasangkan dengan key ds_customerAddress, untuk Daftar Item Barang pasangkan dengan key ds_items, untuk Kode Barang pasangkan dengan key ds_productCode, untuk Nama Barang pasangkan dengan key ds_productName, untuk Quantity pasangkan dengan key ds_quantity, untuk Unit Price pasangkan dengan key ds_unitPrice, untuk Total Nilai Per Baris Barang pasangkan dengan key ds_itemAmt, untuk Ongkos Kirim pasangkan dengan key ds_shippingCost, untuk Alamat Pengiriman pasangkan dengan key ds_shippingAddress, untuk Total Nilai Sales Order pasangkan dengan key ds_totalAmount"
222
+ #prompt2=f"Ekstrak data Sales Order dari teks: {prompt1}. Hari ini adalah tanggal {date.today()}"
223
+ yield (
224
+ json.dumps(
225
+ {
226
+ 'role': 'user',
227
+ 'timestamp': datetime.now(tz=timezone.utc).isoformat(),
228
+ 'content': prompt1,
229
+ }
230
+ ).encode('utf-8')
231
+ + b'\n'
232
+ )
233
+ messages = await database.get_messages()
234
+ async with agent.run_stream(prompt2, message_history=messages) as result:
235
+ async for text in result.stream(debounce_by=0.1):
236
+ m = ModelResponse.from_text(content=text, timestamp=result.timestamp())
237
+ yield json.dumps(to_ds_message(m)).encode('utf-8') + b'\n'
238
+
239
+ ##print(result.usage())
240
+ #await database.add_messages(result.new_messages_json())
241
+ darso = json.loads(result.new_messages_json())
242
+ darso1= darso[1]
243
+ #print("1|", darso1)
244
+ darso2= json.loads(json.dumps(darso1))
245
+ #print("2|",darso2['parts'][0])
246
+ darso3= darso2['parts'][0]
247
+ darso4= json.loads(json.dumps(darso3))
248
+ #print("4|",darso4['content'])
249
+ darso5= darso4['content']
250
+ darso5=darso5.split('```', 2)
251
+ darso5=darso5[1]
252
+ #print("5a|",darso5)
253
+ darso5=darso5.replace('json', '')
254
+ print("5|",darso5,"|")
255
+ try:
256
+ darso6= json.loads(darso5) #json
257
+ darso7= SalesOrder.model_validate(darso6)
258
+ except:
259
+ darso6= "ERR"
260
+ print("6|",darso6,"|")
261
+ if "ds_items" in darso5:
262
+ cek_str="ds_items"
263
+ else:
264
+ cek_str="--"
265
+ if darso6=="ERR":
266
+ ds_id = time.time()
267
+ ds_salesOrderId = "ERR"
268
+ ds_salesDate = 'ERR'
269
+ ds_customerName="-"
270
+ ds_customerAddress="-"
271
+
272
+ ds_productName1 = "Produk1 --- "
273
+ ds_quantity1 = 1
274
+ ds_unitPrice1 = 0
275
+ ds_itemAmt1 = 0
276
+
277
+ ds_productName2 = "Produk2 --- "
278
+ ds_quantity2 = 0
279
+ ds_unitPrice2 = 0
280
+ ds_itemAmt2 = 0
281
+
282
+ ds_productName3 = "Produk3 --- "
283
+ ds_quantity3 = 0
284
+ ds_unitPrice3 = 0
285
+ ds_itemAmt3 = 0
286
+ ds_shippingAddress=""
287
+ ds_shippingCost=0
288
+ ds_totalAmount=0
289
+ else:
290
+ ds_id = time.time()
291
+ ds_salesOrderId = "OK"
292
+ ds_salesDate = 'OK'
293
+ try:
294
+ ds_salesOrderId = darso7.ds_salesOrderId
295
+ print("7|ds_salesOrderId")
296
+ ds_salesDate = darso7.ds_salesDate
297
+ print("7|ds_salesDate")
298
+ ds_customerName=f"""{darso7.ds_customerName}"""
299
+ print("7|ds_customerName:",ds_customerName)
300
+ ds_customerAddress=f"""{darso7.ds_customerAddress}"""
301
+ print("7|ds_customerAddress:", len(darso7.ds_items))
302
+ ds_productName1 = darso7.ds_items[0].ds_productName
303
+ print("7|ds_productName1")
304
+ ds_quantity1 = darso7.ds_items[0].ds_quantity
305
+ print("7|ds_quantity1")
306
+ ds_unitPrice1 = darso7.ds_items[0].ds_unitPrice
307
+ print("7|ds_unitPrice1")
308
+ ds_itemAmt1 = darso7.ds_items[0].ds_itemAmt
309
+ print("7|ds_itemAmt1")
310
+ ds_productName2 = "-"
311
+ ds_quantity2 = 0
312
+ ds_unitPrice2 = 0
313
+ ds_itemAmt2 = 0
314
+ ds_productName3 = "-"
315
+ ds_quantity3 = 0
316
+ ds_unitPrice3 = 0
317
+ ds_itemAmt3 = 0
318
+
319
+ if len(darso7.ds_items)>1:
320
+ ds_productName2 = darso7.ds_items[1].ds_productName
321
+ ds_quantity2 = darso7.ds_items[1].ds_quantity
322
+ ds_unitPrice2 = darso7.ds_items[1].ds_unitPrice
323
+ ds_itemAmt2 = darso7.ds_items[1].ds_itemAmt
324
+ if len(darso7.ds_items)>2:
325
+ ds_productName3 = darso7.ds_items[2].ds_productName
326
+ ds_quantity3 = darso7.ds_items[2].ds_quantity
327
+ ds_unitPrice3 = darso7.ds_items[2].ds_unitPrice
328
+ ds_itemAmt3 = darso7.ds_items[2].ds_itemAmt
329
+
330
+ ds_shippingCost=darso7.ds_shippingCost
331
+ print("7|ds_shippingCost")
332
+ ds_shippingAddress=f"""{darso7.ds_shippingAddress}"""
333
+ print("7|ds_shippingAddress")
334
+ ds_totalAmount=darso7.ds_totalAmount
335
+ print("7|ds_totalAmount")
336
+ except:
337
+ ds_salesOrderId = "OK2"
338
+ ds_salesDate = 'OK2'
339
+ ds_customerName="-"
340
+ ds_customerAddress="-"
341
+
342
+ ds_productName1 = "Produk1"
343
+ ds_quantity1 = 0
344
+ ds_unitPrice1 = 0
345
+ ds_itemAmt1 = 0
346
+
347
+ ds_productName2 = "Produk2"
348
+ ds_quantity2 = 0
349
+ ds_unitPrice2 = 0
350
+ ds_itemAmt2 = 0
351
+
352
+ ds_productName3 = "Produk3"
353
+ ds_quantity3 = 0
354
+ ds_unitPrice3 = 0
355
+ ds_itemAmt3 = 0
356
+ ds_shippingAddress=""
357
+ ds_shippingCost=0
358
+ ds_totalAmount=0
359
+
360
+ formDs = f"""
361
+
362
+ <form id="myaiForm{ds_id}" action="javascript:abcde({ds_id});" class="form-container">
363
+ <h3>Pesanan</h3>
364
+ <table>
365
+ <tr>
366
+ <td><label for="ds_salesOrderId"><b>SO#</b></label><input type="text" placeholder="" name="ds_salesOrderId" value="{ds_salesOrderId}"></td>
367
+ <td><label for="ds_salesDate"><b>Date</b></label><input type="text" placeholder="" name="ds_salesDate" value="{ds_salesDate}"></td>
368
+ </tr>
369
+ <tr>
370
+ <td colspan="2"><label for="ds_customerName"><b>Customer</b></label><input type="text" placeholder="" name="ds_customerName" value="{ds_customerName}"></td>
371
+ </tr>
372
+ <tr>
373
+ <td colspan="2"><label for="ds_customerAddress"><b>Alamat</b></label><input type="text" placeholder="" name="ds_customerAddress" value="{ds_customerAddress}"></td>
374
+ </tr>
375
+ </table style="width:100%">
376
+ <b>Item Barang:</b>
377
+ <table>
378
+ <tr><th>Prod</th><th>Qty</th><th>Prc</th><th>Rp</th></tr>
379
+ <tr>
380
+ <td><input type="text" placeholder="-" name="ds_productName1" value="{ds_productName1}"></td>
381
+ <td><input type="text" placeholder="0" name="ds_quantity1" value={ds_quantity1}></td>
382
+ <td><input type="text" placeholder="0" name="ds_unitPrice1" value={ds_unitPrice1}></td>
383
+ <td><input type="text" placeholder="0" name="ds_itemAmt1" value={ds_itemAmt1}></td>
384
+ </tr>
385
+ <tr>
386
+ <td><input type="text" placeholder="-" name="ds_productName2" value="{ds_productName2}"></td>
387
+ <td><input type="text" placeholder="0" name="ds_quantity2" value={ds_quantity2}></td>
388
+ <td><input type="text" placeholder="0" name="ds_unitPrice2" value={ds_unitPrice2}></td>
389
+ <td><input type="text" placeholder="0" name="ds_itemAmt2" value={ds_itemAmt2}></td>
390
+ </tr>
391
+ <tr>
392
+ <td><input type="text" placeholder="-" name="ds_productName3" value="{ds_productName3}"></td>
393
+ <td><input type="text" placeholder="0" name="ds_quantity3" value={ds_quantity3}></td>
394
+ <td><input type="text" placeholder="0" name="ds_unitPrice3" value={ds_unitPrice3}></td>
395
+ <td><input type="text" placeholder="0" name="ds_itemAmt3" value={ds_itemAmt3}></td>
396
+ </tr>
397
+ </table>
398
+ <table>
399
+ <tr>
400
+ <td><label for="ds_shippingCost"><b>Ongkir</b></label><input type="text" placeholder="0" name="ds_shippingCost" value={ds_shippingCost}></td>
401
+ <td><label for="ds_totalAmount"><b>Total</b></label><input type="text" placeholder="0" name="ds_totalAmount" value={ds_totalAmount}></td>
402
+ </tr>
403
+ <tr>
404
+ <td colspan="2"><label for="ds_shippingAddress"><b></b></label><input type="text" placeholder="" name="ds_shippingAddress" value="{ds_shippingAddress}"></td>
405
+ </tr>
406
+ </table>
407
+ <button type="submit" class="btn">Submit</button>
408
+ <button type="button" class="btn cancel" onclick="closeAiForm({ds_id})">Close</button>
409
+ </form>
410
+ <form id="myaiForm2{ds_id}" class="form-container" style="display:none;">
411
+ <button type="button" class="btn umum" onclick="openAiForm({ds_id})">Open Form</button>
412
+ </form>
413
+ """
414
+ m = ModelResponse.from_text(content=formDs, timestamp=result.timestamp())
415
+ yield json.dumps(to_ds_message(m)).encode('utf-8') + b'\n'
416
+ print("OK")
417
+ ##print(len(items))
418
+ #darso7 = SalesOrder.model_validate(darso6)
419
+ #print("[--",darso7.ds_customerName,"--]")
420
+ #darso8 = darso7.ds_items[0]
421
+ ##, len(darso7.ds_items)
422
+ #print("[--",darso8.ds_productName,"--]")
423
+ except ValueError as e:
424
+ print(e)
425
+ if prompt[0] == "@" :
426
+ #print("@@@", prompt, "@@@")
427
+ nn = len(prompt)
428
+ prompt = prompt[1:nn]
429
+ print(">>>", prompt, "<<<")
430
+ return StreamingResponse(ds_messages(prompt), media_type='text/plain')
431
+ elif prompt[0] != "@" :
432
+ print("biasa")
433
+ return StreamingResponse(stream_messages(), media_type='text/plain')
434
+ print("** selesai **")
435
+ return StreamingResponse(stream_messages(), media_type='text/plain')
436
+
437
+ P = ParamSpec('P')
438
+ R = TypeVar('R')
439
+
440
+
441
+ @dataclass
442
+ class Database:
443
+ """Rudimentary database to store chat messages in SQLite.
444
+
445
+ The SQLite standard library package is synchronous, so we
446
+ use a thread pool executor to run queries asynchronously.
447
+ """
448
+
449
+ con: sqlite3.Connection
450
+ _loop: asyncio.AbstractEventLoop
451
+ _executor: ThreadPoolExecutor
452
+
453
+ @classmethod
454
+ @asynccontextmanager
455
+ async def connect(
456
+ cls, file: Path = THIS_DIR / '.chat_messages.sqlite'
457
+ ) -> AsyncIterator[Database]:
458
+ with logfire.span('connect to DB'):
459
+ loop = asyncio.get_event_loop()
460
+ executor = ThreadPoolExecutor(max_workers=1)
461
+ con = await loop.run_in_executor(executor, cls._connect, file)
462
+ slf = cls(con, loop, executor)
463
+ try:
464
+ yield slf
465
+ finally:
466
+ await slf._asyncify(con.close)
467
+
468
+ @staticmethod
469
+ def _connect(file: Path) -> sqlite3.Connection:
470
+ con = sqlite3.connect(str(file))
471
+ con = logfire.instrument_sqlite3(con)
472
+ cur = con.cursor()
473
+ cur.execute(
474
+ 'CREATE TABLE IF NOT EXISTS messages (id INT PRIMARY KEY, message_list TEXT);'
475
+ )
476
+ con.commit()
477
+ return con
478
+
479
+ async def add_messages(self, messages: bytes):
480
+ await self._asyncify(
481
+ self._execute,
482
+ 'INSERT INTO messages (message_list) VALUES (?);',
483
+ messages,
484
+ commit=True,
485
+ )
486
+ await self._asyncify(self.con.commit)
487
+
488
+ async def get_messages(self) -> list[ModelMessage]:
489
+ c = await self._asyncify(
490
+ self._execute, 'SELECT message_list FROM messages order by id asc'
491
+ )
492
+ rows = await self._asyncify(c.fetchall)
493
+ messages: list[ModelMessage] = []
494
+ for row in rows:
495
+ messages.extend(ModelMessagesTypeAdapter.validate_json(row[0]))
496
+ return messages
497
+
498
+ def _execute(
499
+ self, sql: LiteralString, *args: Any, commit: bool = False
500
+ ) -> sqlite3.Cursor:
501
+ cur = self.con.cursor()
502
+ cur.execute(sql, args)
503
+ if commit:
504
+ self.con.commit()
505
+ return cur
506
+
507
+ async def _asyncify(
508
+ self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
509
+ ) -> R:
510
+ return await self._loop.run_in_executor( # type: ignore
511
+ self._executor,
512
+ partial(func, **kwargs),
513
+ *args, # type: ignore
514
+ )
515
+
516
+
517
+ if __name__ == '__main__':
518
+ import uvicorn
519
+ uvicorn.run(
520
+ 'chat_app:app', reload=True, reload_dirs=[str(THIS_DIR)]
521
+ )