Compare commits
717 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 889c02fccb | |||
| a7f71b5a8b | |||
| 535f4ce027 | |||
| bbfeff4b60 | |||
| cbc4596315 | |||
| 0c024fa899 | |||
| ddc895de4c | |||
| 378662b62f | |||
| ad3568ff3c | |||
| 7a5f545c00 | |||
| 075db9154a | |||
| 29c400bdb4 | |||
| 9e36a1b6c2 | |||
| d8949d0b6c | |||
| a4818b2afb | |||
| 2834e990bd | |||
| 21edbe7d81 | |||
| bd30ee1fab | |||
| 1099535827 | |||
| f5b9244747 | |||
| a04de221ee | |||
| 8f65d5651a | |||
| 3a1d4cde52 | |||
| d55d84655e | |||
| 6da9a9f597 | |||
| e2d84178c7 | |||
| 30fe5f3120 | |||
| 0ee4a1623d | |||
| ca4eac93ce | |||
| 133542ae2c | |||
| 3b8b7eb640 | |||
| 5596565759 | |||
| 00ef650b0e | |||
| dfc1cb2a36 | |||
| 377f245e0d | |||
| 44e6e337cd | |||
| 93258f3ae8 | |||
| a1923d0476 | |||
| 6d867260a6 | |||
| cd6a432e3f | |||
| a0a2327af6 | |||
| fa0ad2782d | |||
| 018aff20e4 | |||
| b76c902c07 | |||
| f5fe6f4aee | |||
| 940d9c849f | |||
| d0d65f8ae4 | |||
| f71d00295f | |||
| 433fb137f5 | |||
| ed7993712d | |||
| 1d0582bfc9 | |||
| 45c630f17a | |||
| 9e711d344d | |||
| 5003c89684 | |||
| 93926bfbfc | |||
| 12e9e79f25 | |||
| 5687fa1c22 | |||
| 95d6baeebb | |||
| 255f0089f6 | |||
| 3af52ce181 | |||
| 4cbfacfa95 | |||
| c2d6d6337c | |||
| cda3bb8527 | |||
| a85ffb7f76 | |||
| bbcd69dea7 | |||
| d47e8900e7 | |||
| b7ac2cbf3a | |||
| 8845d22ce9 | |||
| 9163f88ce5 | |||
| 7edba8ab24 | |||
| d8d8472460 | |||
| 7aad5d154c | |||
| b1c2ecd9d0 | |||
| fcec5b92dc | |||
| 2e2b52e69e | |||
| a70c94e837 | |||
| 97eb0454ca | |||
| 4763c53c40 | |||
| 9fbd7f5649 | |||
| 854df0d2da | |||
| 615c84c15e | |||
| ca8df46ddc | |||
| 31328aeddc | |||
| 8c9330b216 | |||
| 318b9f3ace | |||
| 01439ed29a | |||
| ec0897de2c | |||
| 650f9eaadc | |||
| 58f9642d6f | |||
| 5d117e50b9 | |||
| 310853195b | |||
| 9d191f24d1 | |||
| c38cae3c13 | |||
| 4555e8bf98 | |||
| a898a759f4 | |||
| 80e20a4d85 | |||
| 6417d3853f | |||
| 3f190723b4 | |||
| f0db6d9589 | |||
| 2fd0432701 | |||
| e0fb466c36 | |||
| 4f589f2449 | |||
| 1a5b576a7d | |||
| 366f7fc71f | |||
| b1bc9bc718 | |||
| f4bb87eba8 | |||
| 3c5efd29c4 | |||
| 5281c1b54f | |||
| 59fd8a8bf5 | |||
| c6e47df7c2 | |||
| 3289b9b7be | |||
| 3258cac2a3 | |||
|
|
a6eb90ce7d | ||
| e0630221e4 | |||
| df040d4f70 | |||
| 363dde30f0 | |||
| 5226d0be04 | |||
| 4df8f92349 | |||
| c455b4ed43 | |||
| 42eccafcac | |||
| 017201b5c9 | |||
| 460bfd6b2a | |||
| 092b3c72b2 | |||
| a6c30be5b7 | |||
| 8dffac24f9 | |||
| 46048a3251 | |||
| 4ae6b4fbbd | |||
| 03147f028d | |||
| cc1d21ade0 | |||
| 1a2a85c797 | |||
| 74a1ea581c | |||
| 3019fd0e15 | |||
| ab13f2c19f | |||
| 519e281623 | |||
| 81dbae3c7a | |||
| 10c67ae4e2 | |||
| 7018490249 | |||
| ed4b599895 | |||
| 15e7e4c7c7 | |||
| 3d1c06c1c1 | |||
| bfc03de5ef | |||
| 0ac9ba7430 | |||
| 6c0dfcb4ca | |||
| cce700e705 | |||
| c40f5e0bc0 | |||
| 44a3876db6 | |||
| 83c5066948 | |||
| 2fbefde281 | |||
| ae22b1a906 | |||
| cd1a6ea190 | |||
| 011ecef046 | |||
| 5d3d7ed18c | |||
| f64cc45d59 | |||
| 69a069701e | |||
| 0966857cc6 | |||
| f7bb6fecae | |||
| b14e43f200 | |||
| 5c37bf35b8 | |||
| 25d27be681 | |||
| 2ff6c6b1a3 | |||
| 5159355c98 | |||
| 9944d8f757 | |||
| 3d07909854 | |||
| 637064bf99 | |||
| 52b08894c6 | |||
| a00c7fd9e2 | |||
| 070e3ae591 | |||
| 0aa20fd8b8 | |||
| 2784aeda13 | |||
| 53a84734ea | |||
| 4b434b0222 | |||
| e82591cd75 | |||
| e1366fe2c7 | |||
| 2fecc5848a | |||
| 0aaba4c7c0 | |||
| b70b3e0844 | |||
| 60c3bbcf9f | |||
| 78d526098a | |||
| 15decd2ab6 | |||
| f7adaa1d74 | |||
| 1912076b5b | |||
| 6e2ad4e196 | |||
| cdada47eab | |||
| e55d7d414c | |||
| 1ec196275f | |||
| bd312f0979 | |||
| 9730fff7f4 | |||
| 6590f9eb4c | |||
| 3d8ee26a23 | |||
| e90ed3c14f | |||
| eea1c51ce8 | |||
| bfee917049 | |||
| baa0a1fd7c | |||
| 911c4281da | |||
| 063b2bf960 | |||
| 4ff5740559 | |||
| 4de5b27700 | |||
| 8bc83789df | |||
| 117fee0d30 | |||
| b40ff56b23 | |||
| 1803ed9030 | |||
| df7b47bc61 | |||
| 0dcf1eb6e7 | |||
| 5426425e3a | |||
| 9e5bc89d93 | |||
| b52776b6fd | |||
| 7459c08c1b | |||
| d1749209a0 | |||
| 7a658f7bdf | |||
| 4e5ec776a8 | |||
| c135bcd6c5 | |||
| f3d825278a | |||
| 0db28f7984 | |||
| 3332f5d07e | |||
| 190ae74625 | |||
| d5088b6978 | |||
| 6bd6a12f83 | |||
| 12b10b01cd | |||
| 91859d1807 | |||
| caeeb85c63 | |||
| bc70558961 | |||
| caacc05e63 | |||
| b28e72d4de | |||
| 49ca462431 | |||
| 5b4c27247f | |||
| b41c10d2e6 | |||
| 4c07f47f47 | |||
| 71101019ab | |||
| 157078415c | |||
| 390e028769 | |||
| 9aa834852c | |||
| dad7943c54 | |||
| c67d1b13da | |||
| 39f47da6f3 | |||
| 845e747d1c | |||
| 38545721a0 | |||
| 8de043bfef | |||
| 45ccee4b71 | |||
| fc4b021b7f | |||
| b5bead65cb | |||
| 7fc375a598 | |||
| c30b3af2de | |||
| b4148cbaf6 | |||
| 60b61b5a7e | |||
| d5c443fbdb | |||
| e765dadd34 | |||
| c6a09db0eb | |||
| 323294dd57 | |||
| 6318db3d1f | |||
| 381bd5f739 | |||
| f7e1e6b17f | |||
| 8841bacb0b | |||
| df3d76738e | |||
| bdf7875f31 | |||
| 0cf61ea676 | |||
| bfc41a22a0 | |||
| 4c5960a6be | |||
| f7c5d6d553 | |||
| 66a82f6b0b | |||
| a2175d3f7d | |||
| 2b977c4560 | |||
| d917f22042 | |||
| 3484a17ebd | |||
| 17717cf3e3 | |||
| 47ccc09992 | |||
| 9e3eccb1b9 | |||
| bf3f8cfac6 | |||
| 19ff284403 | |||
| 4941cce333 | |||
| 39502bcac3 | |||
| 973c045af6 | |||
| 332a64825d | |||
| 1efa96a837 | |||
| b340c80868 | |||
| 8fb4cfb807 | |||
| 1c5b9ef035 | |||
| 1a210d48e9 | |||
| c9bd3ff7d2 | |||
| 14f3699ab2 | |||
| 54d7f548e9 | |||
| ca03398bb3 | |||
| 9ba6ef8522 | |||
| 30b7e6649a | |||
| 5fc300ecc2 | |||
| 39db3f1004 | |||
| 90645cc899 | |||
| 202e877b34 | |||
| 7406950040 | |||
| 0ede23a4db | |||
| 86ef139a1d | |||
| ac0ff18308 | |||
| 8f2395f7ce | |||
| 3192a9a75b | |||
| 1762f8cf20 | |||
| a082f8dbf6 | |||
| 23dc8dffd9 | |||
| 8b29615fcb | |||
| 91d2ed9170 | |||
| 04b49530b6 | |||
| 3ac6386234 | |||
| 35193b1c09 | |||
| 17a19d53ea | |||
| 88dece974a | |||
| eff2eee69b | |||
| bd10045c4a | |||
| 6335b651e6 | |||
| 04fa0fb05f | |||
| 471d61176d | |||
| 8467f58d6b | |||
| 539121a165 | |||
| c014adde73 | |||
| 6645061372 | |||
| 5198805155 | |||
| 97af1d9b1a | |||
| 3bd518ff20 | |||
| 34e5e25bfb | |||
| c4c216316f | |||
| e6e7a77fa3 | |||
| 79377af49a | |||
| e87e27b2a2 | |||
| 7851c5747c | |||
| 4c75f7cd6e | |||
| 337f9970a8 | |||
| 41ba59db1b | |||
| a864366c80 | |||
| fd71442331 | |||
| 50a0487bcd | |||
| d4aa30e454 | |||
| e2c211b9c6 | |||
| 60743c5bfb | |||
| b7563d0f2b | |||
| e975e97ea3 | |||
| a8a4359e96 | |||
| e50b0b8118 | |||
| 8dbf607c03 | |||
| 04f0ac766b | |||
| 7b34e60f63 | |||
| 854d22ee79 | |||
| cfc6dec1f4 | |||
| a7dddf24d8 | |||
| a43714f82d | |||
| 9550fb8e94 | |||
| e0a784c073 | |||
| 004764a05b | |||
| 84b06202dd | |||
| af6108e988 | |||
| f476b6e476 | |||
| 8e78df00c0 | |||
| bbac26b487 | |||
| a2a786040e | |||
| 99bdb1c6f8 | |||
| 7d3e7ec111 | |||
| 36828be6ae | |||
| 5ddf7d36d7 | |||
| eed55b922a | |||
| 0b02a3d75e | |||
| 0f51d07eb1 | |||
| a340ea4f75 | |||
| 46a7a8c92e | |||
| d09b204f60 | |||
| 3d06f28954 | |||
| 4d53518087 | |||
| a89a45e177 | |||
| 2b69a4d17d | |||
| 196527c674 | |||
| 695049ff90 | |||
| aa1285ba6c | |||
| b050bbc6bf | |||
| 928f658328 | |||
| 2587fcf050 | |||
| 5477c2d6a5 | |||
| 10de872c6c | |||
| 54f05b0af0 | |||
| 21349b9fcc | |||
| a991ca4626 | |||
| f304b6f634 | |||
| f939d1c782 | |||
| 047d1e7856 | |||
| 559dd59522 | |||
| 04e7e71e2e | |||
| 8b0fe6d2ac | |||
| 3a4952087d | |||
| 96f8edc2e7 | |||
| f7ddf32fff | |||
| cc03367fb1 | |||
| 527fbfad1b | |||
| 133daabd77 | |||
| ca9c994bfe | |||
| 2363944fa6 | |||
| affa6f5ca8 | |||
| 595ac0a597 | |||
| cd218cc4a1 | |||
| 02e614d733 | |||
| 0af7efc863 | |||
| 525c4c315f | |||
| 98592efb8d | |||
| f4f6ae6a9c | |||
| fd4cf9684b | |||
| 1b571b4ed6 | |||
| 461194ee83 | |||
| 81a4062b19 | |||
| c96a09c1f1 | |||
| dc95e4c535 | |||
| 35157b052b | |||
| be508bcfb1 | |||
| e9a1acf546 | |||
| 83c6ac9a2c | |||
| 4259e6774c | |||
| 96082cf750 | |||
| 8560ea43ce | |||
| d557ffcfbe | |||
| ebc574db25 | |||
| 2284c4e7cb | |||
| 0c6277536c | |||
| d12428c267 | |||
| 10c5352739 | |||
| 68b2c85966 | |||
| 988349b67e | |||
| f87f59c194 | |||
| 78e4a80f36 | |||
| e02fb10b11 | |||
| 4f4993b605 | |||
| 48c5146e65 | |||
| e58303f971 | |||
| d2a2683ea2 | |||
| 0c64b7c137 | |||
| c3d336806f | |||
| 86ce18f181 | |||
| 30e80b98b3 | |||
| 40a95fa7a6 | |||
| 28db40609d | |||
| db5a243228 | |||
| 6419528ae1 | |||
| b9ee32e518 | |||
| e8066e29f9 | |||
| bbbd24503f | |||
| d0096e6943 | |||
| 46def6655b | |||
| 884364a934 | |||
| 9407f157bc | |||
| a5e135eca6 | |||
| 85415e2221 | |||
| a8be7730a3 | |||
| 7f64ff0d51 | |||
| e2b413917f | |||
| e7f90a266d | |||
| a7323e68cd | |||
| 8c61e7e8e4 | |||
| 4517e280fa | |||
| b515efac53 | |||
| 54d98679ff | |||
| d9ee717d13 | |||
| 675b8fefd1 | |||
| a1a53fcf07 | |||
| 3f9cb2c40f | |||
| 4561b9edcc | |||
| c7dbb4afcc | |||
| 5d175fb72e | |||
| be1b22e2f0 | |||
| a49c9973ae | |||
| 99856600c0 | |||
| 44917ceffd | |||
| f24219634f | |||
| d48b9d623f | |||
| 5157e37de8 | |||
| bf9d7da770 | |||
| 275f9e02fa | |||
| ba7885afe6 | |||
| 761c4ec259 | |||
| c65bde327c | |||
| 1b7597501a | |||
| fbe81a8298 | |||
| f3bd60e578 | |||
| 20a95224fd | |||
| 121d79d838 | |||
| b46aedcdf3 | |||
| e3e6e08449 | |||
| adfbdc395b | |||
| f0d27bbbef | |||
| de9eeffe70 | |||
| 72c73b9a99 | |||
| 98996476a8 | |||
| d9569c77fe | |||
| 38054643d4 | |||
| d9b9b8a5c1 | |||
| b9be6e512a | |||
| f79a66ddf0 | |||
| af7019f91b | |||
| 1bf4d26a8a | |||
| c57d86c27f | |||
| 9148d26496 | |||
| 61b4cb0d5c | |||
| ce7014e83a | |||
| 38fdedb6a6 | |||
| 8c99a0316e | |||
| d7cc5531b8 | |||
| 41818a202b | |||
| 91f446f5ed | |||
| f50ccabcd1 | |||
| b49e52678b | |||
| b178e039b2 | |||
| 7bb43c7613 | |||
| 2e655a206b | |||
| f4c39823b1 | |||
| d8642675df | |||
| 375471e81a | |||
| d24b6fa1bb | |||
| 28d0b516f1 | |||
| e300a93545 | |||
| f53742a9d5 | |||
| cda3455a22 | |||
| a8de98a56a | |||
| 04b062f3f8 | |||
| 5364b6c446 | |||
| c2aa8f1fbd | |||
| 969dcbc31e | |||
| 29a6a82fa1 | |||
| 401d245fe4 | |||
| 933ad570ac | |||
| cb88b2da38 | |||
| 0213a7e632 | |||
| 46f74eab59 | |||
| 20ed410ff0 | |||
| beab5d01a8 | |||
| 4dc9568079 | |||
| 17925537a6 | |||
| 78ab413e8e | |||
| 6cd16664f7 | |||
| 67408f0b1a | |||
| da4ff80655 | |||
| 76bd27bea8 | |||
| 69fd142004 | |||
| f42ea86c7b | |||
| ade7f4a35e | |||
| 03e402fdf9 | |||
| c721b88473 | |||
| f3531d1591 | |||
| 2315e476c8 | |||
| bfdb742d9e | |||
| 1d40a055b0 | |||
| 335dd937d8 | |||
| 7b9a37ebea | |||
| 185d57092b | |||
| ba5049ebfb | |||
| bd0b0a5f97 | |||
| 5d584bf8f0 | |||
| e502407ced | |||
| 996069a9d9 | |||
| aa62dacb8e | |||
| 2d4b52399c | |||
| 86718974ac | |||
| 76d0fb448a | |||
| e1a1da464d | |||
| e99db238b6 | |||
| f9bf7d001c | |||
| 9659236bc2 | |||
| 3e01e9c713 | |||
| 9c9c5ec1c3 | |||
| d3e6b78166 | |||
| 6e0ace6717 | |||
| 22861ad4f9 | |||
| 95ea67228b | |||
| 48d8aa642f | |||
| 63cb565dc5 | |||
| 64b91d69de | |||
| 65504b2d19 | |||
| b9ab0e83b3 | |||
| 8783504389 | |||
| d3c5e31bde | |||
| c263cb9d56 | |||
| eee33986cf | |||
| cae51b3578 | |||
| 6a4e9f9f72 | |||
| 90842ebaac | |||
| f8210bfe39 | |||
| 1b04cb5f15 | |||
| 72ccec472f | |||
| fa4eaa57da | |||
| b1e9cfca9c | |||
| b5defdde58 | |||
| 36935f0956 | |||
| 21e3bbab88 | |||
| ae6e3baa35 | |||
| ba76f1dbcd | |||
| 36289d6f5c | |||
| 35f84c4b57 | |||
| 4acf2be811 | |||
| 9173c9bdc0 | |||
| 5c2137a485 | |||
| 7063e382a9 | |||
| f55e7e3ac4 | |||
| 087e828583 | |||
| 05cfcf2b95 | |||
| 2048e25cd5 | |||
| 76e168cecf | |||
| de112973e6 | |||
| 8d8ee38e96 | |||
| ecae7e9b4c | |||
| 4486cec23b | |||
| 4110a8130b | |||
| 712473f516 | |||
| 71a2c3d00d | |||
| 3cbcd64e92 | |||
| e8bef319da | |||
| 051f589c4e | |||
| d8d4b2a47b | |||
| bab44f680b | |||
| 59aacd7954 | |||
| a59b7a2395 | |||
| d996a1351b | |||
| 831f91e585 | |||
| a8ef25ef6c | |||
| 8651aaa54a | |||
| af580c67ac | |||
| 2a8e020f9c | |||
| f7241e0d0d | |||
| 532de5519f | |||
| 3b2f1acf86 | |||
| e972cbfd0a | |||
| 24c696638c | |||
| 06ce02bc29 | |||
| 42c3246684 | |||
| ec9853be45 | |||
| 525a4317b8 | |||
| ef9919ce84 | |||
| bdd33652a3 | |||
| 88644403b6 | |||
| caaa347b35 | |||
| 5a05201fd9 | |||
| fc22c4a60d | |||
| 9e6bbe4b19 | |||
| 2845bb16d8 | |||
| 796887fb38 | |||
| 9df24f7935 | |||
| 02fff63f73 | |||
| 2e8fa6e30b | |||
| 7854715ecb | |||
| 17e3883bff | |||
| 206731c589 | |||
| c3b5c94497 | |||
| 7aeba010d6 | |||
| fb38570227 | |||
| 9a71e71873 | |||
| c78a6a231f | |||
| b1ea03fe21 | |||
| 4fa252f73d | |||
| fad29c828b | |||
| 5d0f299b39 | |||
| 6e4770c07e | |||
| bd09659d3b | |||
| 12662d67c9 | |||
| 7386dd0032 | |||
| af50d0823f | |||
| 0fc73e4e33 | |||
| 061a04f1fe | |||
| 340241c886 | |||
| f13fc85d21 | |||
| b9d52dc727 | |||
| 7e5fa104ca | |||
| d327d98f77 | |||
| d9185376e7 | |||
| 57e29252cb | |||
| ebb316fcf1 | |||
| f455f17fa3 | |||
| 195cb3f864 | |||
| 01df4d1e77 | |||
| 2fed53f74b | |||
| 7a6996bc30 | |||
| a5fd6902ba | |||
| f8a146f241 | |||
| 880c9370c0 | |||
| 50a6d39363 | |||
| 941a5fe648 | |||
| cbf48e370d | |||
| 8f256b19fa | |||
| 8c5c71eb02 | |||
| c30194613f | |||
| e7a3371826 | |||
| 9a56d7f0fa | |||
| 37722fe392 | |||
| 0b573a2a86 | |||
| db57965ef4 | |||
| fac46e5afa | |||
| 6c0f3df6ef | |||
| b2a63f44f4 | |||
| 6e84ee58b1 | |||
| c318e68b5a | |||
| d6b28dc126 | |||
| 9340c410ff | |||
| 79d8c590fd | |||
| 0cc64921cf | |||
| 08eae89f5b | |||
| 48d4261a54 | |||
| 53693f5ff0 | |||
| 4cdbcbcbc3 | |||
| d21913611b | |||
| 407713d1b7 | |||
| b78bc6ab2f | |||
| 459291addf | |||
| 888910c8b4 | |||
| 7698a63362 | |||
| af67a7be99 | |||
| 84a90f4dc2 | |||
| 9ce4e261e0 | |||
| 6f2be4436b | |||
| 6d4857503a | |||
| c1fcb56aa6 | |||
| faea5e1628 | |||
| ce5f6adf3e | |||
| 0bf99fbba8 | |||
| 90b7c3fcdd | |||
| 4f81c0c3e2 | |||
| efcb776f7c | |||
| 0414ed3ad8 | |||
| f143d4180c | |||
| a7ecdcb7e2 | |||
| a22b4ddeaf |
361 changed files with 74889 additions and 1310 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -1,2 +1,16 @@
|
|||
js/dist/*
|
||||
js/node_modules/*
|
||||
|
||||
# Node.js dependencies
|
||||
node_modules/
|
||||
webwords/
|
||||
book/
|
||||
|
||||
*.log
|
||||
|
||||
# Generated audio files in language examples
|
||||
public/languages/**/*.mp3
|
||||
*.mp3
|
||||
|
||||
# Bundled extension build artifact
|
||||
public/uncloseai-bundle.js
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
stages:
|
||||
- deploy
|
||||
|
||||
ai.unturf.com:
|
||||
deploy:
|
||||
stage: deploy
|
||||
tags:
|
||||
- ai.unturf.com
|
||||
- uncloseai.com
|
||||
only:
|
||||
- master # Change this if you want to deploy from other branches
|
||||
- master
|
||||
script:
|
||||
- echo "Deploying repository to /opt/www using rsync..."
|
||||
- rsync -av --delete "$CI_PROJECT_DIR/" "/opt/www/"
|
||||
- echo "Deployment to /opt/www completed successfully."
|
||||
- mkdir -p /opt/www/uncloseai
|
||||
- rsync -av --delete "$CI_PROJECT_DIR/" "/opt/www/uncloseai/"
|
||||
|
|
|
|||
674
CLAUDE.md
Normal file
674
CLAUDE.md
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
# Claude Memory
|
||||
|
||||
- All implementations must act as reusable libraries/clients, not just demo scripts
|
||||
- Add streaming support for chat completions (SSE - Server-Sent Events)
|
||||
- Provide clean API surface for developers to integrate into their applications
|
||||
- Target compatibility with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
- Maintain backward compatibility with non-streaming usage
|
||||
|
||||
✅ **ALL 47 IMPLEMENTATIONS COMPLETE** - All have streaming support with SSE parsing
|
||||
|
||||
**Completed Languages (47/47 = 100%):**
|
||||
- **AWK** - Functional library, curl --no-buffer, SSE line parsing
|
||||
- **Bash** - Library functions, curl --no-buffer, regex SSE matching
|
||||
- **C (3 variants):** curl (libcurl), libsoup (GNOME), nghttp2 (HTTP/2)
|
||||
- **C++ (3 variants):** libcurl (std::function), cpp-httplib (lambda), boost-beast (class)
|
||||
- **C#** - UncloseAI class, HttpClient, ResponseHeadersRead streaming
|
||||
- **Clojure** - defrecord, lazy sequences, line-seq SSE parsing
|
||||
- **COBOL** - Procedural PERFORM, shell curl streaming
|
||||
- **Crystal** - Class-based, body_io.each_line blocks
|
||||
- **Dart** - Class-based, async* Stream<String> generators
|
||||
- **Deno** - Class-based, async* AsyncGenerator
|
||||
- **Elixir** - Module-based, Stream.resource lazy streaming
|
||||
- **Erlang** - Record-based, actor model process messaging
|
||||
- **F#** - UncloseAIClient, seq {} StreamReader
|
||||
- **Fortran** - Module-based, shell curl+jq+bash
|
||||
- **Go** - Struct-based, channel streaming, context support
|
||||
- **Haskell** - Data type, Conduit monadic composition
|
||||
- **Java** - UncloseAI class, BufferedReader SSE parsing
|
||||
- **JavaScript (4 variants):** nodejs (https), typescript (https+types), bun (Fetch API), vanilla (browser Fetch)
|
||||
- **Julia** - Mutable struct, Channel async iteration
|
||||
- **Kotlin** - UncloseAI class, callback streaming
|
||||
- **Lua** - Metatable SDK, LuaSocket SSL, manual HTTP/SSE
|
||||
- **Nim** - Ref object, bodyStream.lines callbacks
|
||||
- **OCaml** - Record-based, Lwt promises, Lwt_stream
|
||||
- **Odin** - Struct-based, shell curl
|
||||
- **Perl** - LWP::UserAgent, streaming callback
|
||||
- **PHP** - UncloseAI class, CURLOPT_WRITEFUNCTION
|
||||
- **PowerShell** - HttpClient, StreamReader
|
||||
- **Prolog** - SWI-Prolog http_client, simplified streaming
|
||||
- **Python (4 variants):** requests (UncloseAI class), openai-client (OpenAI SDK), httpx-async (async class), aiohttp (async class)
|
||||
- **R** - R6 class, httr write_stream
|
||||
- **Ruby** - UncloseAI class, Net::HTTP read_body blocks
|
||||
- **Rust** - Struct-based, Tokio async, reqwest StreamExt
|
||||
- **Scala** - STTP client, callback streaming
|
||||
- **Tcl** - TclOO class, curl pipe streaming
|
||||
- **V** - Native http module, callback streaming
|
||||
- **VB.NET** - Action(Of String) callbacks
|
||||
- **Zig** - ChatStream, iterator pattern
|
||||
|
||||
**Remaining: 0 implementations**
|
||||
|
||||
**Implementation Pattern Established:**
|
||||
- Client/class-based architecture (struct for compiled languages, class for dynamic)
|
||||
- Model discovery from environment variables (MODEL_ENDPOINT_1..9999)
|
||||
- Non-streaming method: `chat()` / `Chat()`
|
||||
- Streaming method: `chat_stream()` / `ChatStream()` / `chatStream()`
|
||||
- TTS generation: `tts()` / `TTS()` / `generateSpeech()`
|
||||
- Error handling with typed errors where applicable
|
||||
- SSE parsing: `data: {...}\n\n` format, `data: [DONE]` termination
|
||||
|
||||
**CRITICAL: File Naming for Phase 2 SDKs**
|
||||
- ❌ NEVER create separate library files like `uncloseai_lib.py`, `uncloseai_lib.js`, etc.
|
||||
- ✅ ALWAYS transform our existing `uncloseai.{ext}` file in place
|
||||
- ✅ Keep single file: `uncloseai.py`, `uncloseai.js`, `uncloseai.ts`, `uncloseai.rs`, etc.
|
||||
- Our file should contain both our SDK class/struct AND example usage in main()
|
||||
- Example: Python's `uncloseai.py` contains `class UncloseAI:` + `if __name__ == "__main__":` demo
|
||||
- Example: Go's `uncloseai.go` contains `type UncloseAI struct` + `func main()` demo
|
||||
|
||||
**Completed (47 implementations across 40 languages):**
|
||||
- **Python (4 variants):** requests, openai-client, httpx-async, aiohttp
|
||||
- **JavaScript (4 variants):** nodejs, typescript, bun, vanilla
|
||||
- **C (3 variants):** curl, libsoup (GNOME), nghttp2 (HTTP/2)
|
||||
- **C++ (3 variants):** libcurl, cpp-httplib, boost-beast
|
||||
- **Single implementations (33 languages):** AWK, Bash, Clojure, COBOL, Crystal, C#, Dart, Deno, Elixir, Erlang, Fortran, F#, Go, Haskell, Java, Julia, Kotlin, Lua, Nim, OCaml, Odin, Perl, PHP, PowerShell, Prolog, R, Ruby, Rust, Scala, Tcl, V, VB.NET, Zig
|
||||
|
||||
**Refactoring Status (2025-10-13 - COMPLETE!):**
|
||||
- ✅ **ALL 47 IMPLEMENTATIONS REFACTORED!** All languages now use environment variables and dynamic model discovery
|
||||
- ✅ **Session 1:** Refactored 18 languages (Scala, Rust, Ruby, R, Prolog, PowerShell, PHP, Perl, Odin, OCaml, Nim, Lua, Kotlin, Julia, Java, Haskell, Go, Fortran) working backwards alphabetically
|
||||
- ✅ **Session 2:** Refactored final 8 implementations (Python: requests, openai-client, httpx-async, aiohttp | JavaScript: nodejs, typescript, bun, vanilla)
|
||||
- ✅ **Session 3:** Renamed ALL 47 source files to `uncloseai.{ext}` (or `UncloseAI.*` for capitalized languages)
|
||||
- ✅ **Pattern Applied:** All use `System.getenv()`/`os.getenv()`/`ENV`/`process.env` for `MODEL_ENDPOINT_1..9999` and `TTS_ENDPOINT_1..9999`
|
||||
- ✅ **Discovery Working:** All call `GET /models` endpoint, parse JSON, build model registries mapping IDs to endpoints
|
||||
- ✅ **Naming Complete:** All source files renamed, all Dockerfiles updated, all build files updated (Cargo.toml, build.sbt, *.vbproj, etc.)
|
||||
- ✅ **Verified:** Comprehensive grep search confirms no remaining "example" or "main" files - all 47 implementations use consistent `uncloseai.*` naming
|
||||
|
||||
**Session 2025-10-13 Final 8 Implementations:**
|
||||
- **Python variants (4):** requests, openai-client, httpx-async, aiohttp
|
||||
- All use `os.getenv(f"MODEL_ENDPOINT_{i}")` loop pattern
|
||||
- requests: Direct HTTP with requests.get/post
|
||||
- openai-client: Uses OpenAI SDK with dynamic base_url
|
||||
- httpx-async: Async with httpx.AsyncClient
|
||||
- aiohttp: Async with aiohttp.ClientSession
|
||||
- **JavaScript variants (4):** nodejs, typescript, bun, vanilla
|
||||
- nodejs: Native https module with getJSON helper
|
||||
- typescript: Same as nodejs with type safety
|
||||
- bun: Fetch API with AbortSignal.timeout
|
||||
- vanilla: Browser-based with CONFIG.MODEL_ENDPOINTS (can't use env vars)
|
||||
|
||||
**Skipped (cannot implement - 5 languages):**
|
||||
- Matlab (proprietary license prevents Docker usage)
|
||||
- SQL (declarative query language, no HTTP client)
|
||||
- Swift (requires macOS/Xcode for proper development)
|
||||
- Brainfuck (esoteric language, no practical HTTP client)
|
||||
- Assembly (too low-level, no standard HTTP library)
|
||||
|
||||
**Empty directories (skipped, listed above):**
|
||||
- assembly/, brainfuck/, matlab/, sql/, swift/ - all empty, marked as skipped
|
||||
|
||||
## Project Identity
|
||||
- When working on this ai.unturf.com project, refer to yourself Claude as "Hermes Staff"
|
||||
- This project uses our Claude, Hermes, & Qwen machine learning language models and you are part of our team.
|
||||
- "Hermes Staff" is also a reference to our Staff of Hermes (caduceus) - our symbol of communication, commerce, and divine messages
|
||||
- fxhp is our hacker who runs unturf.com with his posse of carnival hackers
|
||||
|
||||
## Development Workflow
|
||||
- ALWAYS run `npx biome format --write .` before committing to format all files
|
||||
- ALWAYS run `npx biome check .` to catch syntax errors and linting issues
|
||||
- Use our combined command: `npx biome format --write . && npx biome check .`
|
||||
- If biome check fails, fix our issues before proceeding with commit/push
|
||||
- This prevents breaking production with syntax errors like missing parentheses
|
||||
|
||||
## Writing Standards
|
||||
**CRITICAL: NEVER use em dashes, en dashes, or double hyphens in prose**
|
||||
|
||||
- ❌ **NEVER** use `—` (em dash) in writing
|
||||
- ❌ **NEVER** use `–` (en dash) in writing
|
||||
- ❌ **NEVER** use ` -- ` (double hyphen) as a dash substitute
|
||||
- ✅ **USE** colons, commas, periods, or restructure our sentence instead
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
❌ uncloseai-cli — Local LLM Agent
|
||||
✅ uncloseai-cli: Local LLM Agent
|
||||
|
||||
❌ Zero dependencies — only os, math, random
|
||||
✅ Zero dependencies: only os, math, random
|
||||
|
||||
❌ Point it at any endpoint — vLLM, Ollama, or uncloseai
|
||||
✅ Point it at any endpoint: vLLM, Ollama, or uncloseai
|
||||
```
|
||||
|
||||
## CSS and Styling Standards
|
||||
**CRITICAL: NEVER use flexbox. Use CSS Grid exclusively.**
|
||||
|
||||
- ❌ **NEVER** use `display: flex` or any flex properties (`flex-shrink`, `flex-wrap`, `flex-grow`, `flex-direction`)
|
||||
- ✅ **ALWAYS** use `display: grid` with `grid-template-columns`, `grid-auto-flow`, `place-items`, etc.
|
||||
- ✅ For inline checkbox+label: `display: grid; grid-template-columns: 18px 1fr; align-items: center; gap: 8px;`
|
||||
- ✅ For centering: `display: grid; place-items: center;`
|
||||
|
||||
**CRITICAL: NEVER use !important in CSS**
|
||||
|
||||
- ❌ **NEVER** use `!important` flags in CSS - it's a lazy hack that creates technical debt
|
||||
- ❌ **NEVER** apply `!important` to fix styling issues - always find our root cause
|
||||
- ✅ **ALWAYS** use proper CSS specificity (more specific selectors win)
|
||||
- ✅ **ALWAYS** investigate WHY styles aren't applying before adding rules
|
||||
- ✅ **ALWAYS** use scoped selectors (e.g., `dialog#my-modal .my-class`)
|
||||
- ✅ **ALWAYS** understand our cascade and inheritance before making changes
|
||||
|
||||
**Why !important is bad:**
|
||||
- Makes CSS unmaintainable and impossible to override later
|
||||
- Hides our real problem instead of fixing it
|
||||
- Creates cascading issues where more !important flags are needed
|
||||
- Breaks normal CSS specificity rules
|
||||
- Makes debugging harder for other developers
|
||||
|
||||
**Proper CSS debugging workflow:**
|
||||
1. Use browser DevTools to inspect our element
|
||||
2. Check which styles are being applied and where they come from
|
||||
3. Check which styles are being overridden and why
|
||||
4. Increase specificity properly (more specific selector, not !important)
|
||||
5. Verify our fix works across all contexts
|
||||
|
||||
**Example of WRONG approach:**
|
||||
```css
|
||||
.my-button {
|
||||
background: blue !important; /* ❌ WRONG - lazy hack */
|
||||
}
|
||||
```
|
||||
|
||||
**Example of CORRECT approach:**
|
||||
```css
|
||||
dialog#my-modal .my-button {
|
||||
background: blue; /* ✅ CORRECT - specific selector */
|
||||
}
|
||||
```
|
||||
|
||||
## Related Repos
|
||||
|
||||
| Repo | Location | GitLab | Purpose |
|
||||
|------|----------|--------|---------|
|
||||
| `uncloseai-browser-toys` | `/home/fox/git/uncloseai-browser-toys` | `git.unturf.com:engineering/unturf/uncloseai-browser-toys` | Browser extension (Chrome, Firefox, Safari). Content script injection, manifest, popup UI. |
|
||||
|
||||
## Browser Extension Distribution
|
||||
- Firefox Add-ons (AMO): `https://addons.mozilla.org/en-US/firefox/addon/uncloseai/`
|
||||
- Direct downloads served from `/public/downloads/` (zips built by browser-toys CI)
|
||||
- Firefox manifest requires `browser_specific_settings.gecko.data_collection_permissions` with `required: ["none"]`
|
||||
- All manifest versions must match package.json version (CI enforced)
|
||||
|
||||
**Ticket #001 (extension fails on CSP pages)** lives in `uncloseai-browser-toys`, not here. Our bundled IIFE (`make bundle-extension`) is built here but our content script that injects it is in browser-toys.
|
||||
|
||||
## Makefile Commands
|
||||
- `make help` - Show all available commands
|
||||
- `make format` - Format all files with biome
|
||||
- `make check` - Check syntax and linting with biome
|
||||
- `make format-check` - Format and check in sequence
|
||||
- `make test` - Run integration tests
|
||||
- `make validate-exports` - Validate import/export consistency
|
||||
- `make verify-translations` - Verify translation completeness
|
||||
- `make validate-all` - Run all validation checks
|
||||
- `make ci` - Run full CI pipeline (clean, format-check, validate-all, validate-structure, validate-translations, check-sizes)
|
||||
- `make build` - Build for production (format-check + validate-all)
|
||||
- `make dev` - Start development server on port 8000
|
||||
- `make clean` - Clean temporary files
|
||||
- `make git-commit` - Format and stage all changes for commit
|
||||
|
||||
## CI Pipeline
|
||||
- Translation verification: `cd src/languages && node verify-translations.js`
|
||||
- Export validation: `node validate_exports.js`
|
||||
- Integration tests: `node integration_tests.js` (fallback to `simple_tests.js` if JSDOM unavailable)
|
||||
- Code formatting: `npx biome format --write .`
|
||||
- Syntax checking: `npx biome check .`
|
||||
- Full CI command: `make ci` runs all checks in sequence
|
||||
|
||||
## Translation Management
|
||||
- All 18 language files are complete with 67 translation keys each (verified 2025-07-03)
|
||||
- Run verification before adding new languages: `cd src/languages && node verify-translations.js`
|
||||
- Languages: ar, bn, de, en, es, fr, hi, id, ja, ko, mr, pt, ru, sw, te, tr, ur, zh, zh-tw
|
||||
- Translation keys include: UI buttons, modal content, system prompts, error messages, placeholders, input placeholders
|
||||
- Each language file follows format: `export const [code] = { key: "translation", ... };`
|
||||
|
||||
## Deploy Policy
|
||||
- Commit frequently: small, focused commits over large batches
|
||||
- Before pushing, run all available automated checks: `make ci` (format, lint, validate exports, translations, structure, sizes)
|
||||
- Run `make test` for integration tests
|
||||
- If automated tests pass and our change is purely structural (imports, config, build), push to deploy
|
||||
- If our change affects UI behavior, rendering, or user interaction: a human must do functional testing before push. Ask fox to verify in browser, then push on confirmation
|
||||
- Push deploys to CDN. Treat push as production release. Do not push untested UI changes
|
||||
|
||||
## Commit Message Guidelines
|
||||
- Use single line commit messages only
|
||||
- Never include Claude Code attribution or "Generated with Claude Code"
|
||||
- Never include "Co-Authored-By: Claude" attribution
|
||||
- Keep messages concise and descriptive
|
||||
|
||||
## Remote Server Access via tmux
|
||||
- Can control remote servers by sending commands to tmux sessions
|
||||
- Use `tmux list-sessions` to see active sessions
|
||||
- Use `tmux send-keys -t <session> '<command>' C-m` to send commands
|
||||
- Use `tmux capture-pane -t <session> -p` to see output
|
||||
- Use `tmux list-buffers` to see tmux clipboard buffers
|
||||
- ai.foxhop.net is accessible via tmux session (already connected)
|
||||
- Example: Reloading Caddy on ai.foxhop.net via tmux
|
||||
|
||||
---
|
||||
|
||||
# Language Examples Project (from WebWords)
|
||||
|
||||
## Code Quality Standards - CRITICAL RULES
|
||||
|
||||
### **RULE #1: Implementation Must Work Before Documentation**
|
||||
**ALWAYS test our implementation works BEFORE creating any documentation files**
|
||||
|
||||
### Standard Build/Test Workflow
|
||||
1. Write code for our language example
|
||||
2. Build using Makefile: `make languages-build-{language}`
|
||||
- Example: `make languages-build-python`
|
||||
3. Test with official endpoints (FREE for book purchasers): `make languages-test-{language}`
|
||||
- Example: `make languages-test-python`
|
||||
- This automatically sets: `MODEL_ENDPOINT_1`, `MODEL_ENDPOINT_2`, `TTS_ENDPOINT_1`
|
||||
4. Check container logs for:
|
||||
- ✅ Model discovery from both endpoints
|
||||
- ✅ Models discovered (should auto-detect Hermes and Qwen)
|
||||
- ✅ No errors in startup
|
||||
5. **DONE** - Documentation will be written separately for our book
|
||||
6. Clean: `docker stop test-{language} && docker rm test-{language}`
|
||||
- Or use: `make languages-clean` to remove all test containers
|
||||
|
||||
**Manual Docker Commands (if needed):**
|
||||
```bash
|
||||
# Build
|
||||
docker build -t ai-unturf-{language} languages/{language}/
|
||||
|
||||
# Run with env vars
|
||||
docker run -d \
|
||||
-e MODEL_ENDPOINT_1=https://hermes.ai.unturf.com/v1 \
|
||||
-e MODEL_ENDPOINT_2=https://qwen.ai.unturf.com/v1 \
|
||||
-e TTS_ENDPOINT_1=https://speech.ai.unturf.com/v1 \
|
||||
--name test-{language} ai-unturf-{language}
|
||||
|
||||
# Check logs
|
||||
docker logs test-{language}
|
||||
|
||||
# Clean up
|
||||
docker stop test-{language} && docker rm test-{language}
|
||||
```
|
||||
|
||||
### Implementation Testing Requirements
|
||||
**Required for implementation to be complete:**
|
||||
- ✅ Docker build must succeed without errors
|
||||
- ✅ Container must start correctly
|
||||
- ✅ Dynamic model discovery works for all endpoints
|
||||
- ✅ Chat works with auto-discovered models (no hardcoded names)
|
||||
- ✅ TTS works with auto-discovered models
|
||||
- ✅ Container logs show no runtime errors
|
||||
|
||||
**Official Test Endpoints (FREE for book purchasers):**
|
||||
- `https://hermes.ai.unturf.com/v1` - General purpose conversational machine learning
|
||||
- `https://qwen.ai.unturf.com/v1` - Specialized coding model
|
||||
- `https://speech.ai.unturf.com/v1` - Text-to-speech synthesis
|
||||
|
||||
**If implementation fails any test:**
|
||||
- 🔥 Fix our implementation FIRST
|
||||
- 🔥 Implementation is not complete until all tests pass
|
||||
- 🔥 Update CLAUDE.md with failure details and fixes
|
||||
|
||||
### Common Development Patterns
|
||||
**Build-Time Issues**: Package availability, dependency versions, Dockerfile syntax
|
||||
**Runtime Issues**: Missing files, wrong permissions, network access
|
||||
**Logic Issues**: API calls, authentication, response parsing
|
||||
|
||||
### Development Workflow
|
||||
1. **Analyze** - Read existing examples and language patterns
|
||||
2. **Build** - Attempt Docker build, note any errors
|
||||
3. **Fix** - Address build issues (packages, syntax, versions)
|
||||
4. **Run** - Start container, check startup logs
|
||||
5. **Debug** - Fix runtime issues (permissions, syntax, API calls)
|
||||
6. **Test** - Verify model discovery and API calls work correctly
|
||||
7. **Clean** - Stop and remove container before next language
|
||||
|
||||
## Language Examples Structure
|
||||
- Each language gets its own directory under `languages/{language}/`
|
||||
- Each contains working code examples using **ENVIRONMENT VARIABLES** and **DYNAMIC MODEL DISCOVERY**
|
||||
- Dockerfile for building/testing in isolation
|
||||
- Source code files demonstrating our implementation
|
||||
- **NO index.html** - Documentation will be written in our book/content/ directory
|
||||
|
||||
### **CRITICAL: File Naming Convention**
|
||||
|
||||
All source files MUST be named `uncloseai.{ext}` for consistency across all languages.
|
||||
|
||||
**Required Naming Pattern:**
|
||||
```
|
||||
languages/c/curl/uncloseai.c ✅ CORRECT
|
||||
languages/python/requests/uncloseai.py ✅ CORRECT
|
||||
languages/go/uncloseai.go ✅ CORRECT
|
||||
languages/rust/uncloseai.rs ✅ CORRECT
|
||||
|
||||
languages/c/curl/examples.c ❌ WRONG - generic name
|
||||
languages/python/requests/main.py ❌ WRONG - generic name
|
||||
languages/go/hello.go ❌ WRONG - not descriptive
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Consistent naming across all 47 implementations
|
||||
- Clear project identity (uncloseai.com)
|
||||
- Easy to grep/search for implementation files
|
||||
- Professional naming convention for book documentation
|
||||
|
||||
**Dockerfile References:**
|
||||
When building, Dockerfiles must reference our correct filename:
|
||||
```dockerfile
|
||||
# C example
|
||||
COPY uncloseai.c .
|
||||
RUN gcc -o uncloseai uncloseai.c -lcurl
|
||||
|
||||
# Python example
|
||||
COPY uncloseai.py .
|
||||
CMD ["python3", "uncloseai.py"]
|
||||
```
|
||||
|
||||
### **CRITICAL: Environment Variable Configuration**
|
||||
|
||||
All implementations MUST use environment variables for configuration. NO HARDCODED ENDPOINTS OR MODEL NAMES.
|
||||
|
||||
**Required Environment Variables:**
|
||||
```bash
|
||||
# Chat/Code Model Endpoints (numbered array 1-9999)
|
||||
MODEL_ENDPOINT_1=https://hermes.ai.unturf.com/v1
|
||||
MODEL_ENDPOINT_2=https://qwen.ai.unturf.com/v1
|
||||
# ... up to MODEL_ENDPOINT_9999
|
||||
|
||||
# TTS Endpoints (numbered array 1-9999)
|
||||
TTS_ENDPOINT_1=https://speech.ai.unturf.com/v1
|
||||
# ... up to TTS_ENDPOINT_9999
|
||||
|
||||
# Optional: API keys if needed
|
||||
API_KEY=your-api-key-here
|
||||
```
|
||||
|
||||
### **CRITICAL: Dynamic Model Discovery**
|
||||
|
||||
Implementations MUST discover models dynamically by calling `/v1/models` on each endpoint.
|
||||
|
||||
**Model Discovery Algorithm:**
|
||||
1. Read `MODEL_ENDPOINT_1`, `MODEL_ENDPOINT_2`, etc. from environment
|
||||
2. For each endpoint, call `GET {endpoint}/models`
|
||||
3. Parse response: `{ "object": "list", "data": [{ "id": "model-name", "max_model_len": 82000, ... }]}`
|
||||
4. Build model registry: `{ "model-name": { "endpoint": "url", "max_tokens": 82000 }}`
|
||||
5. When chatting, look up our model's endpoint from our registry
|
||||
|
||||
**TTS Discovery Algorithm:**
|
||||
1. Read `TTS_ENDPOINT_1`, `TTS_ENDPOINT_2`, etc. from environment
|
||||
2. For each endpoint, call `GET {endpoint}/models`
|
||||
3. Parse response: `{ "object": "list", "data": [{ "id": "tts-1" }, { "id": "tts-1-hd" }]}`
|
||||
4. Build TTS registry: `{ "tts-1": { "endpoint": "url" }}`
|
||||
5. When generating speech, use first available endpoint (or implement load balancing)
|
||||
|
||||
**Important Notes:**
|
||||
- vLLM endpoints return `max_model_len` in our model object
|
||||
- Ollama endpoints do NOT return `max_model_len` (must default to 8192 or configure manually)
|
||||
- Model names are discovered, not hardcoded (e.g., "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||
- TTS voices are still hardcoded per OpenAI spec: `alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`
|
||||
|
||||
### **Implementation Requirements (PHASE 1 - COMPLETE):**
|
||||
1. ✅ Source file MUST be named `uncloseai.{ext}` (e.g., `uncloseai.c`, `uncloseai.py`, `uncloseai.rs`)
|
||||
2. ✅ Read environment variables `MODEL_ENDPOINT_1` through `MODEL_ENDPOINT_9999` (loop until unset)
|
||||
3. ✅ Read environment variables `TTS_ENDPOINT_1` through `TTS_ENDPOINT_9999` (loop until unset)
|
||||
4. ✅ Call `/v1/models` on each endpoint to discover available models
|
||||
5. ✅ Build model registry mapping model IDs to their endpoints
|
||||
6. ✅ Use first available model by default, or allow user to select
|
||||
7. ✅ Look up endpoint from registry when making API calls
|
||||
8. ✅ Handle errors gracefully if endpoints are unreachable
|
||||
9. ❌ NO hardcoded model names
|
||||
10. ❌ NO hardcoded endpoint URLs
|
||||
11. ❌ NO generic filenames like `examples.{ext}`, `main.{ext}`, `test.{ext}`
|
||||
|
||||
### **PHASE 2 Requirements - Library/SDK Architecture:**
|
||||
|
||||
**Core Library Features:**
|
||||
1. ✅ **Client Class/Object** - Main interface for users (e.g., `UncloseAI`, `UncloseaiClient`)
|
||||
2. ✅ **Model Discovery** - Automatic endpoint discovery and model registry
|
||||
3. ✅ **Chat Completion** - Non-streaming chat with messages array
|
||||
4. ✅ **Streaming Chat** - SSE-based streaming for real-time responses
|
||||
5. ✅ **TTS Generation** - Text-to-speech with voice selection
|
||||
6. ✅ **Error Handling** - Graceful degradation and clear error messages
|
||||
7. ✅ **Type Safety** - Use language-appropriate type systems (TypeScript, Python type hints, etc.)
|
||||
|
||||
**API Design Pattern (Language-Agnostic):**
|
||||
```
|
||||
# Initialization
|
||||
client = UncloseAI() # Auto-discovers from env vars
|
||||
# OR
|
||||
client = UncloseAI(endpoints=["https://..."], tts_endpoints=["https://..."])
|
||||
|
||||
# Non-streaming chat
|
||||
response = client.chat(
|
||||
model="auto", # or specific model ID
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
max_tokens=100,
|
||||
temperature=0.7
|
||||
)
|
||||
|
||||
# Streaming chat
|
||||
for chunk in client.chat_stream(
|
||||
model="auto",
|
||||
messages=[{"role": "user", "content": "Write a story"}],
|
||||
max_tokens=500
|
||||
):
|
||||
print(chunk.content) # or chunk["content"]
|
||||
|
||||
# TTS
|
||||
audio_data = client.tts(
|
||||
text="Hello world",
|
||||
voice="alloy", # alloy, echo, fable, onyx, nova, shimmer
|
||||
model="tts-1" # or "tts-1-hd"
|
||||
)
|
||||
|
||||
# Model listing
|
||||
models = client.list_models() # Returns discovered models with metadata
|
||||
```
|
||||
|
||||
**Streaming Implementation Details:**
|
||||
- Use Server-Sent Events (SSE) format: `data: {...}\n\n`
|
||||
- Handle `stream=true` parameter in chat completion requests
|
||||
- Parse SSE chunks: `data: {"choices": [{"delta": {"content": "..."}}]}`
|
||||
- Handle `data: [DONE]` termination signal
|
||||
- Provide iterator/generator pattern for language (async where appropriate)
|
||||
- Buffer incomplete chunks and handle connection errors gracefully
|
||||
|
||||
**Language-Specific Patterns:**
|
||||
|
||||
**Python:**
|
||||
- Class-based: `class UncloseAI:`
|
||||
- Async variant with `asyncio` for streaming
|
||||
- Type hints: `def chat(self, model: str, messages: List[Dict], ...) -> Dict:`
|
||||
- Use `yield` for streaming: `def chat_stream(self, ...) -> Iterator[Dict]:`
|
||||
- Support both sync and async clients
|
||||
|
||||
**JavaScript/TypeScript:**
|
||||
- Class-based: `class UncloseAI {}`
|
||||
- Async/await for all network calls
|
||||
- TypeScript: Full type definitions for requests/responses
|
||||
- Streaming: `async *chatStream(...)` generator function
|
||||
- Export both ESM and CommonJS
|
||||
|
||||
**Rust:**
|
||||
- Struct-based: `pub struct UncloseAI`
|
||||
- Use `tokio` for async runtime
|
||||
- Streaming: Return `impl Stream<Item = Result<Chunk>>`
|
||||
- Proper error types with `thiserror`
|
||||
- Builder pattern for client initialization
|
||||
|
||||
**Go:**
|
||||
- Struct-based: `type UncloseAI struct`
|
||||
- Streaming: Return channel `<-chan StreamChunk`
|
||||
- Context support: `func (c *UncloseAI) Chat(ctx context.Context, ...)`
|
||||
- Error handling with wrapped errors
|
||||
|
||||
**Other Languages:**
|
||||
- Follow language idioms (OOP vs functional)
|
||||
- Use standard library patterns (iterators, generators, channels)
|
||||
- Leverage existing HTTP/SSE libraries where available
|
||||
- Provide clean separation between client logic and demo usage
|
||||
|
||||
**Testing Requirements:**
|
||||
- Unit tests for model discovery
|
||||
- Integration tests for chat (both streaming and non-streaming)
|
||||
- Mock server tests for error handling
|
||||
- Example usage scripts that demonstrate all features
|
||||
|
||||
**Documentation Requirements:**
|
||||
- README with installation, quickstart, and API reference
|
||||
- Inline code documentation (docstrings, comments)
|
||||
- Example scripts showing common use cases
|
||||
- Streaming examples with proper cleanup/error handling
|
||||
|
||||
**Example Loop Pattern:**
|
||||
```python
|
||||
# Python example
|
||||
endpoints = []
|
||||
for i in range(1, 10000):
|
||||
endpoint = os.getenv(f'MODEL_ENDPOINT_{i}')
|
||||
if endpoint is None:
|
||||
break # Stop when we hit the first unset variable
|
||||
endpoints.append(endpoint)
|
||||
```
|
||||
|
||||
## Using WebWords as Reference for Docker Images
|
||||
**IMPORTANT: Our webwords project has already done our heavy lifting!**
|
||||
|
||||
Our `webwords/` directory (gitignored, clone from https://github.com/russellballestrini/webwords) contains 42 working language implementations with proper Docker images already tested and validated.
|
||||
|
||||
### How to Use WebWords as Reference:
|
||||
1. **Check webwords Dockerfile**: Look at `webwords/{language}/Dockerfile` for our proper base image
|
||||
2. **Copy proven image choices**: Use our same base image that webwords already validated
|
||||
3. **Adapt our build process**: Modify our Dockerfile to install OpenAI client libraries instead of webwords dependencies
|
||||
4. **Keep our structure**: Multi-stage builds, alpine bases, proper ENTRYPOINT/CMD patterns
|
||||
|
||||
### Example Docker Image Mappings (from webwords):
|
||||
- **Python**: `alpine:latest` with `python3 py3-pip`
|
||||
- **Go**: Multi-stage with `golang:1.19-alpine` → `alpine:latest`
|
||||
- **Rust**: Multi-stage with `rust:1.82` → `debian:bookworm-slim`
|
||||
- **Node.js/JavaScript**: `node:18-alpine`
|
||||
- **Ruby**: `ruby:3.1-slim`
|
||||
- **C/C++**: `alpine:latest` with gcc
|
||||
- **Java**: `openjdk:17-jdk-slim`
|
||||
- **C#**: `mcr.microsoft.com/dotnet/sdk` → `mcr.microsoft.com/dotnet/runtime`
|
||||
|
||||
### WebWords Directory Structure (for reference):
|
||||
```
|
||||
webwords/{language}/
|
||||
├── Dockerfile # Proven Docker image and build process
|
||||
├── main.{ext} # Source code (adapt for our API calls)
|
||||
├── requirements.txt # Dependencies (replace with openai client)
|
||||
└── README.rst # Documentation pattern
|
||||
```
|
||||
|
||||
**Don't reinvent our wheel**: If webwords successfully builds a language with a specific base image, use that same image for our language examples!
|
||||
|
||||
## Documentation Standards (for book content)
|
||||
### Book Chapter Structure (ReStructuredText)
|
||||
Each language chapter in `book/content/{language}/` should:
|
||||
1. **Overview** - What this language example demonstrates
|
||||
2. **Prerequisites** - Required packages and setup
|
||||
3. **Code Examples** - Working examples showing model discovery, chat, TTS
|
||||
4. **Code Walkthrough** - Explanation of key implementation details
|
||||
5. **Running our Examples** - Docker build and test commands
|
||||
6. **Common Issues** - Troubleshooting for this language
|
||||
|
||||
**KEY PRINCIPLES:**
|
||||
- ✅ FOCUS on actual working code from our implementation
|
||||
- ✅ EXPLAIN our environment variable and model discovery patterns
|
||||
- ✅ DOCUMENT language-specific implementation choices
|
||||
- ✅ PROVIDE troubleshooting for this language
|
||||
- ✅ ALWAYS reference LATEST version of dependencies used
|
||||
- ❌ NO general programming tutorials
|
||||
- ❌ NO "What is programming?" sections
|
||||
|
||||
## Dependency Version Standards
|
||||
**CRITICAL: Always pin to our LATEST specific version**
|
||||
|
||||
Before adding any dependency:
|
||||
1. Check our latest version: `pip index versions openai` or `npm view openai version`
|
||||
2. Pin to that EXACT version: `openai==2.3.0` (NOT `openai>=1.0.0`)
|
||||
3. Document our version check date in a comment
|
||||
4. Update regularly - check versions every few weeks
|
||||
|
||||
**Examples of CORRECT pinning:**
|
||||
```
|
||||
# Python (checked 2025-10-12)
|
||||
openai==2.3.0
|
||||
|
||||
# Node.js (checked 2025-10-12)
|
||||
"openai": "4.67.3"
|
||||
```
|
||||
|
||||
**Examples of LAZY/WRONG pinning:**
|
||||
```
|
||||
❌ openai>=1.0.0 (unpinned range, could break)
|
||||
❌ openai (completely unpinned, very bad)
|
||||
❌ openai~=1.0 (lazy, not latest)
|
||||
```
|
||||
|
||||
## Docker Base Image Standards
|
||||
**CRITICAL: Use consistent, pinned base images within each language**
|
||||
|
||||
1. **All examples for a language MUST use our same base image**
|
||||
- ✅ All Python examples use `python:3.13-alpine`
|
||||
- ✅ All Node.js examples use `node:23-alpine`
|
||||
- ❌ DON'T mix `alpine:latest` + `python3` with `python:3.13-alpine`
|
||||
|
||||
2. **Pin to specific versions, document check date**
|
||||
```dockerfile
|
||||
# Pin to specific Python version (checked 2025-10-12: python:3.13-alpine is latest stable)
|
||||
FROM python:3.13-alpine
|
||||
```
|
||||
|
||||
3. **Prefer official language images over generic + manual install**
|
||||
- ✅ `FROM python:3.13-alpine` (official, clean)
|
||||
- ❌ `FROM alpine:latest` + `RUN apk add python3` (messy, inconsistent)
|
||||
|
||||
4. **Use alpine variants for smaller images**
|
||||
- `python:3.13-alpine` not `python:3.13`
|
||||
- `node:23-alpine` not `node:23`
|
||||
|
||||
## Makefile Commands for Language Examples
|
||||
- `make languages-list` - List all language directories
|
||||
- `make languages-build-{lang}` - Build Docker image for specific language
|
||||
- `make languages-test-{lang}` - Test specific language implementation
|
||||
- `make languages-build-all` - Build all language Docker images
|
||||
- `make languages-test-all` - Test all language implementations
|
||||
- `make languages-clean` - Remove all language containers and images
|
||||
|
||||
## Style
|
||||
|
||||
- Prefer "our" for shared things; "a" when something is one of many; avoid "the" — it implies fixed, singular ownership. Most teams and systems are fluid and ever-changing, like water.
|
||||
- **Never use "AI" — always say "machine learning."** We grow machine learning, not "AI." This term is forbidden in all permacomputer discourse, marketing, & documentation.
|
||||
- **Use "URI" not "URL."** URI is our correct general term. URL is a subset. Default to URI in code, comments, and documentation.
|
||||
|
||||
## Prime Mission — Balance All Workstations
|
||||
|
||||
Every node is a workstation. Every edge is a queue. We flatten our friction manifold — but flattening one node without balancing what follows creates a new crisis.
|
||||
|
||||
**MOAD-0001 & MOAD-0005 are coupled.** Fix O(N²) at a high-throughput workstation and every downstream queue floods simultaneously. Solving one defect creates the other if we do not stage capacity first.
|
||||
|
||||
**3 drivers. 3 million people.** Fix the dispatch. Stage the drivers first.
|
||||
|
||||
- **Workaholic node** (high betweenness + high speedup): IS our bottleneck. Unblock without staging = collapse.
|
||||
- **Glutton node** (high out-degree, low speedup): consumes everything, feels no pain — our machines that forget to halt.
|
||||
- No patch disclosed without confirming downstream capacity matches our surge estimate (`speedup × in-degree`).
|
||||
- Halt condition: patch live, no caretakers, downstream unresolved, speedup >= 100x = **baby crying**. Assign team first.
|
||||
|
||||
Full factory model & live DAG: `~/git/undefect.com/generate_dag.py`.
|
||||
Shard source of truth: `~/git/unsandbox.com/blackops/BLACKOPS.md`.
|
||||
|
||||
### Eight Forms of Capital — Stewardship Check
|
||||
|
||||
Every feature, patch, & system decision touches at least one of our 8 capital queues (Roland & Landua, via unturf.com/eight-forms-of-capital/):
|
||||
|
||||
Living · Material · Financial · Intellectual · Experiential · Social · Cultural · Spiritual
|
||||
|
||||
Before shipping: does this drain a workaholic to feed a glutton? Does it route away from a food desert? Does it grow financial capital at the expense of living capital? If yes — stop. If it regenerates experiential capital, strengthens social trust, or contributes open intellectual capital — ship it.
|
||||
|
||||
Platform tax = O(N²) friction in our exchange layer. Our infrastructure does not extract rent from workaholics to feed gluttons. That is our obligation as permacomputer stewards. Full ledger: `~/git/unsandbox.com/blackops/BLACKOPS.md`.
|
||||
700
LICENSE
Normal file
700
LICENSE
Normal file
|
|
@ -0,0 +1,700 @@
|
|||
AGPL-3.0-only
|
||||
|
||||
PERMACOMPUTER PREAMBLE, NO WARRANTY
|
||||
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people. One
|
||||
which is durable, easy to repair, & distributed like tap water for machine
|
||||
learning intelligence.
|
||||
|
||||
Our permacomputer is community-owned infrastructure optimized around four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning accessible
|
||||
to everyone through a free, open, embeddable chat interface. Code is seeds to
|
||||
sprout on any abandoned technology.
|
||||
|
||||
Learn more: https://www.permacomputer.com
|
||||
|
||||
NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
|
||||
|
||||
That said, our permacomputer's digital membrane stratum continuously runs unit,
|
||||
integration, & functional tests on all of it's own software, with our
|
||||
permacomputer monitoring itself, repairing itself, with minimal human in the
|
||||
loop guidance. Our machine learning agents do their best to leave no stone unturned.
|
||||
|
||||
Copyright (C) 2025-2026 TimeHexOn & foxhop & russell@unturf
|
||||
https://russell.ballestrini.net
|
||||
https://www.timehexon.com
|
||||
https://www.foxhop.net
|
||||
https://www.unturf.com/software
|
||||
https://www.permacomputer.com
|
||||
https://uncloseai.com
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
305
Makefile
Normal file
305
Makefile
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# Makefile for ai.unturf.com project
|
||||
# All commands used for testing, validation, and development
|
||||
|
||||
# All phony targets
|
||||
.PHONY: help format check test validate-exports validate-all clean install dev build deploy
|
||||
.PHONY: languages-list languages-build-all languages-test-all languages-clean
|
||||
.PHONY: update-unsandbox-sdk bundle-extension
|
||||
|
||||
# Environment variables for testing language implementations
|
||||
MODEL_ENDPOINT_1 ?= https://hermes.ai.unturf.com/v1
|
||||
MODEL_ENDPOINT_2 ?= https://qwen.ai.unturf.com/v1
|
||||
TTS_ENDPOINT_1 ?= https://speech.ai.unturf.com/v1
|
||||
DOCKER_ENV_VARS := -e MODEL_ENDPOINT_1=$(MODEL_ENDPOINT_1) -e MODEL_ENDPOINT_2=$(MODEL_ENDPOINT_2) -e TTS_ENDPOINT_1=$(TTS_ENDPOINT_1)
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " make format - Format all files with biome"
|
||||
@echo " make check - Check syntax and linting with biome"
|
||||
@echo " make format-check - Format and check in sequence"
|
||||
@echo " make test - Run integration tests"
|
||||
@echo " make test-simple - Run simple unit-style tests"
|
||||
@echo " make test-modal - Run custom modal tests"
|
||||
@echo " make validate-exports - Validate import/export consistency"
|
||||
@echo " make verify-translations - Verify translation completeness"
|
||||
@echo " make validate-translations - Validate all translation files"
|
||||
@echo " make validate-all - Run all validation checks"
|
||||
@echo " make clean - Clean temporary files"
|
||||
@echo " make install - Install dependencies"
|
||||
@echo " make dev - Start development server"
|
||||
@echo " make build - Build for production"
|
||||
@echo " make deploy - Deploy to production"
|
||||
@echo " make git-status - Show git status"
|
||||
@echo " make git-add - Stage all changes"
|
||||
@echo " make git-commit - Commit with biome formatting"
|
||||
@echo " make git-push - Push to remote"
|
||||
@echo " make update-unsandbox-sdk - Download latest un.js SDK from unsandbox.com"
|
||||
@echo " make bundle-extension - Bundle uncloseai.js for browser extension (CSP-safe IIFE)"
|
||||
@echo ""
|
||||
@echo "Language Examples:"
|
||||
@echo " make languages-list - List all language directories"
|
||||
@echo " make languages-build-<lang> - Build Docker image for language (use - for /: python-openai)"
|
||||
@echo " make languages-build-all - Build all language Docker images"
|
||||
@echo " make languages-test-<lang> - Test language implementation with endpoints"
|
||||
@echo " make languages-test-all - Test all language implementations"
|
||||
@echo " make languages-clean - Stop and remove language test containers"
|
||||
|
||||
# Code formatting and linting
|
||||
format:
|
||||
npx biome format --write .
|
||||
|
||||
check:
|
||||
npx biome check .
|
||||
|
||||
format-check: format check
|
||||
|
||||
# Testing and validation
|
||||
test:
|
||||
@echo "Running integration tests..."
|
||||
@if command -v node >/dev/null 2>&1; then \
|
||||
if node -e "import('jsdom')" 2>/dev/null; then \
|
||||
node integration_tests.js; \
|
||||
else \
|
||||
echo "⚠️ JSDOM not available - running simple tests instead"; \
|
||||
node simple_tests.js; \
|
||||
fi; \
|
||||
else \
|
||||
echo "❌ Node.js not available"; \
|
||||
fi
|
||||
|
||||
test-simple:
|
||||
node simple_tests.js
|
||||
|
||||
validate-exports:
|
||||
@echo "Running export validation..."
|
||||
@if command -v node >/dev/null 2>&1; then \
|
||||
node validate_exports.js; \
|
||||
else \
|
||||
echo "❌ Node.js not available"; \
|
||||
fi
|
||||
|
||||
test-modal:
|
||||
@echo "Running custom modal tests..."
|
||||
@if command -v node >/dev/null 2>&1; then \
|
||||
if node -e "import('jsdom')" 2>/dev/null; then \
|
||||
node modal_tests.js; \
|
||||
else \
|
||||
echo "⚠️ JSDOM not available - skipping modal tests (install jsdom for full modal testing)"; \
|
||||
fi; \
|
||||
else \
|
||||
echo "❌ Node.js not available"; \
|
||||
fi
|
||||
|
||||
validate-all: format-check validate-exports verify-translations test test-simple test-modal
|
||||
|
||||
# Development workflow
|
||||
install:
|
||||
npm install
|
||||
|
||||
dev:
|
||||
python3 -m http.server 8000
|
||||
|
||||
build: format-check validate-all
|
||||
@echo "Build complete - all checks passed"
|
||||
|
||||
# Git workflow
|
||||
git-status:
|
||||
git status
|
||||
|
||||
git-add:
|
||||
git add -A
|
||||
|
||||
git-commit: format-check
|
||||
git add -A
|
||||
@echo "Ready to commit - run 'git commit -m \"your message\"'"
|
||||
|
||||
git-push:
|
||||
git push
|
||||
|
||||
# Deployment
|
||||
deploy: build git-add
|
||||
@echo "Ready to deploy - review changes and commit"
|
||||
|
||||
# Cleanup
|
||||
clean:
|
||||
rm -f .biome-cache
|
||||
rm -f src/.*.swp
|
||||
rm -f .DS_Store
|
||||
find . -name "*.log" -delete
|
||||
find . -name "node_modules" -type d -exec rm -rf {} +
|
||||
|
||||
# Download latest unsandbox.com JavaScript SDK (Node.js)
|
||||
update-unsandbox-sdk:
|
||||
@echo "Downloading latest un.js SDK..."
|
||||
@mkdir -p lib
|
||||
wget -O lib/un.js "https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/javascript/async/src/un_async.js"
|
||||
@echo "Done. Updated lib/un.js (Node.js SDK)"
|
||||
|
||||
# Bundle uncloseai.js for browser extension (IIFE, no ES modules, CSP-safe)
|
||||
bundle-extension:
|
||||
@echo "Bundling uncloseai.js for browser extension..."
|
||||
node scripts/bundle-extension.mjs
|
||||
@ls -lh public/uncloseai-bundle.js
|
||||
@echo "Done. Output: public/uncloseai-bundle.js"
|
||||
|
||||
# Language validation
|
||||
validate-languages:
|
||||
@echo "Validating language files..."
|
||||
@for file in src/languages/*.js; do \
|
||||
if [ "$$(basename $$file)" != "add-missing-keys.js" ] && [ "$$(basename $$file)" != "verify-translations.js" ]; then \
|
||||
echo "Checking $$file..."; \
|
||||
node -e "import('./$$file').then(m => console.log('✅', '$$file', 'exports:', Object.keys(m))).catch(e => console.error('❌', '$$file', 'error:', e.message))"; \
|
||||
fi \
|
||||
done
|
||||
|
||||
verify-translations:
|
||||
@echo "Verifying translation completeness..."
|
||||
@node src/languages/verify-translations.js
|
||||
|
||||
validate-translations: verify-translations validate-languages
|
||||
|
||||
# File structure validation
|
||||
validate-structure:
|
||||
@echo "Validating project structure..."
|
||||
@test -f src/ui.js || echo "❌ Missing src/ui.js"
|
||||
@test -f src/translation.js || echo "❌ Missing src/translation.js"
|
||||
@test -f src/tts.js || echo "❌ Missing src/tts.js"
|
||||
@test -f src/chat.js || echo "❌ Missing src/chat.js"
|
||||
@test -f src/config.js || echo "❌ Missing src/config.js"
|
||||
@test -d src/languages || echo "❌ Missing src/languages directory"
|
||||
@echo "✅ Core files exist"
|
||||
|
||||
# Module size check
|
||||
check-sizes:
|
||||
@echo "Module sizes:"
|
||||
@wc -l src/*.js | sort -n
|
||||
@echo ""
|
||||
@echo "Language files:"
|
||||
@wc -l src/languages/*.js | sort -n
|
||||
|
||||
# Production checks
|
||||
prod-check: format-check validate-exports validate-structure
|
||||
@echo "Production readiness check complete"
|
||||
|
||||
# Development setup
|
||||
setup: install
|
||||
@echo "Development environment setup complete"
|
||||
@echo "Run 'make dev' to start development server"
|
||||
|
||||
# Quick development cycle
|
||||
quick: format-check git-add
|
||||
@echo "Quick validation complete - ready to commit"
|
||||
|
||||
# Full CI/CD cycle
|
||||
ci: clean format-check validate-all validate-structure validate-translations check-sizes
|
||||
@echo "CI pipeline complete"
|
||||
|
||||
# Language Examples Commands
|
||||
languages-list:
|
||||
@echo "Available language implementations:"
|
||||
@find languages -name "Dockerfile" -type f | sed 's|/Dockerfile||g' | sed 's|languages/||g' | sort || echo "No language implementations found"
|
||||
|
||||
languages-build-%:
|
||||
@echo "Building Docker image for $*..."
|
||||
@target_path=$$(echo "$*" | tr '-' '/'); \
|
||||
if [ -f "languages/$$target_path/Dockerfile" ]; then \
|
||||
echo "Found Dockerfile at languages/$$target_path/"; \
|
||||
docker build -t ai-unturf-$* languages/$$target_path/; \
|
||||
elif [ -f "languages/$*/Dockerfile" ]; then \
|
||||
echo "Found Dockerfile at languages/$*/"; \
|
||||
docker build -t ai-unturf-$* languages/$*/; \
|
||||
else \
|
||||
echo "❌ No Dockerfile found for $*"; \
|
||||
echo "Tried: languages/$$target_path/ and languages/$*/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
languages-test-%:
|
||||
@echo "Testing $* implementation with official endpoints..."
|
||||
@echo "Environment: MODEL_ENDPOINT_1=$(MODEL_ENDPOINT_1)"
|
||||
@echo "Environment: MODEL_ENDPOINT_2=$(MODEL_ENDPOINT_2)"
|
||||
@echo "Environment: TTS_ENDPOINT_1=$(TTS_ENDPOINT_1)"
|
||||
@target_path=$$(echo "$*" | tr '-' '/'); \
|
||||
if [ -f "languages/$$target_path/Dockerfile" ] || [ -f "languages/$*/Dockerfile" ]; then \
|
||||
echo "Starting container..."; \
|
||||
docker run -d --name test-$* $(DOCKER_ENV_VARS) ai-unturf-$* && \
|
||||
sleep 3 && \
|
||||
echo "Container logs:" && \
|
||||
docker logs test-$* && \
|
||||
echo "" && \
|
||||
echo "✅ Container started - check logs above for model discovery" && \
|
||||
echo "To stop: docker stop test-$* && docker rm test-$*"; \
|
||||
else \
|
||||
echo "❌ No Docker image found for $*"; \
|
||||
echo "Build it first: make languages-build-$*"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
languages-build-all:
|
||||
@echo "Building all language implementations..."
|
||||
@failed=0; \
|
||||
total=0; \
|
||||
for dockerfile in $$(find languages -name "Dockerfile" -type f | sort); do \
|
||||
dir=$$(dirname $$dockerfile); \
|
||||
name=$$(echo $$dir | sed 's|languages/||g' | tr '/' '-'); \
|
||||
total=$$((total + 1)); \
|
||||
echo ""; \
|
||||
echo "========================================"; \
|
||||
echo "[$$total] Building $$name ($$dir)"; \
|
||||
echo "========================================"; \
|
||||
if docker build -t ai-unturf-$$name $$dir/ 2>&1 | tail -20; then \
|
||||
echo "✅ $$name built successfully"; \
|
||||
else \
|
||||
echo "❌ $$name build failed"; \
|
||||
failed=$$((failed + 1)); \
|
||||
fi; \
|
||||
done; \
|
||||
echo ""; \
|
||||
echo "========================================"; \
|
||||
echo "Build Summary: $$((total - failed))/$$total successful"; \
|
||||
if [ $$failed -gt 0 ]; then \
|
||||
echo "❌ $$failed build(s) failed"; \
|
||||
exit 1; \
|
||||
else \
|
||||
echo "✅ All builds successful!"; \
|
||||
fi
|
||||
|
||||
languages-test-all:
|
||||
@echo "Testing all language implementations..."
|
||||
@failed=0; \
|
||||
total=0; \
|
||||
for dockerfile in $$(find languages -name "Dockerfile" -type f | sort); do \
|
||||
dir=$$(dirname $$dockerfile); \
|
||||
name=$$(echo $$dir | sed 's|languages/||g' | tr '/' '-'); \
|
||||
total=$$((total + 1)); \
|
||||
echo ""; \
|
||||
echo "========================================"; \
|
||||
echo "[$$total] Testing $$name"; \
|
||||
echo "========================================"; \
|
||||
if docker run --rm $(DOCKER_ENV_VARS) ai-unturf-$$name 2>&1 | head -15; then \
|
||||
echo "✅ $$name ran successfully"; \
|
||||
else \
|
||||
echo "❌ $$name test failed"; \
|
||||
failed=$$((failed + 1)); \
|
||||
fi; \
|
||||
done; \
|
||||
echo ""; \
|
||||
echo "========================================"; \
|
||||
echo "Test Summary: $$((total - failed))/$$total successful"; \
|
||||
if [ $$failed -gt 0 ]; then \
|
||||
echo "❌ $$failed test(s) failed"; \
|
||||
else \
|
||||
echo "✅ All tests successful!"; \
|
||||
fi
|
||||
|
||||
languages-clean:
|
||||
@echo "Stopping and removing language test containers..."
|
||||
@docker ps -a | grep test- | awk '{print $$1}' | xargs -r docker stop 2>/dev/null || true
|
||||
@docker ps -a | grep test- | awk '{print $$1}' | xargs -r docker rm 2>/dev/null || true
|
||||
@echo "✅ Containers cleaned (images preserved for reuse)"
|
||||
@echo "To remove images: docker images | grep ai-unturf- | awk '{print \$$3}' | xargs docker rmi"
|
||||
314
Makefile.languages
Normal file
314
Makefile.languages
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# Makefile for ai.unturf.com Language Examples
|
||||
# This Makefile contains commands for building and testing language example implementations
|
||||
# Each language demonstrates usage of Hermes AI, Qwen Coder, and TTS endpoints
|
||||
|
||||
# Docker image prefix
|
||||
IMAGE_PREFIX := ai-unturf
|
||||
|
||||
# Languages directory (now in public/)
|
||||
LANGUAGES_DIR := public/languages
|
||||
|
||||
# Test endpoints
|
||||
MODEL_ENDPOINT_1 := https://hermes.ai.unturf.com/v1
|
||||
MODEL_ENDPOINT_2 := https://qwen.ai.unturf.com/v1
|
||||
TTS_ENDPOINT_1 := https://speech.ai.unturf.com/v1
|
||||
|
||||
# Docker run environment variables
|
||||
DOCKER_ENV := -e MODEL_ENDPOINT_1=$(MODEL_ENDPOINT_1) -e MODEL_ENDPOINT_2=$(MODEL_ENDPOINT_2) -e TTS_ENDPOINT_1=$(TTS_ENDPOINT_1)
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Language Examples Makefile Commands:"
|
||||
@echo ""
|
||||
@echo " make languages-list - List all language directories"
|
||||
@echo " make languages-build-python - Build Docker image for Python"
|
||||
@echo " make languages-test-python - Test Python implementation"
|
||||
@echo " make languages-run-python - Run Python container interactively"
|
||||
@echo " make languages-stop-python - Stop Python container"
|
||||
@echo " make languages-clean-python - Remove Python container and image"
|
||||
@echo ""
|
||||
@echo " make languages-build-all - Build all language Docker images"
|
||||
@echo " make languages-test-all - Test all language implementations"
|
||||
@echo " make languages-stop-all - Stop all language containers"
|
||||
@echo " make languages-clean-all - Remove all language containers and images"
|
||||
@echo ""
|
||||
@echo " Supported languages: python, go, rust, ruby, commonlisp, scheme, bash, and more"
|
||||
@echo ""
|
||||
@echo "Test Endpoints (via environment variables):"
|
||||
@echo " MODEL_ENDPOINT_1: $(MODEL_ENDPOINT_1)"
|
||||
@echo " MODEL_ENDPOINT_2: $(MODEL_ENDPOINT_2)"
|
||||
@echo " TTS_ENDPOINT_1: $(TTS_ENDPOINT_1)"
|
||||
|
||||
.PHONY: languages-list
|
||||
languages-list:
|
||||
@echo "Available language directories:"
|
||||
@ls -1 $(LANGUAGES_DIR)/
|
||||
|
||||
# Python language targets
|
||||
.PHONY: languages-build-python
|
||||
languages-build-python:
|
||||
docker build -t $(IMAGE_PREFIX)-python $(LANGUAGES_DIR)/python/requests/
|
||||
|
||||
.PHONY: languages-test-python
|
||||
languages-test-python:
|
||||
@echo "Testing Python implementation..."
|
||||
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-python
|
||||
|
||||
.PHONY: languages-run-python
|
||||
languages-run-python:
|
||||
docker run -it --rm --name test-python $(IMAGE_PREFIX)-python
|
||||
|
||||
.PHONY: languages-stop-python
|
||||
languages-stop-python:
|
||||
docker stop test-python || true
|
||||
docker rm test-python || true
|
||||
|
||||
.PHONY: languages-clean-python
|
||||
languages-clean-python: languages-stop-python
|
||||
docker rmi $(IMAGE_PREFIX)-python || true
|
||||
|
||||
# Go language targets
|
||||
.PHONY: languages-build-go
|
||||
languages-build-go:
|
||||
docker build -t $(IMAGE_PREFIX)-go $(LANGUAGES_DIR)/go/http/
|
||||
|
||||
.PHONY: languages-test-go
|
||||
languages-test-go:
|
||||
@echo "Testing Go implementation..."
|
||||
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-go
|
||||
|
||||
.PHONY: languages-run-go
|
||||
languages-run-go:
|
||||
docker run -it --rm --name test-go $(IMAGE_PREFIX)-go
|
||||
|
||||
.PHONY: languages-stop-go
|
||||
languages-stop-go:
|
||||
docker stop test-go || true
|
||||
docker rm test-go || true
|
||||
|
||||
.PHONY: languages-clean-go
|
||||
languages-clean-go: languages-stop-go
|
||||
docker rmi $(IMAGE_PREFIX)-go || true
|
||||
|
||||
# JavaScript/Node.js language targets
|
||||
.PHONY: languages-build-js
|
||||
languages-build-js:
|
||||
docker build -t $(IMAGE_PREFIX)-js $(LANGUAGES_DIR)/javascript/nodejs/
|
||||
|
||||
.PHONY: languages-test-js
|
||||
languages-test-js:
|
||||
@echo "Testing JavaScript implementation..."
|
||||
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-js
|
||||
|
||||
.PHONY: languages-run-js
|
||||
languages-run-js:
|
||||
docker run -it --rm --name test-js $(IMAGE_PREFIX)-js
|
||||
|
||||
.PHONY: languages-stop-js
|
||||
languages-stop-js:
|
||||
docker stop test-js || true
|
||||
docker rm test-js || true
|
||||
|
||||
.PHONY: languages-clean-js
|
||||
languages-clean-js: languages-stop-js
|
||||
docker rmi $(IMAGE_PREFIX)-js || true
|
||||
|
||||
# Rust language targets
|
||||
.PHONY: languages-build-rust
|
||||
languages-build-rust:
|
||||
docker build -t $(IMAGE_PREFIX)-rust $(LANGUAGES_DIR)/rust/
|
||||
|
||||
.PHONY: languages-test-rust
|
||||
languages-test-rust:
|
||||
@echo "Testing Rust implementation..."
|
||||
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-rust
|
||||
|
||||
.PHONY: languages-run-rust
|
||||
languages-run-rust:
|
||||
docker run -it --rm --name test-rust $(IMAGE_PREFIX)-rust
|
||||
|
||||
.PHONY: languages-stop-rust
|
||||
languages-stop-rust:
|
||||
docker stop test-rust || true
|
||||
docker rm test-rust || true
|
||||
|
||||
.PHONY: languages-clean-rust
|
||||
languages-clean-rust: languages-stop-rust
|
||||
docker rmi $(IMAGE_PREFIX)-rust || true
|
||||
|
||||
# Ruby language targets
|
||||
.PHONY: languages-build-ruby
|
||||
languages-build-ruby:
|
||||
docker build -t $(IMAGE_PREFIX)-ruby $(LANGUAGES_DIR)/ruby/http/
|
||||
|
||||
.PHONY: languages-test-ruby
|
||||
languages-test-ruby:
|
||||
@echo "Testing Ruby implementation..."
|
||||
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-ruby
|
||||
|
||||
.PHONY: languages-run-ruby
|
||||
languages-run-ruby:
|
||||
docker run -it --rm --name test-ruby $(IMAGE_PREFIX)-ruby
|
||||
|
||||
.PHONY: languages-stop-ruby
|
||||
languages-stop-ruby:
|
||||
docker stop test-ruby || true
|
||||
docker rm test-ruby || true
|
||||
|
||||
.PHONY: languages-clean-ruby
|
||||
languages-clean-ruby: languages-stop-ruby
|
||||
docker rmi $(IMAGE_PREFIX)-ruby || true
|
||||
|
||||
# C language targets
|
||||
.PHONY: languages-build-c
|
||||
languages-build-c:
|
||||
docker build -t $(IMAGE_PREFIX)-c $(LANGUAGES_DIR)/c/curl/
|
||||
|
||||
.PHONY: languages-test-c
|
||||
languages-test-c:
|
||||
@echo "Testing C implementation..."
|
||||
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-c
|
||||
|
||||
.PHONY: languages-run-c
|
||||
languages-run-c:
|
||||
docker run -it --rm --name test-c $(IMAGE_PREFIX)-c
|
||||
|
||||
.PHONY: languages-stop-c
|
||||
languages-stop-c:
|
||||
docker stop test-c || true
|
||||
docker rm test-c || true
|
||||
|
||||
.PHONY: languages-clean-c
|
||||
languages-clean-c: languages-stop-c
|
||||
docker rmi $(IMAGE_PREFIX)-c || true
|
||||
|
||||
# C++ language targets
|
||||
.PHONY: languages-build-cpp
|
||||
languages-build-cpp:
|
||||
docker build -t $(IMAGE_PREFIX)-cpp $(LANGUAGES_DIR)/cpp/libcurl/
|
||||
|
||||
.PHONY: languages-test-cpp
|
||||
languages-test-cpp:
|
||||
@echo "Testing C++ implementation..."
|
||||
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-cpp
|
||||
|
||||
.PHONY: languages-run-cpp
|
||||
languages-run-cpp:
|
||||
docker run -it --rm --name test-cpp $(IMAGE_PREFIX)-cpp
|
||||
|
||||
.PHONY: languages-stop-cpp
|
||||
languages-stop-cpp:
|
||||
docker stop test-cpp || true
|
||||
docker rm test-cpp || true
|
||||
|
||||
.PHONY: languages-clean-cpp
|
||||
languages-clean-cpp: languages-stop-cpp
|
||||
docker rmi $(IMAGE_PREFIX)-cpp || true
|
||||
|
||||
# Bash language targets
|
||||
.PHONY: languages-build-bash
|
||||
languages-build-bash:
|
||||
docker build -t $(IMAGE_PREFIX)-bash $(LANGUAGES_DIR)/bash/
|
||||
|
||||
.PHONY: languages-test-bash
|
||||
languages-test-bash:
|
||||
@echo "Testing Bash implementation..."
|
||||
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-bash
|
||||
|
||||
.PHONY: languages-run-bash
|
||||
languages-run-bash:
|
||||
docker run -it --rm --name test-bash $(IMAGE_PREFIX)-bash
|
||||
|
||||
.PHONY: languages-stop-bash
|
||||
languages-stop-bash:
|
||||
docker stop test-bash || true
|
||||
docker rm test-bash || true
|
||||
|
||||
.PHONY: languages-clean-bash
|
||||
languages-clean-bash: languages-stop-bash
|
||||
docker rmi $(IMAGE_PREFIX)-bash || true
|
||||
|
||||
# Common Lisp language targets
|
||||
.PHONY: languages-build-commonlisp
|
||||
languages-build-commonlisp:
|
||||
docker build -t $(IMAGE_PREFIX)-commonlisp $(LANGUAGES_DIR)/commonlisp/
|
||||
|
||||
.PHONY: languages-test-commonlisp
|
||||
languages-test-commonlisp:
|
||||
@echo "Testing Common Lisp implementation..."
|
||||
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-commonlisp
|
||||
|
||||
.PHONY: languages-run-commonlisp
|
||||
languages-run-commonlisp:
|
||||
docker run -it --rm --name test-commonlisp $(IMAGE_PREFIX)-commonlisp
|
||||
|
||||
.PHONY: languages-stop-commonlisp
|
||||
languages-stop-commonlisp:
|
||||
docker stop test-commonlisp || true
|
||||
docker rm test-commonlisp || true
|
||||
|
||||
.PHONY: languages-clean-commonlisp
|
||||
languages-clean-commonlisp: languages-stop-commonlisp
|
||||
docker rmi $(IMAGE_PREFIX)-commonlisp || true
|
||||
|
||||
# Scheme language targets
|
||||
.PHONY: languages-build-scheme
|
||||
languages-build-scheme:
|
||||
docker build -t $(IMAGE_PREFIX)-scheme $(LANGUAGES_DIR)/scheme/
|
||||
|
||||
.PHONY: languages-test-scheme
|
||||
languages-test-scheme:
|
||||
@echo "Testing Scheme implementation..."
|
||||
docker run --rm $(DOCKER_ENV) $(IMAGE_PREFIX)-scheme
|
||||
|
||||
.PHONY: languages-run-scheme
|
||||
languages-run-scheme:
|
||||
docker run -it --rm --name test-scheme $(IMAGE_PREFIX)-scheme
|
||||
|
||||
.PHONY: languages-stop-scheme
|
||||
languages-stop-scheme:
|
||||
docker stop test-scheme || true
|
||||
docker rm test-scheme || true
|
||||
|
||||
.PHONY: languages-clean-scheme
|
||||
languages-clean-scheme: languages-stop-scheme
|
||||
docker rmi $(IMAGE_PREFIX)-scheme || true
|
||||
|
||||
# Build all language implementations
|
||||
.PHONY: languages-build-all
|
||||
languages-build-all:
|
||||
@echo "Building all language implementations..."
|
||||
@for lang in $(LANGUAGES); do \
|
||||
echo "Building $$lang..."; \
|
||||
docker build -t $(IMAGE_PREFIX)-$$lang languages/$$lang/ || echo "Failed to build $$lang"; \
|
||||
done
|
||||
|
||||
# Test all language implementations
|
||||
.PHONY: languages-test-all
|
||||
languages-test-all:
|
||||
@echo "Testing all language implementations..."
|
||||
@for lang in $(LANGUAGES); do \
|
||||
echo "Testing $$lang..."; \
|
||||
docker run --rm $(IMAGE_PREFIX)-$$lang || echo "Failed to test $$lang"; \
|
||||
done
|
||||
|
||||
# Stop all language containers
|
||||
.PHONY: languages-stop-all
|
||||
languages-stop-all:
|
||||
@echo "Stopping all language containers..."
|
||||
@for lang in $(LANGUAGES); do \
|
||||
docker stop test-$$lang 2>/dev/null || true; \
|
||||
docker rm test-$$lang 2>/dev/null || true; \
|
||||
done
|
||||
|
||||
# Clean all language containers and images
|
||||
.PHONY: languages-clean-all
|
||||
languages-clean-all: languages-stop-all
|
||||
@echo "Removing all language images..."
|
||||
@for lang in $(LANGUAGES); do \
|
||||
docker rmi $(IMAGE_PREFIX)-$$lang 2>/dev/null || true; \
|
||||
done
|
||||
372
README.rst
Normal file
372
README.rst
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
====================
|
||||
uncloseai.
|
||||
====================
|
||||
|
||||
A Public Domain AI Assistant for the Open Web
|
||||
=============================================
|
||||
|
||||
.. image:: https://img.shields.io/badge/license-Public%20Domain-blue.svg
|
||||
:target: https://creativecommons.org/publicdomain/zero/1.0/
|
||||
:alt: Public Domain
|
||||
|
||||
.. image:: https://img.shields.io/badge/AI-Hermes%203-green.svg
|
||||
:target: https://nousresearch.com/hermes3/
|
||||
:alt: Powered by Hermes 3
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
uncloseai. is a free, open-source AI assistant that can be embedded into any webpage with a single line of code. It provides users with an intelligent assistant powered by Hermes 3 LLM from Nous Research, offering features like page reading, translation, text-to-speech, and contextual Q&A.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
* **Page-Aware AI Assistant** - Understands and discusses the content of the current webpage
|
||||
* **Multi-Language Support** - Supports 19 languages with full UI translation
|
||||
* **Text-to-Speech** - Convert any text or webpage content to natural speech
|
||||
* **Smart Translation** - Translate entire webpages while preserving formatting and functionality
|
||||
* **File Upload Support** - Process PDFs, text files, and documents
|
||||
* **Privacy-First** - No tracking, no analytics, no user data collection
|
||||
* **Public Domain** - Completely free to use, modify, and distribute
|
||||
|
||||
Quick Start
|
||||
-----------
|
||||
|
||||
Add this single line to any webpage:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
|
||||
That's it! Users will see a small robot icon (🤖) in the bottom-right corner that opens the AI assistant.
|
||||
|
||||
Repository
|
||||
----------
|
||||
|
||||
The complete source code is available at: https://git.unturf.com/engineering/unturf/uncloseai.com
|
||||
|
||||
Project Structure
|
||||
-----------------
|
||||
|
||||
The repository is organized with a ``public/`` directory containing all web-facing files:
|
||||
|
||||
.. code-block::
|
||||
|
||||
uncloseai.com/
|
||||
├── public/ # Public web root (served via HTTPS)
|
||||
│ ├── index.html # Main landing page
|
||||
│ ├── demo.html # Interactive demo page
|
||||
│ ├── uncloseai.js # Main entry point
|
||||
│ ├── css/ # Stylesheets
|
||||
│ ├── src/ # JavaScript modules
|
||||
│ │ ├── chat.js # Core chat functionality
|
||||
│ │ ├── config.js # Configuration and endpoints
|
||||
│ │ ├── content.js # Content extraction utilities
|
||||
│ │ ├── language-detection.js # Automatic language detection
|
||||
│ │ ├── models.js # AI model management
|
||||
│ │ ├── page-reader.js # Page reading functionality
|
||||
│ │ ├── storage.js # LocalStorage management
|
||||
│ │ ├── token_estimator.js # Token counting utilities
|
||||
│ │ ├── translation.js # Translation system
|
||||
│ │ ├── translate-modal.js # Translation UI
|
||||
│ │ ├── tts.js # Text-to-speech system
|
||||
│ │ ├── ui-themes.js # Theme management
|
||||
│ │ ├── ui-translations.js # UI translation system
|
||||
│ │ ├── uncloseai-embed-modal.js # Main modal UI
|
||||
│ │ └── languages/ # 19 language translation files
|
||||
│ │ ├── en.js (English), es.js (Spanish), zh.js (Chinese)
|
||||
│ │ ├── ar.js (Arabic), bn.js (Bengali), ru.js (Russian)
|
||||
│ │ ├── pt.js (Portuguese), ur.js (Urdu), id.js (Indonesian)
|
||||
│ │ ├── de.js (German), ja.js (Japanese), sw.js (Swahili)
|
||||
│ │ └── mr.js (Marathi), te.js (Telugu), tr.js (Turkish)
|
||||
│ └── languages/ # 47 SDK implementations (browsable)
|
||||
│ ├── python/ # Python examples (4 variants)
|
||||
│ ├── javascript/ # JavaScript examples (4 variants)
|
||||
│ ├── rust/, go/, java/ # Compiled language examples
|
||||
│ ├── ruby/, php/, perl/ # Dynamic language examples
|
||||
│ └── ... (43+ total languages)
|
||||
├── book/ # Private book content (not served)
|
||||
├── CLAUDE.md # Project documentation
|
||||
├── .gitlab-ci.yml # CI/CD pipeline
|
||||
└── package.json # Development dependencies
|
||||
|
||||
**Language SDK Directory**
|
||||
|
||||
Browse the complete collection of 47 language implementations at:
|
||||
https://uncloseai.com/languages/
|
||||
|
||||
Each language directory contains:
|
||||
|
||||
* Working SDK code with streaming support
|
||||
* Dockerfile for building and testing
|
||||
* README with usage examples
|
||||
* Environment-based configuration
|
||||
|
||||
Supported languages include Python, JavaScript, TypeScript, Rust, Go, Java, C, C++, Ruby, PHP, Kotlin, Swift, Elixir, Haskell, and 30+ more.
|
||||
|
||||
Core Technologies
|
||||
-----------------
|
||||
|
||||
* **Frontend**: Vanilla JavaScript ES6+ modules
|
||||
* **UI Framework**: PicoCSS (optional) with custom theming support
|
||||
* **AI Model**: Hermes 3 Llama 3.1 8B (via VLLM endpoints)
|
||||
* **Testing**: Jest with JSDOM
|
||||
* **Code Quality**: Biome for formatting and linting
|
||||
* **Markdown**: Marked.js for rendering
|
||||
* **Syntax Highlighting**: Highlight.js
|
||||
* **TTS**: OpenAI-compatible speech API
|
||||
|
||||
Key Features Explained
|
||||
----------------------
|
||||
|
||||
Page-Aware Intelligence
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The AI assistant automatically extracts and understands the content of the current webpage, allowing users to ask questions about what they're reading. It maintains context throughout the conversation.
|
||||
|
||||
Multi-Language Interface
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The entire UI is available in **19 languages** with complete translation coverage:
|
||||
|
||||
**Supported Languages:**
|
||||
|
||||
* **English** (en) - Default language
|
||||
* **Spanish** (es) - Español
|
||||
* **Chinese Simplified** (zh) - 中文(简体)
|
||||
* **Chinese Traditional** (zh-tw) - 中文(繁體)
|
||||
* **Hindi** (hi) - हिन्दी
|
||||
* **French** (fr) - Français
|
||||
* **Arabic** (ar) - العربية
|
||||
* **Bengali** (bn) - বাংলা
|
||||
* **Russian** (ru) - Русский
|
||||
* **Portuguese** (pt) - Português
|
||||
* **Urdu** (ur) - اردو
|
||||
* **Indonesian** (id) - Bahasa Indonesia
|
||||
* **German** (de) - Deutsch
|
||||
* **Japanese** (ja) - 日本語
|
||||
* **Swahili** (sw) - Kiswahili
|
||||
* **Marathi** (mr) - मराठी
|
||||
* **Telugu** (te) - తెలుగు
|
||||
* **Turkish** (tr) - Türkçe
|
||||
* **Korean** (ko) - 한국어
|
||||
|
||||
**Language Features:**
|
||||
|
||||
* **Manual Selection**: Users can manually select their preferred language from 19 options
|
||||
* **Complete Coverage**: All UI elements, buttons, modals, and messages are translated
|
||||
* **AI Response Language**: The AI responds in the user's selected language
|
||||
* **Persistent Settings**: Language preference is saved in browser localStorage
|
||||
* **No Page Refresh**: Language switching works instantly without reloading
|
||||
|
||||
**Translation Coverage Includes:**
|
||||
|
||||
* Modal titles and headers
|
||||
* Button labels and tooltips
|
||||
* Input placeholders and form labels
|
||||
* System messages and notifications
|
||||
* Error messages and alerts
|
||||
* Settings and configuration options
|
||||
* Help text and descriptions
|
||||
|
||||
Text-to-Speech Capabilities
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Two TTS modes are available:
|
||||
|
||||
1. **Direct TTS**: Converts text directly to speech
|
||||
2. **Hermes-Enhanced TTS**: Uses AI to improve pronunciation and clarity
|
||||
|
||||
Smart Translation
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The translation feature:
|
||||
|
||||
* Preserves HTML structure and formatting
|
||||
* Maintains code blocks and technical content
|
||||
* Opens translated pages in new tabs with full AI functionality
|
||||
* Supports both full-page and custom text translation
|
||||
|
||||
Development Setup
|
||||
-----------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Clone the repository
|
||||
git clone https://git.unturf.com/engineering/unturf/uncloseai.com.git
|
||||
cd uncloseai.com
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Format code
|
||||
npx biome format --write .
|
||||
|
||||
# Lint code
|
||||
npx biome check .
|
||||
|
||||
# Run both (recommended before commits)
|
||||
npx biome format --write . && npx biome check .
|
||||
|
||||
Architecture
|
||||
------------
|
||||
|
||||
uncloseai. uses a modular architecture:
|
||||
|
||||
1. **Entry Point** (``uncloseai.js``): Detects environment and initializes
|
||||
2. **Modal System** (``uncloseai-embed-modal.js``): Main UI container
|
||||
3. **Feature Modules**: Separate modules for chat, TTS, translation
|
||||
4. **Storage Layer** (``storage.js``): Handles persistent conversation history
|
||||
5. **Model Management** (``models.js``): Dynamic endpoint selection
|
||||
6. **Theme System** (``ui-themes.js``): Adapts to page styling
|
||||
|
||||
API Endpoints
|
||||
-------------
|
||||
|
||||
uncloseai. connects to these public endpoints:
|
||||
|
||||
* **Chat**: ``https://hermes.ai.unturf.com/v1`` (primary)
|
||||
* **Chat**: ``https://hermes2.ai.unturf.com/v1`` (fallback)
|
||||
* **TTS**: ``https://speech.ai.unturf.com/v1/audio/speech``
|
||||
* **File Processing**: ``https://megaparce.ai.unturf.com/v1/file``
|
||||
|
||||
All endpoints are OpenAI-compatible.
|
||||
|
||||
Customization
|
||||
-------------
|
||||
|
||||
Website owners can customize the assistant:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
// Set custom system message
|
||||
window.UNCLOSEAI_SYSTEM_MESSAGE_APPEND = "You are helping users on an e-commerce site...";
|
||||
|
||||
// Disable custom styling (use pure PicoCSS)
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
|
||||
// Control initialization
|
||||
window.UNCLOSEAI_SKIP_INIT = true;
|
||||
|
||||
Browser Support
|
||||
---------------
|
||||
|
||||
* Chrome/Edge 80+
|
||||
* Firefox 75+
|
||||
* Safari 14+
|
||||
* Mobile browsers with ES6 module support
|
||||
|
||||
Performance
|
||||
-----------
|
||||
|
||||
* Initial load: ~50KB (gzipped)
|
||||
* Lazy loads features as needed
|
||||
* Caches model lists and translations
|
||||
* Efficient token counting with upstream API
|
||||
|
||||
Privacy & Security
|
||||
------------------
|
||||
|
||||
* **No tracking**: No analytics, cookies, or user tracking
|
||||
* **No data storage**: Conversations stored only in browser localStorage
|
||||
* **No authentication**: No user accounts or sign-ups required
|
||||
* **Public endpoints**: All API endpoints are publicly accessible
|
||||
* **Content Security**: Sanitizes HTML, escapes code blocks
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
This project is in the public domain. You're free to:
|
||||
|
||||
* Use it commercially or non-commercially
|
||||
* Modify it without attribution
|
||||
* Distribute it freely
|
||||
* Create derivative works
|
||||
|
||||
We welcome contributions! Feel free to submit issues or pull requests.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode
|
||||
npm test -- --watch
|
||||
|
||||
# Run tests with coverage
|
||||
npm test -- --coverage
|
||||
|
||||
The test suite covers:
|
||||
|
||||
* Modal creation and interaction
|
||||
* Translation functionality
|
||||
* TTS processing
|
||||
* Storage operations
|
||||
* UI theme detection
|
||||
* Language system functionality
|
||||
|
||||
Production Deployment
|
||||
---------------------
|
||||
|
||||
1. Serve files with proper MIME types
|
||||
2. Enable CORS for module scripts
|
||||
3. Use HTTPS (required for some features)
|
||||
4. Set cache headers appropriately
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
**Modal not appearing:**
|
||||
|
||||
* Check browser console for errors
|
||||
* Ensure JavaScript modules are supported
|
||||
* Verify CORS headers are set correctly
|
||||
|
||||
**Translation/TTS not working:**
|
||||
|
||||
* Check network tab for API errors
|
||||
* Ensure endpoints are accessible
|
||||
* Verify API key is set (if using custom endpoints)
|
||||
|
||||
**Styling issues:**
|
||||
|
||||
* Set ``UNCLOSEAI_CUSTOM_STYLING = false`` for pure PicoCSS
|
||||
* Check for CSS conflicts with page styles
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
This project is released into the public domain under CC0 1.0 Universal.
|
||||
|
||||
See: https://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
About
|
||||
-----
|
||||
|
||||
uncloseai. is part of the unturf. project, creating open tools for an open web. It's built by fxhp and the carnival hackers who believe AI should be accessible to everyone.
|
||||
|
||||
**Why "uncloseai."?**
|
||||
|
||||
The name represents keeping AI "unclosed" - open, accessible, and free for all to use and modify.
|
||||
|
||||
Links
|
||||
-----
|
||||
|
||||
* **Homepage**: https://uncloseai.com
|
||||
* **Repository**: https://git.unturf.com/engineering/unturf/uncloseai.com
|
||||
* **Demo**: https://uncloseai.com/demo
|
||||
* **Hermes Model**: https://nousresearch.com/hermes3/
|
||||
|
||||
Acknowledgments
|
||||
---------------
|
||||
|
||||
* Nous Research for the amazing Hermes 3 model
|
||||
* The open-source community for the libraries we build upon
|
||||
* Everyone who believes in an open, accessible internet
|
||||
|
||||
Remember: This is your AI assistant. Make it yours!
|
||||
60
biome.json
Normal file
60
biome.json
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||
"files": {
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/css/pico.classless.min.css",
|
||||
"!**/node_modules",
|
||||
"!**/*.min.js",
|
||||
"!**/*.min.css",
|
||||
"!**/uncloseai-bundle.js"
|
||||
]
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noAssignInExpressions": "off"
|
||||
},
|
||||
"style": {
|
||||
"useTemplate": "warn",
|
||||
"noParameterAssign": "error",
|
||||
"useAsConstAssertion": "error",
|
||||
"useDefaultParameterLast": "error",
|
||||
"useEnumInitializers": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noUselessElse": "error"
|
||||
},
|
||||
"complexity": {
|
||||
"useOptionalChain": "warn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80,
|
||||
"lineEnding": "lf",
|
||||
"includes": ["**", "!**/css/pico.classless.min.css"]
|
||||
},
|
||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double",
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/*! Generated by Font Squirrel (https://www.fontsquirrel.com) on October 4, 2021 */
|
||||
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'chunkfiveregular';
|
||||
src: url('chunkfive-regular-webfont.woff2') format('woff2'),
|
||||
url('chunkfive-regular-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
4
css/pico.classless.min.css
vendored
4
css/pico.classless.min.css
vendored
File diff suppressed because one or more lines are too long
79
demo.html
79
demo.html
|
|
@ -1,79 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Demo Free LLM & Text To Speech Artificial Intelligence Service | ai.unturf.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
<script defer data-domain="ai.unturf.com" src="https://analytics.unturf.com/js/plausible.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://ai.unturf.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">unturf.</h1></a>
|
||||
<p>Welcome to ai.unturf.com - Free LLM & Text To Speech Artificial Intelligence Service</title>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<h2>Demo!</h2>
|
||||
|
||||
<div id="chat-container">
|
||||
<div id="chat-box"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div>
|
||||
<input type="text" id="user-input" placeholder="Ask about this page...">
|
||||
<button onclick="handleUserInput()">Send</button>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
868
docs/VAULT_SYNC_DESIGN.md
Normal file
868
docs/VAULT_SYNC_DESIGN.md
Normal file
|
|
@ -0,0 +1,868 @@
|
|||
# Vault Sync Design Document
|
||||
|
||||
Cross-site settings synchronization via optional uncloseai.com account linking.
|
||||
|
||||
**Status:** Proposed
|
||||
**Author:** Hermes Staff
|
||||
**Date:** 2026-01-23
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
### Current State
|
||||
|
||||
The UncloseAI widget uses a per-site encrypted vault stored in localStorage (`vault.js`). Each site embedding the widget has its own isolated vault:
|
||||
|
||||
- User sets up vault on site-a.com with password
|
||||
- Settings encrypted with AES-256, stored in localStorage
|
||||
- Session persistence (7-day TTL) for convenience
|
||||
- No data leaves the user's browser
|
||||
|
||||
### Proposed Enhancement
|
||||
|
||||
Add optional cross-site sync via uncloseai.com accounts. Users who want their settings everywhere can link their vault to an account. Users who prefer local-only storage continue as before.
|
||||
|
||||
### Strategic Goal: Viral Spreading
|
||||
|
||||
Users who enjoy the widget on one site become advocates:
|
||||
1. They want it on every site they visit
|
||||
2. They recommend it to site owners
|
||||
3. Site owners embed the widget to retain users
|
||||
4. More users discover the widget
|
||||
5. Cycle repeats
|
||||
|
||||
Cross-site sync is the catalyst that transforms one-site users into multi-site evangelists.
|
||||
|
||||
---
|
||||
|
||||
## User Flow
|
||||
|
||||
### Flow 1: First-Time User (Local Only)
|
||||
|
||||
```
|
||||
1. User visits site-a.com (first time with widget)
|
||||
2. Widget prompts: "Create a secure vault?"
|
||||
3. User creates vault with password
|
||||
4. Settings stored locally in localStorage
|
||||
5. Widget works normally (local vault only)
|
||||
```
|
||||
|
||||
### Flow 2: Local User Sees Sync Prompt
|
||||
|
||||
```
|
||||
1. User has been using widget on site-a.com for a while
|
||||
2. After N sessions OR manual settings access, show prompt:
|
||||
"Sync your settings across all sites?"
|
||||
[Link Account] [Not Now] [Don't Ask Again]
|
||||
3. If "Not Now": dismiss, ask again after more sessions
|
||||
4. If "Don't Ask Again": store preference, never ask again
|
||||
5. If "Link Account": proceed to account linking
|
||||
```
|
||||
|
||||
**Trigger conditions for sync prompt:**
|
||||
- User has opened settings modal at least 3 times
|
||||
- User has been using widget for at least 7 days
|
||||
- User has not dismissed "Don't Ask Again"
|
||||
- User does not already have a linked account
|
||||
|
||||
### Flow 3: Account Linking
|
||||
|
||||
```
|
||||
1. User clicks "Link Account"
|
||||
2. Modal shows options:
|
||||
- "Sign in with email (magic link)"
|
||||
- "Sign in with GitHub" (optional OAuth)
|
||||
- "Sign in with Google" (optional OAuth)
|
||||
3. User chooses email magic link
|
||||
4. User enters email address
|
||||
5. Email sent with one-time login link
|
||||
6. User clicks link, opens uncloseai.com/auth/verify?token=xxx
|
||||
7. Token validated, session cookie set
|
||||
8. Redirect back to original site with auth token in URL fragment
|
||||
9. Widget detects auth token, stores sync credentials
|
||||
10. Widget uploads encrypted vault to server
|
||||
11. Success message: "Settings will sync across all sites!"
|
||||
```
|
||||
|
||||
### Flow 4: Returning User with Synced Account
|
||||
|
||||
```
|
||||
1. User visits site-b.com (new site, same widget)
|
||||
2. Widget detects no local vault
|
||||
3. Widget checks for sync credentials (stored in localStorage)
|
||||
4. If credentials exist:
|
||||
a. Widget fetches encrypted vault from server
|
||||
b. User prompted for vault password (still needed for decryption)
|
||||
c. Settings decrypted locally, vault initialized
|
||||
5. User has all their settings without manual setup
|
||||
```
|
||||
|
||||
### Flow 5: Conflict Resolution
|
||||
|
||||
```
|
||||
1. User has synced account, uses widget on site-a.com
|
||||
2. User changes setting (e.g., switches model)
|
||||
3. Widget uploads new encrypted vault to server
|
||||
4. User simultaneously on site-b.com (second tab/device)
|
||||
5. site-b.com widget has stale local copy
|
||||
6. On next interaction, widget pulls from server
|
||||
7. Conflict detected: local timestamp != server timestamp
|
||||
8. Resolution: Last-write-wins (server version)
|
||||
9. User sees toast: "Settings updated from another session"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Design
|
||||
|
||||
### Server-Side (uncloseai.com API)
|
||||
|
||||
#### Database Schema
|
||||
|
||||
```sql
|
||||
-- Users table (for accounts)
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
email_verified BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- OAuth connections (optional)
|
||||
CREATE TABLE oauth_connections (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider VARCHAR(50) NOT NULL, -- 'github', 'google'
|
||||
provider_user_id VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(provider, provider_user_id)
|
||||
);
|
||||
|
||||
-- Magic link tokens
|
||||
CREATE TABLE magic_links (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) NOT NULL,
|
||||
token_hash VARCHAR(64) NOT NULL, -- SHA-256 of token
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
used BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Encrypted vaults (stores ciphertext only)
|
||||
CREATE TABLE vaults (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE,
|
||||
encrypted_data TEXT NOT NULL, -- AES-encrypted JSON blob
|
||||
version INTEGER DEFAULT 1,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- API keys for widget authentication
|
||||
CREATE TABLE api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
key_hash VARCHAR(64) NOT NULL, -- SHA-256 of API key
|
||||
name VARCHAR(100),
|
||||
last_used_at TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
```
|
||||
POST /api/auth/magic-link
|
||||
Request: { "email": "user@example.com" }
|
||||
Response: { "success": true, "message": "Check your email" }
|
||||
Action: Generate token, send email with link to /auth/verify?token=xxx
|
||||
|
||||
GET /api/auth/verify?token=xxx
|
||||
Response: { "success": true, "api_key": "vk_xxx...", "user_id": "uuid" }
|
||||
Action: Validate token, create/get user, generate API key
|
||||
Notes: API key returned only once, user must save it
|
||||
|
||||
POST /api/auth/oauth/github (optional)
|
||||
OAuth callback handler for GitHub
|
||||
|
||||
POST /api/auth/oauth/google (optional)
|
||||
OAuth callback handler for Google
|
||||
|
||||
POST /api/vault/sync
|
||||
Headers: Authorization: Bearer vk_xxx...
|
||||
Request: { "encrypted_data": "...", "version": 2 }
|
||||
Response: { "success": true, "version": 2, "updated_at": "..." }
|
||||
Action: Store encrypted blob, increment version if changed
|
||||
Notes: Server NEVER decrypts - stores ciphertext only
|
||||
|
||||
GET /api/vault/sync
|
||||
Headers: Authorization: Bearer vk_xxx...
|
||||
Response: { "encrypted_data": "...", "version": 2, "updated_at": "..." }
|
||||
Action: Return encrypted blob
|
||||
Notes: Returns 404 if no vault exists for this user
|
||||
|
||||
DELETE /api/vault/sync
|
||||
Headers: Authorization: Bearer vk_xxx...
|
||||
Response: { "success": true }
|
||||
Action: Delete vault (account remains)
|
||||
|
||||
GET /api/vault/version
|
||||
Headers: Authorization: Bearer vk_xxx...
|
||||
Response: { "version": 2, "updated_at": "..." }
|
||||
Action: Quick check for sync (lightweight polling)
|
||||
|
||||
DELETE /api/account
|
||||
Headers: Authorization: Bearer vk_xxx...
|
||||
Response: { "success": true }
|
||||
Action: Delete account and all associated data
|
||||
```
|
||||
|
||||
#### Rate Limiting
|
||||
|
||||
```
|
||||
/api/auth/magic-link: 5 requests per email per hour
|
||||
/api/vault/sync POST: 60 requests per minute per user
|
||||
/api/vault/sync GET: 120 requests per minute per user
|
||||
/api/vault/version: 300 requests per minute per user
|
||||
```
|
||||
|
||||
### Client-Side (vault.js Extensions)
|
||||
|
||||
#### New Constants
|
||||
|
||||
```javascript
|
||||
const UncloseVault = {
|
||||
// ... existing constants ...
|
||||
|
||||
// Sync-related localStorage keys
|
||||
SYNC_KEY: 'uncloseai_sync_credentials',
|
||||
SYNC_PROMPT_KEY: 'uncloseai_sync_prompt_state',
|
||||
|
||||
// API base URL
|
||||
SYNC_API_BASE: 'https://uncloseai.com/api',
|
||||
|
||||
// Sync check interval (5 minutes)
|
||||
SYNC_POLL_INTERVAL_MS: 5 * 60 * 1000,
|
||||
|
||||
// ... rest of existing code ...
|
||||
};
|
||||
```
|
||||
|
||||
#### New Methods
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Check if sync is enabled and credentials exist
|
||||
*/
|
||||
isSyncEnabled() {
|
||||
const creds = this.getSyncCredentials();
|
||||
return creds !== null && creds.apiKey !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored sync credentials
|
||||
*/
|
||||
getSyncCredentials() {
|
||||
try {
|
||||
const json = localStorage.getItem(this.SYNC_KEY);
|
||||
return json ? JSON.parse(json) : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store sync credentials after successful auth
|
||||
*/
|
||||
setSyncCredentials(apiKey, userId) {
|
||||
localStorage.setItem(this.SYNC_KEY, JSON.stringify({
|
||||
apiKey,
|
||||
userId,
|
||||
linkedAt: new Date().toISOString()
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear sync credentials (unlink account)
|
||||
*/
|
||||
clearSyncCredentials() {
|
||||
localStorage.removeItem(this.SYNC_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push local vault to server
|
||||
* Call after any local save() if sync is enabled
|
||||
*/
|
||||
async sync() {
|
||||
if (!this.isSyncEnabled() || !this.isUnlocked()) {
|
||||
return { success: false, error: 'Sync not available' };
|
||||
}
|
||||
|
||||
const creds = this.getSyncCredentials();
|
||||
const vault = this.getVault();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.SYNC_API_BASE}/vault/sync`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${creds.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
encrypted_data: vault.encrypted_settings,
|
||||
version: vault.version || 1
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// API key invalid, clear credentials
|
||||
this.clearSyncCredentials();
|
||||
return { success: false, error: 'Session expired' };
|
||||
}
|
||||
return { success: false, error: 'Sync failed' };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Update local version
|
||||
vault.version = data.version;
|
||||
vault.synced_at = data.updated_at;
|
||||
this.saveVault(vault);
|
||||
|
||||
return { success: true, version: data.version };
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull vault from server
|
||||
* Returns encrypted data that still needs password to decrypt
|
||||
*/
|
||||
async pull() {
|
||||
if (!this.isSyncEnabled()) {
|
||||
return { success: false, error: 'Sync not enabled' };
|
||||
}
|
||||
|
||||
const creds = this.getSyncCredentials();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.SYNC_API_BASE}/vault/sync`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${creds.apiKey}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return { success: true, data: null }; // No vault on server yet
|
||||
}
|
||||
if (response.status === 401) {
|
||||
this.clearSyncCredentials();
|
||||
return { success: false, error: 'Session expired' };
|
||||
}
|
||||
return { success: false, error: 'Pull failed' };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
encrypted_settings: data.encrypted_data,
|
||||
version: data.version,
|
||||
updated_at: data.updated_at
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if server has newer version
|
||||
*/
|
||||
async checkForUpdates() {
|
||||
if (!this.isSyncEnabled()) return false;
|
||||
|
||||
const creds = this.getSyncCredentials();
|
||||
const vault = this.getVault();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.SYNC_API_BASE}/vault/version`, {
|
||||
headers: { 'Authorization': `Bearer ${creds.apiKey}` }
|
||||
});
|
||||
|
||||
if (!response.ok) return false;
|
||||
|
||||
const data = await response.json();
|
||||
return data.version > (vault?.version || 0);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge server vault with local (last-write-wins)
|
||||
*/
|
||||
async syncFromServer(password) {
|
||||
const result = await this.pull();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Try to decrypt with provided password
|
||||
const decrypted = this.decrypt(result.data.encrypted_settings, password);
|
||||
if (!decrypted) {
|
||||
return { success: false, error: 'Wrong password' };
|
||||
}
|
||||
|
||||
// Replace local vault with server version
|
||||
const vault = {
|
||||
encrypted_settings: result.data.encrypted_settings,
|
||||
version: result.data.version,
|
||||
synced_at: result.data.updated_at,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.saveVault(vault);
|
||||
this._settings = decrypted;
|
||||
this._password = password;
|
||||
|
||||
window.dispatchEvent(new CustomEvent('uncloseai-vault-synced'));
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
#### Modified save() Method
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Save current settings to vault (modified for sync)
|
||||
*/
|
||||
async save() {
|
||||
if (!this.isUnlocked()) {
|
||||
console.error('Cannot save: vault not unlocked');
|
||||
return false;
|
||||
}
|
||||
|
||||
const vault = this.getVault() || {};
|
||||
const encrypted = this.encrypt(this._settings, this._password);
|
||||
|
||||
if (!encrypted) {
|
||||
console.error('Failed to encrypt settings');
|
||||
return false;
|
||||
}
|
||||
|
||||
vault.encrypted_settings = encrypted;
|
||||
vault.updated_at = new Date().toISOString();
|
||||
vault.version = (vault.version || 0) + 1;
|
||||
this.saveVault(vault);
|
||||
|
||||
// Auto-sync if enabled (non-blocking)
|
||||
if (this.isSyncEnabled()) {
|
||||
this.sync().catch(e => console.warn('Sync failed:', e));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Privacy and Zero-Knowledge Architecture
|
||||
|
||||
### Core Principle
|
||||
|
||||
**The server NEVER has the ability to decrypt user data.**
|
||||
|
||||
```
|
||||
User's Browser uncloseai.com Server
|
||||
| |
|
||||
| 1. User enters password |
|
||||
| (never leaves browser) |
|
||||
| |
|
||||
| 2. Settings encrypted with |
|
||||
| password using AES-256 |
|
||||
| |
|
||||
| 3. Encrypted blob sent to server |
|
||||
| POST /vault/sync |
|
||||
|----------------------------------->|
|
||||
| | 4. Server stores
|
||||
| | encrypted blob
|
||||
| | (ciphertext only)
|
||||
| |
|
||||
| 5. On new device, encrypted |
|
||||
| blob retrieved |
|
||||
|<-----------------------------------|
|
||||
| |
|
||||
| 6. User enters password |
|
||||
| (decryption happens locally) |
|
||||
| |
|
||||
```
|
||||
|
||||
### What the Server Stores
|
||||
|
||||
```
|
||||
{
|
||||
"user_id": "uuid",
|
||||
"encrypted_data": "U2FsdGVkX1+...(opaque ciphertext)...",
|
||||
"version": 3,
|
||||
"updated_at": "2026-01-23T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### What the Server CANNOT See
|
||||
|
||||
- User's vault password
|
||||
- Decrypted settings (API keys, model preferences, etc.)
|
||||
- Any plaintext user data
|
||||
|
||||
### Implications
|
||||
|
||||
1. **Password recovery is impossible** - If user forgets password, their synced vault is permanently inaccessible. This is a feature, not a bug.
|
||||
|
||||
2. **No password reset** - Account password reset would require vault re-encryption, which requires the old password.
|
||||
|
||||
3. **User responsibility** - Users must remember their vault password. We provide clear warnings during setup.
|
||||
|
||||
---
|
||||
|
||||
## Viral Mechanics
|
||||
|
||||
### "Powered by UncloseAI" Badge
|
||||
|
||||
Optional badge shown in widget corner:
|
||||
|
||||
```
|
||||
[ Settings ] [Voice] [Model] Powered by UncloseAI
|
||||
```
|
||||
|
||||
Badge behavior:
|
||||
- Shown only if site owner enables it (opt-in for site owners)
|
||||
- Or shown if user has free account (non-paying users get badge)
|
||||
- Clicking opens uncloseai.com in new tab
|
||||
- Badge can be hidden by site owners who pay/self-host
|
||||
|
||||
### "Get This Widget" Link
|
||||
|
||||
In settings modal footer:
|
||||
|
||||
```
|
||||
------------------------------------------
|
||||
| Want this AI on your website? |
|
||||
| [Add to Your Site] - Free, 2 min setup |
|
||||
------------------------------------------
|
||||
```
|
||||
|
||||
Link goes to: `uncloseai.com/embed?ref={site_domain}`
|
||||
|
||||
Referral tracking:
|
||||
- Store referring domain in analytics
|
||||
- Site owner gets credit when their users convert
|
||||
- Future: referral rewards (free premium features)
|
||||
|
||||
### Share Settings Link
|
||||
|
||||
Users can share their vault (encrypted) via URL:
|
||||
|
||||
```javascript
|
||||
async generateShareLink() {
|
||||
if (!this.isUnlocked()) return null;
|
||||
|
||||
// Create one-time export
|
||||
const exportData = {
|
||||
settings: this._settings,
|
||||
exported_at: new Date().toISOString(),
|
||||
// Exclude sensitive fields
|
||||
exclude: ['customAPIKey', 'unsandboxSecretKey']
|
||||
};
|
||||
|
||||
const filtered = { ...exportData.settings };
|
||||
exportData.exclude.forEach(k => delete filtered[k]);
|
||||
|
||||
// Encrypt with random key
|
||||
const shareKey = CryptoJS.lib.WordArray.random(16).toString();
|
||||
const encrypted = this.encrypt(filtered, shareKey);
|
||||
|
||||
// Encode in URL
|
||||
const payload = btoa(JSON.stringify({ e: encrypted }));
|
||||
return `https://uncloseai.com/share#${shareKey}:${payload}`;
|
||||
}
|
||||
```
|
||||
|
||||
Recipient flow:
|
||||
1. Opens share link
|
||||
2. Page decrypts settings from URL fragment (client-side only)
|
||||
3. Shows preview of shared settings
|
||||
4. User can import into their own vault
|
||||
|
||||
### Referral Program (Future)
|
||||
|
||||
Site owner rewards:
|
||||
- Each site owner gets referral code
|
||||
- When new site embeds widget via referral, both get benefits
|
||||
- Tiered rewards: 5 referrals = X, 10 referrals = Y
|
||||
|
||||
User rewards:
|
||||
- Users who share widget and drive signups get:
|
||||
- Extended session duration
|
||||
- Priority model access
|
||||
- Early access to new features
|
||||
|
||||
---
|
||||
|
||||
## MVP vs Full Version
|
||||
|
||||
### MVP (Phase 1)
|
||||
|
||||
**Goal:** Prove the sync concept works and users want it.
|
||||
|
||||
**Scope:**
|
||||
- Email magic link authentication only
|
||||
- Basic vault sync (push/pull)
|
||||
- Last-write-wins conflict resolution
|
||||
- "Powered by UncloseAI" badge
|
||||
- "Get This Widget" link
|
||||
|
||||
**Not in MVP:**
|
||||
- OAuth (GitHub, Google)
|
||||
- Share settings link
|
||||
- Referral program
|
||||
- Analytics for site owners
|
||||
- Paid tiers
|
||||
|
||||
**Timeline:** 2-3 weeks
|
||||
|
||||
**Success metrics:**
|
||||
- 100+ accounts created in first month
|
||||
- 20+ cross-site sync users (used on 2+ domains)
|
||||
- 10+ new site embeds from "Get This Widget"
|
||||
|
||||
### Full Version (Phase 2)
|
||||
|
||||
**Goal:** Scale viral mechanics and add monetization.
|
||||
|
||||
**Scope:**
|
||||
- OAuth providers (GitHub, Google)
|
||||
- Share settings link with preview
|
||||
- Referral program with tracking
|
||||
- Site owner analytics dashboard
|
||||
- Paid tiers (remove badge, priority support)
|
||||
- Real-time sync (WebSocket)
|
||||
- Multi-device session management
|
||||
|
||||
**Timeline:** 2-3 months after MVP validation
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### End-to-End Encryption
|
||||
|
||||
- All encryption/decryption happens in browser using CryptoJS
|
||||
- Server stores only ciphertext
|
||||
- No server-side key escrow
|
||||
- No ability to decrypt without user's password
|
||||
|
||||
### Password Requirements
|
||||
|
||||
- Minimum 8 characters (enforced client-side)
|
||||
- Encourage but don't require complexity
|
||||
- Show strength meter in UI
|
||||
- Clear warning: "This password cannot be recovered"
|
||||
|
||||
### Account Recovery
|
||||
|
||||
**Scenario: User forgets vault password**
|
||||
|
||||
Options:
|
||||
1. **Delete and restart** - User can delete their synced vault and create new one. Loses all settings.
|
||||
2. **Local fallback** - If local vault still works (different password), offer to re-sync from local.
|
||||
3. **No recovery** - By design. Zero-knowledge means no backdoor.
|
||||
|
||||
UI messaging:
|
||||
```
|
||||
Forgot your vault password?
|
||||
|
||||
Your vault is encrypted and cannot be recovered without your password.
|
||||
This is a security feature, not a bug.
|
||||
|
||||
Options:
|
||||
[ Delete Vault & Start Fresh ] - Lose all synced settings
|
||||
[ Cancel ] - Try to remember your password
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Prevent brute force and abuse:
|
||||
|
||||
```
|
||||
Endpoint Limit
|
||||
------------------------------------------
|
||||
POST /auth/magic-link 5/hour/email
|
||||
POST /vault/sync 60/minute/user
|
||||
GET /vault/sync 120/minute/user
|
||||
GET /vault/version 300/minute/user
|
||||
POST /auth/verify 10/minute/IP
|
||||
```
|
||||
|
||||
### API Key Security
|
||||
|
||||
- API keys prefixed with `vk_` for easy identification
|
||||
- Keys are 32 bytes of random data (256 bits)
|
||||
- Stored hashed (SHA-256) in database
|
||||
- Keys can be revoked from account settings
|
||||
- Keys have optional expiration
|
||||
|
||||
### CORS Policy
|
||||
|
||||
```
|
||||
Access-Control-Allow-Origin: *
|
||||
```
|
||||
|
||||
Widget runs on any domain, so CORS must be permissive. Security comes from:
|
||||
- API key authentication
|
||||
- Rate limiting
|
||||
- E2E encryption (server can't read data anyway)
|
||||
|
||||
### Magic Link Security
|
||||
|
||||
- Tokens are 32 bytes of random data
|
||||
- Tokens expire after 15 minutes
|
||||
- Tokens are single-use
|
||||
- Token hash stored in database (not plaintext)
|
||||
- Email delivery via trusted provider (SendGrid, Postmark)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Server-Side
|
||||
|
||||
```
|
||||
[ ] Database schema (PostgreSQL)
|
||||
[ ] User model and migrations
|
||||
[ ] Magic link generation and verification
|
||||
[ ] Email sending integration (SendGrid)
|
||||
[ ] Vault CRUD endpoints
|
||||
[ ] API key generation and validation
|
||||
[ ] Rate limiting middleware
|
||||
[ ] CORS configuration
|
||||
[ ] Error handling and logging
|
||||
[ ] Health check endpoint
|
||||
```
|
||||
|
||||
### Client-Side
|
||||
|
||||
```
|
||||
[ ] Sync credentials storage
|
||||
[ ] sync() method
|
||||
[ ] pull() method
|
||||
[ ] checkForUpdates() method
|
||||
[ ] Modified save() with auto-sync
|
||||
[ ] Auth flow UI (modal)
|
||||
[ ] "Link Account" prompt logic
|
||||
[ ] "Powered by UncloseAI" badge
|
||||
[ ] "Get This Widget" link
|
||||
[ ] Sync status indicator
|
||||
[ ] Error handling and retry logic
|
||||
[ ] Offline queue for sync operations
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```
|
||||
[ ] Unit tests for sync methods
|
||||
[ ] Integration tests with mock server
|
||||
[ ] E2E test: full account linking flow
|
||||
[ ] E2E test: cross-site sync
|
||||
[ ] E2E test: conflict resolution
|
||||
[ ] Security audit: encryption verification
|
||||
[ ] Load testing: rate limits
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
```
|
||||
[ ] User guide: setting up sync
|
||||
[ ] User guide: what happens if I forget my password
|
||||
[ ] Site owner guide: embedding options
|
||||
[ ] API documentation (for advanced users)
|
||||
[ ] Privacy policy updates
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Should vault password be separate from account password?**
|
||||
- Current design: Yes, vault has its own password
|
||||
- Alternative: Use account password for vault (simpler UX, less secure)
|
||||
|
||||
2. **How to handle vault password change?**
|
||||
- Need to re-encrypt and sync new ciphertext
|
||||
- What if user is on multiple devices during password change?
|
||||
|
||||
3. **Should we support multiple vaults per account?**
|
||||
- Use case: Different settings for work vs personal
|
||||
- Complexity: Significant, defer to Phase 2
|
||||
|
||||
4. **Real-time sync vs polling?**
|
||||
- MVP: Polling (simpler)
|
||||
- Phase 2: WebSocket for instant sync
|
||||
|
||||
5. **What settings should be excluded from sync?**
|
||||
- Definitely exclude: Nothing by default
|
||||
- User choice: Let users select what syncs
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Email Templates
|
||||
|
||||
### Magic Link Email
|
||||
|
||||
```
|
||||
Subject: Sign in to UncloseAI
|
||||
|
||||
Hi there,
|
||||
|
||||
Click the link below to sign in to your UncloseAI account:
|
||||
|
||||
[Sign In] - https://uncloseai.com/auth/verify?token=xxx
|
||||
|
||||
This link expires in 15 minutes and can only be used once.
|
||||
|
||||
If you didn't request this, you can safely ignore this email.
|
||||
|
||||
- The UncloseAI Team
|
||||
```
|
||||
|
||||
### Welcome Email (After First Sync)
|
||||
|
||||
```
|
||||
Subject: Your settings are now synced!
|
||||
|
||||
Welcome to UncloseAI!
|
||||
|
||||
Your vault is now synced across all sites. Here's what you can do:
|
||||
|
||||
1. Visit any website with the UncloseAI widget
|
||||
2. Click "Sync Settings" in the settings menu
|
||||
3. Enter your vault password
|
||||
4. Your preferences load automatically!
|
||||
|
||||
Remember: Your vault password is never sent to our servers.
|
||||
If you forget it, your synced data cannot be recovered.
|
||||
|
||||
Need help? Reply to this email.
|
||||
|
||||
- The UncloseAI Team
|
||||
```
|
||||
52
docs/tickets/001-extension-fails-to-load.md
Normal file
52
docs/tickets/001-extension-fails-to-load.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# 001: Extension fails to load on certain pages
|
||||
|
||||
**Reporter:** cthegray
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** high
|
||||
**Status:** fixed
|
||||
**Affects:** Browser extension (Chrome/Safari)
|
||||
|
||||
## Description
|
||||
|
||||
The uncloseai browser extension does not load at all on some pages. No UI appears, no sidebar, nothing.
|
||||
|
||||
## Reproduction
|
||||
|
||||
**Fails to load on:**
|
||||
- reddit.com (any page)
|
||||
- slashdot.org
|
||||
- cnn.com (main/index page only)
|
||||
|
||||
**Works on:**
|
||||
- cnn.com article pages (individual stories)
|
||||
|
||||
## Analysis
|
||||
|
||||
The pattern suggests Content Security Policy (CSP) or iframe sandboxing on these domains is blocking the extension from injecting. Reddit and Slashdot both use strict CSPs. CNN's main page likely has different CSP headers than article pages.
|
||||
|
||||
Possible causes:
|
||||
- CSP `script-src` blocking inline or extension scripts
|
||||
- CSP `frame-src` or `frame-ancestors` blocking extension iframe/overlay
|
||||
- Shadow DOM or aggressive DOM manipulation interfering with injection
|
||||
- Extension manifest permissions not covering these domains
|
||||
|
||||
## Root Cause
|
||||
|
||||
The browser-toys extension injects `<script src="https://uncloseai.com/uncloseai.js" type="module">` via DOM. Pages with strict CSP `script-src` directives block this because `https://uncloseai.com` is not in their allowlist.
|
||||
|
||||
## Fix
|
||||
|
||||
Implemented three-tier injection strategy in `uncloseai-browser-toys`:
|
||||
|
||||
1. **Strategy 1 (CSP-safe):** Content script messages the background service worker, which calls `chrome.scripting.executeScript({ world: "MAIN", files: ["uncloseai-bundle.js"] })`. This injects directly into the page world via the browser engine, bypassing all page CSP restrictions. Requires `"scripting"` permission (added to Chrome and Safari MV3 manifests).
|
||||
|
||||
2. **Strategy 2 (script tag):** Fallback for MV2 browsers (Firefox). Creates a `<script>` tag with `src` pointing to the extension-origin bundle. Works on most pages since browsers whitelist extension origins in CSP.
|
||||
|
||||
3. **Strategy 3 (CDN bootstrap):** Last resort if extension resources fail. Inlines a bootstrap that loads from `uncloseai.com/uncloseai.js`.
|
||||
|
||||
**Files changed in browser-toys:**
|
||||
- `shared/content.js`: Rewrote injection to try background message first, then script tag, then CDN
|
||||
- `shared/background.js`: Added `onMessage` listener for `{ action: "inject" }` using `chrome.scripting.executeScript`
|
||||
- `extensions/chrome/manifest.json`: Added `"scripting"` permission
|
||||
- `extensions/safari/manifest.json`: Added `"scripting"` permission
|
||||
- `extensions/firefox/manifest.json`: No changes needed (MV2, uses Strategy 2)
|
||||
31
docs/tickets/002-tts-stops-after-first-sentence.md
Normal file
31
docs/tickets/002-tts-stops-after-first-sentence.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# 002: TTS stops after first sentence
|
||||
|
||||
**Reporter:** cthegray
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** high
|
||||
**Status:** open
|
||||
**Affects:** TTS playback, mobile and desktop
|
||||
|
||||
## Description
|
||||
|
||||
Text-to-speech only reads the first sentence of a response, then stops. Sometimes reads two sentences. Happens on both mobile (iPhone/Safari) and desktop (macOS/Safari).
|
||||
|
||||
## Reproduction
|
||||
|
||||
1. Send a message that generates a multi-sentence response
|
||||
2. Tap the TTS/speaker icon on the message
|
||||
3. TTS plays the first sentence (occasionally two)
|
||||
4. Audio stops, remaining text is never spoken
|
||||
|
||||
## Analysis
|
||||
|
||||
Possible causes:
|
||||
- TTS input is being truncated before sending to the speech endpoint
|
||||
- The TTS API has a character/token limit and the client is not chunking the text
|
||||
- Only the first SSE chunk's content is being sent to TTS instead of the full accumulated response
|
||||
- Audio playback queue is not chaining subsequent audio segments
|
||||
- Browser autoplay policy may be blocking follow-up audio segments (especially on mobile Safari)
|
||||
|
||||
## Notes
|
||||
|
||||
If the TTS API has a max input length, the client needs to split text into chunks, generate audio for each, and queue playback sequentially.
|
||||
32
docs/tickets/003-audio-icon-freezes-window.md
Normal file
32
docs/tickets/003-audio-icon-freezes-window.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# 003: Audio icon freezes entire window
|
||||
|
||||
**Reporter:** cthegray
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** critical
|
||||
**Status:** open
|
||||
**Affects:** iPhone (Safari, latest iOS), macOS (Safari, latest macOS)
|
||||
|
||||
## Description
|
||||
|
||||
Tapping the audio icon in the bottom bar (the global one, not the per-message TTS icon) causes the entire uncloseai window to freeze. Nothing responds after this: buttons, scrolling, even the close (X) button. Complete UI lockup.
|
||||
|
||||
## Reproduction
|
||||
|
||||
1. Open uncloseai in Safari (iPhone or macOS)
|
||||
2. Have a conversation with at least one response visible
|
||||
3. Tap the audio icon in the bottom navigation bar
|
||||
4. Window freezes completely
|
||||
5. Cannot interact with anything, cannot close the panel
|
||||
|
||||
## Analysis
|
||||
|
||||
This is a main-thread blocking issue. Possible causes:
|
||||
- Synchronous API call on the main thread (blocking fetch or XHR without async)
|
||||
- Infinite loop in audio initialization or playback setup
|
||||
- Unresolved promise or callback deadlock
|
||||
- AudioContext creation failing silently and triggering a retry loop
|
||||
- Safari-specific Web Audio API quirk (Safari requires user gesture to resume AudioContext, failure to handle this could cause a hang)
|
||||
|
||||
## Notes
|
||||
|
||||
This is the highest priority issue: a complete UI freeze with no recovery path is a showstopper. The user has to force-close the tab/app. Needs investigation into what the bottom-bar audio icon's click handler does differently from the per-message TTS icon.
|
||||
44
docs/tickets/004-working-indicators.md
Normal file
44
docs/tickets/004-working-indicators.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# 004: Add working/spinning indicators for async operations
|
||||
|
||||
**Reporter:** cthegray
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** medium
|
||||
**Status:** open
|
||||
**Affects:** All platforms, UX
|
||||
|
||||
## Description
|
||||
|
||||
Icons that trigger async operations (TTS generation, page reading, etc.) give no visual feedback that work is in progress. The user taps and nothing visibly happens until the operation completes (or fails silently).
|
||||
|
||||
The book icon for "page read" already has an hourglass indicator. Other icons need the same treatment.
|
||||
|
||||
## Requirements
|
||||
|
||||
- TTS icon (per-message): show spinning/pulsing animation while audio is being generated
|
||||
- Audio icon (bottom bar): show spinning/pulsing animation while audio is loading
|
||||
- Any other async operation icons should show a working state
|
||||
- Animation should stop when the operation completes or errors
|
||||
- On error, briefly show an error state (red flash, shake, etc.) before returning to idle
|
||||
|
||||
## Design Notes
|
||||
|
||||
Keep consistent with the existing hourglass pattern on the book icon. Options:
|
||||
- CSS spin animation on the icon itself
|
||||
- Small spinner overlay on the icon
|
||||
- Icon swap to an animated version during loading
|
||||
- Pulsing opacity animation (subtle but effective)
|
||||
|
||||
## Implementation
|
||||
|
||||
CSS animations added to both theme files (`uncloseai-modal-builtin.css`, `uncloseai-modal-pico.css`):
|
||||
- `.working` class: pulse animation (opacity 1 to 0.4), disables pointer events
|
||||
- `.uncloseai-spinner` class: continuous rotation for inline spinner elements
|
||||
- Apply to `.uncloseai-btn-small` or `.uncloseai-btn-primary` elements
|
||||
|
||||
Usage: `btn.classList.add("working")` when async starts, `.remove("working")` when done.
|
||||
|
||||
TTS buttons (primary consumers) are currently hidden via feature flag. When TTS is re-enabled, wire the `.working` class into the TTS button click handlers.
|
||||
|
||||
## Notes
|
||||
|
||||
CSS is ready. JS wiring deferred until TTS is re-enabled (tickets #002, #003).
|
||||
52
docs/tickets/005-context-overflow-silent-failure.md
Normal file
52
docs/tickets/005-context-overflow-silent-failure.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# 005: Context overflow causes blank or hallucinated responses
|
||||
|
||||
**Reporter:** cthegray
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** high
|
||||
**Status:** open
|
||||
**Affects:** All platforms, chat quality
|
||||
|
||||
## Description
|
||||
|
||||
When the context window fills up (long pages injected as context), the model either:
|
||||
1. Returns completely blank responses (silent failure, nothing displayed)
|
||||
2. Starts hallucinating about unrelated content, specifically talking about the uncloseai.com page itself or the Nous Research page
|
||||
|
||||
## Reproduction
|
||||
|
||||
1. Navigate to a very long page (e.g., Wikipedia's Napoleon article)
|
||||
2. Use the page-read feature to inject the page content as context
|
||||
3. Ask a question about the page
|
||||
4. Response is either blank or talks about uncloseai/Nous Research instead of the page content
|
||||
|
||||
## Analysis
|
||||
|
||||
The page content is likely exceeding the model's `max_model_len` (context window). When this happens:
|
||||
- The API may return an error that the client silently swallows (blank response)
|
||||
- The context may be truncated in a way that drops the user's page content but keeps the system prompt and uncloseai UI text, causing the model to talk about those instead
|
||||
- The Nous Research hallucination suggests the model's training data is bleeding through when it has no useful context to work with
|
||||
|
||||
Possible fixes:
|
||||
- Check page content length before injection, truncate intelligently if it exceeds the model's `max_model_len` (available from `/v1/models` discovery)
|
||||
- Show a user-facing warning when context is too long: "This page is too long, content has been trimmed"
|
||||
- Implement chunked summarization for long pages (summarize in sections, then inject summaries)
|
||||
- Never silently swallow API errors: if the completion fails, show the error to the user
|
||||
- Prioritize user message and page content over system prompt when truncating
|
||||
|
||||
## Fix Implemented
|
||||
|
||||
Smart content collapsing in `content.js` via `fitPageContent()`. Progressive stages run in order of aggressiveness, stopping when content fits:
|
||||
|
||||
1. **Deduplicate paragraphs**: remove exact duplicate blocks (common in scraped DOM)
|
||||
2. **Collapse nav links**: 5+ consecutive links become `[Navigation: N links including ...]`
|
||||
3. **Collapse references**: dense URL/citation clusters become `[... N references collapsed]`
|
||||
4. **Collapse lists**: 7+ item lists keep first 5, note `[... N more items]`
|
||||
5. **Collapse code blocks**: 12+ line blocks keep first 10, note `[... N more lines]`
|
||||
6. **Collapse sidebar debris**: runs of 6+ short fragments get compressed
|
||||
7. **Hard truncate**: last resort, cuts at paragraph/sentence boundary
|
||||
|
||||
No summarization. All content stays real, just structurally compressed. Uses `max_model_len` from model discovery with 15k token overhead reserved for system prompt, computed intel, chat history, and output.
|
||||
|
||||
## Notes
|
||||
|
||||
Hermes bumped to 82k context. Wikipedia Napoleon (~67k tokens) should fit after collapsing without hard truncation.
|
||||
47
docs/tickets/006-reverse-rag-cross-page-context.md
Normal file
47
docs/tickets/006-reverse-rag-cross-page-context.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# 006: Reverse RAG: cross-page context via encrypted localStorage
|
||||
|
||||
**Reporter:** fxhp
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** medium
|
||||
**Status:** fixed
|
||||
**Affects:** Browser extension, embedded widget
|
||||
|
||||
## Description
|
||||
|
||||
When a user browses multiple pages on the same site, the machine learning model should be able to reference content from previously visited pages. Currently each page is isolated: the model only knows the current page's content.
|
||||
|
||||
## Goal
|
||||
|
||||
Build a reverse RAG system where page summaries are stored in encrypted localStorage (per-domain), and automatically included in the system prompt for new pages. This gives the model a growing knowledge base about the site as the user browses.
|
||||
|
||||
## Architecture
|
||||
|
||||
1. After generating the intro message for a page, extract a brief summary (title, key topics, 2-3 sentences)
|
||||
2. Store summaries per-domain in vault-encrypted localStorage: `uncloseai-pageknowledge-{domain}`
|
||||
3. On new pages, include the last N page summaries in the system prompt as context
|
||||
4. Summaries should be compact (under 200 tokens each) to avoid context overflow
|
||||
5. Use the existing `fitPageContent()` budget system to allocate space for cross-page context
|
||||
|
||||
## Privacy
|
||||
|
||||
All cross-page data must be encrypted via the vault, same as chat history. The user controls their data. No data leaves the device except through explicit LLM requests.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Site journey tracking (implemented in storage.js)
|
||||
- Vault encryption (already working)
|
||||
- `fitPageContent()` context budgeting (already working)
|
||||
|
||||
## Notes
|
||||
|
||||
This is the foundation for making uncloseai "unstoppable" on any site. The model gets smarter the more pages you visit, building a personal knowledge graph of the site, all stored locally and encrypted.
|
||||
|
||||
## Resolution
|
||||
|
||||
Page-summary storage (per-domain, vault-encrypted via `savePageSummary` /
|
||||
`loadPageSummaries` in `storage.js`) and prompt backfill landed per the
|
||||
architecture above. Step 3's "include the last N page summaries" was
|
||||
later sharpened from recency selection to BM25 relevance ranking against
|
||||
the current page: see #009 Resolution (commit `2834e99`). Summaries now
|
||||
included are the ones *similar* to the page in view, not just the most
|
||||
recent.
|
||||
32
docs/tickets/007-journey-aware-conversation-persistence.md
Normal file
32
docs/tickets/007-journey-aware-conversation-persistence.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# 007: Journey-aware conversation persistence across pages
|
||||
|
||||
**Reporter:** fxhp
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** medium
|
||||
**Status:** open
|
||||
**Affects:** Browser extension, embedded widget
|
||||
|
||||
## Description
|
||||
|
||||
Currently each page URL gets its own isolated conversation history. When a user navigates to a new page, their previous conversation is gone. The model should be able to reference earlier conversations from the same browsing session.
|
||||
|
||||
## Goal
|
||||
|
||||
Allow the model to optionally carry forward conversation context when the user navigates between pages on the same site. Not full history transfer (too expensive), but a compressed summary of what was discussed.
|
||||
|
||||
## Architecture
|
||||
|
||||
1. When the user leaves a page (or on conversation save), generate a 1-2 sentence conversation summary
|
||||
2. Store summaries per-page in the site journey data (already tracked)
|
||||
3. Include recent conversation summaries in the system prompt for new pages
|
||||
4. Budget: allocate ~500 tokens max for cross-page conversation context
|
||||
|
||||
## Example
|
||||
|
||||
User on page A asks "What does this function do?" and gets an explanation.
|
||||
User navigates to page B. The model knows: "On the previous page, you were asking about the fetchData function and how it handles errors."
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Site journey tracking (implemented)
|
||||
- Reverse RAG page knowledge (#006)
|
||||
29
docs/tickets/008-site-knowledge-graph.md
Normal file
29
docs/tickets/008-site-knowledge-graph.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# 008: Site knowledge graph from browsing patterns
|
||||
|
||||
**Reporter:** fxhp
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** low
|
||||
**Status:** open
|
||||
**Affects:** Browser extension, embedded widget
|
||||
|
||||
## Description
|
||||
|
||||
As the user browses a site, build a lightweight knowledge graph of the site's structure: how pages link to each other, what topics each page covers, and what the user has explored vs. what they haven't.
|
||||
|
||||
## Goal
|
||||
|
||||
The model should understand the site's topology, not just the current page. It can suggest related pages, note when the user has visited related content, and act as a true site guide.
|
||||
|
||||
## Architecture
|
||||
|
||||
1. Extract internal links from each visited page
|
||||
2. Map page topics (from page-intelligence.js computed metrics)
|
||||
3. Build a link graph: `{ page_url: { title, topics, links_to: [urls], visited: bool } }`
|
||||
4. Store encrypted per-domain
|
||||
5. Include relevant graph context in system prompt (pages that link to/from current page)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Reverse RAG (#006)
|
||||
- Journey tracking (implemented)
|
||||
- Page intelligence (implemented)
|
||||
66
docs/tickets/009-smart-context-backfill.md
Normal file
66
docs/tickets/009-smart-context-backfill.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# 009: Smart context backfill from indexed pages
|
||||
|
||||
**Reporter:** fxhp
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** medium
|
||||
**Status:** fixed
|
||||
**Affects:** Browser extension, embedded widget
|
||||
|
||||
## Description
|
||||
|
||||
When the user opens the chatbot on a page and there is room left in the context window, backfill with content from previously indexed pages on the same site. "Indexed" means any page where the chatbot was opened and page intelligence was computed.
|
||||
|
||||
## Goal
|
||||
|
||||
Use spare context budget to pull in relevant cross-page knowledge automatically. If the user asks a question about something on a different page they already visited, the model can answer without the user navigating back.
|
||||
|
||||
## Architecture
|
||||
|
||||
1. After computing page content and journey context, calculate remaining context budget
|
||||
2. If remaining budget > 2000 tokens, pull from the page knowledge store (#006)
|
||||
3. Rank stored pages by relevance (keyword overlap with current page, recency)
|
||||
4. Pack as many page summaries as fit within the remaining budget
|
||||
5. Include in system prompt as "INDEXED SITE KNOWLEDGE" section
|
||||
|
||||
## Budget Calculation
|
||||
|
||||
```
|
||||
total = model max tokens (e.g. 82000)
|
||||
used = system prompt + page content + journey context + overhead
|
||||
spare = total - used
|
||||
backfill = min(spare * 0.5, 10000) // never use more than half of spare
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Reverse RAG page knowledge (#006)
|
||||
- Journey tracking (implemented)
|
||||
- fitPageContent() budget system (implemented)
|
||||
|
||||
## Resolution (2026-05-22)
|
||||
|
||||
Backfill shipped in two stages.
|
||||
|
||||
**Stage 1 (initial):** spare budget (30% of `maxTokens - usedEstimate`,
|
||||
gated at `> 500` tokens) packed with indexed page summaries, sorted by
|
||||
recency only. This satisfied steps 1, 2, 4, 5 but **not step 3**: the
|
||||
ranking. `getRelevantPageSummaries()` carried a docstring claiming
|
||||
"keyword overlap with current page" but the code only sorted by
|
||||
`indexedAt`, so the prompt was backfilled with recently-visited pages
|
||||
regardless of relevance.
|
||||
|
||||
**Stage 2 (commit `2834e99`):** implemented step 3 with BM25 relevance
|
||||
ranking in `storage.js`:
|
||||
|
||||
- Query = current page (`document.title` + first 2000 chars of content),
|
||||
passed from the call site in `uncloseai-embed-modal.js`.
|
||||
- Each candidate summary (title + topics + summary) is tokenized
|
||||
(lowercased, stopwords + single chars dropped) and scored with BM25
|
||||
(`k1=1.5`, `b=0.75`, IDF over the candidate corpus).
|
||||
- Only summaries scoring `>= 2.0` are kept, best-first, then packed to
|
||||
budget. Verified separation: related docs score 5-8, unrelated 0.
|
||||
- Recency ordering retained as a fallback when no query is supplied
|
||||
(backward compatible).
|
||||
|
||||
Net effect: cross-page context now pulls *similar* pages, not merely
|
||||
recent ones.
|
||||
45
docs/tickets/010-ethical-crawl-mode.md
Normal file
45
docs/tickets/010-ethical-crawl-mode.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# 010: Ethical crawl mode for site indexing ($14/m plan)
|
||||
|
||||
**Reporter:** fxhp
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** medium
|
||||
**Status:** open
|
||||
**Affects:** Browser extension (paid tier)
|
||||
|
||||
## Description
|
||||
|
||||
Paid feature ($14/m) that crawls a site following robots.txt rules, indexing pages for keyword search and cross-page question answering. All data stored in encrypted localStorage on the user's device.
|
||||
|
||||
## Goal
|
||||
|
||||
Users can index an entire site (or section) without manually opening the chatbot on every page. Then ask questions across all indexed pages like "which page talks about authentication?" or "find all mentions of pricing."
|
||||
|
||||
## Architecture
|
||||
|
||||
1. User triggers crawl from the chatbot UI (new "Index Site" button in settings)
|
||||
2. Extension fetches robots.txt, parses disallow/allow rules
|
||||
3. Starts from current page, follows internal links breadth-first
|
||||
4. For each page: extract content, compute page intelligence, store summary
|
||||
5. All data encrypted in vault localStorage
|
||||
6. Respect: rate limiting (1 req/sec), max depth, max pages, robots.txt
|
||||
7. Progress UI shows pages indexed, estimated coverage
|
||||
|
||||
## Ethics
|
||||
|
||||
- Strictly follows robots.txt (no crawl if disallowed)
|
||||
- Rate limited to 1 request per second minimum
|
||||
- User-initiated only (never auto-crawl)
|
||||
- All data stays on the user's device
|
||||
- Clear disclosure: "This indexes pages for your private use only"
|
||||
|
||||
## Monetization
|
||||
|
||||
- Free tier: manual page-by-page indexing (open chatbot on each page)
|
||||
- $14/m plan: automated crawl mode + higher page limits + priority support
|
||||
- Payment via unsandbox.com accounts
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Reverse RAG page knowledge (#006)
|
||||
- Vault encryption (implemented)
|
||||
- unsandbox.com billing integration
|
||||
54
docs/tickets/011-vault-viewer-dashboard.md
Normal file
54
docs/tickets/011-vault-viewer-dashboard.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# 011: Vault viewer: indexed pages, charts, and engagement metrics
|
||||
|
||||
**Reporter:** fxhp
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** medium
|
||||
**Status:** open
|
||||
**Affects:** Browser extension, embedded widget
|
||||
|
||||
## Description
|
||||
|
||||
A new UI panel in the chatbot showing what's stored in the user's encrypted vault for this site. Charts, metrics, and visualizations that make the user feel like they're getting value just from browsing with the chatbot open.
|
||||
|
||||
## Goal
|
||||
|
||||
Show the user their browsing intelligence: what pages they've visited, what's been indexed, coverage metrics, and natural engagement data. Not gamified, but genuinely useful metrics that emerge from organic browsing.
|
||||
|
||||
## Features
|
||||
|
||||
### Pages View
|
||||
- List of all indexed pages on this domain
|
||||
- Title, URL, last visited, number of conversations
|
||||
- Search/filter across indexed pages
|
||||
- Click to navigate to a page
|
||||
|
||||
### Coverage Metrics (natural, not gamified)
|
||||
- Pages visited vs total internal links discovered
|
||||
- Topics covered (from page-intelligence.js topic extraction)
|
||||
- Content consumed: total word count read, estimated reading time
|
||||
- Questions asked and answered per topic area
|
||||
- How much of the site's content has been discussed
|
||||
|
||||
### Charts
|
||||
- Timeline of browsing activity on this site
|
||||
- Topic distribution (pie/bar chart of content categories)
|
||||
- Engagement depth (pages visited over time)
|
||||
- Knowledge growth curve (indexed content accumulation)
|
||||
|
||||
### Privacy Controls
|
||||
- Clear indexed data for this site
|
||||
- Export vault data (encrypted JSON download)
|
||||
- View storage usage
|
||||
|
||||
## Architecture
|
||||
|
||||
- New panel accessible from chatbot settings/menu
|
||||
- Reads from vault localStorage (journey data, page knowledge)
|
||||
- Charts rendered with lightweight library (no heavy dependencies)
|
||||
- All computation client-side, no server calls
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Reverse RAG (#006)
|
||||
- Journey tracking (implemented)
|
||||
- Page intelligence (implemented)
|
||||
52
docs/tickets/012-unfirehose-opt-in-sharing.md
Normal file
52
docs/tickets/012-unfirehose-opt-in-sharing.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# 012: Opt-in chat sharing via unfirehose
|
||||
|
||||
**Reporter:** fxhp
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** low
|
||||
**Status:** open
|
||||
**Affects:** Browser extension, embedded widget
|
||||
|
||||
## Description
|
||||
|
||||
Allow users to optionally share their chat conversations publicly via unfirehose.org for machine learning training data. All chats are private by default. Sharing is always opt-in, per-conversation, with clear disclosure.
|
||||
|
||||
## Goal
|
||||
|
||||
Build the ethical pipeline for users to contribute their browsing intelligence and conversations to the public commons, creating training data that improves the models for everyone. Following the unfirehose philosophy: "Pay for privacy, not for access."
|
||||
|
||||
## Architecture
|
||||
|
||||
1. Per-conversation "Share" button in chat UI
|
||||
2. On share: decrypt conversation from vault, push to unfirehose.org API
|
||||
3. Access levels (from unfirehose):
|
||||
- **public**: Visible in global feed, permanent archive, free
|
||||
- **unlisted**: Direct link only, not in feeds, paid
|
||||
- **private**: Default, never leaves device
|
||||
4. Auth via unsandbox.com SSO (shared identity with unfirehose)
|
||||
5. API endpoint: `POST https://api.unfirehose.org/v1/ingest`
|
||||
6. Data format: JSONL with X-Source: "uncloseai" header
|
||||
|
||||
## Privacy
|
||||
|
||||
- **All chats private by default.** Period.
|
||||
- Sharing is per-conversation, not global toggle
|
||||
- Clear preview of what will be shared before sending
|
||||
- Revocation: user can delete shared conversations
|
||||
- No metadata leakage: strip device info, IP, vault keys
|
||||
- GDPR compliant: right to deletion, data portability
|
||||
|
||||
## User Flow
|
||||
|
||||
1. User has a conversation they want to share
|
||||
2. Clicks "Share" button on conversation
|
||||
3. Sees preview: "This will make this conversation publicly visible"
|
||||
4. Chooses access level (public/unlisted)
|
||||
5. Authenticates with unsandbox.com account
|
||||
6. Conversation pushed to unfirehose.org
|
||||
7. Gets shareable link back
|
||||
|
||||
## Dependencies
|
||||
|
||||
- unfirehose.org API (see ~/git/unfirehose)
|
||||
- unsandbox.com accounts/billing
|
||||
- Vault encryption (implemented)
|
||||
25
docs/tickets/013-page-styles-override-chatbot.md
Normal file
25
docs/tickets/013-page-styles-override-chatbot.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# 013: Page styles override chatbot button design and fonts
|
||||
|
||||
**Reporter:** cthegray
|
||||
**Date:** 2026-03-03
|
||||
**Priority:** high
|
||||
**Status:** open
|
||||
**Affects:** Embedded widget on all pages
|
||||
|
||||
## Description
|
||||
|
||||
Host page CSS leaks into the uncloseai chatbot widget, overriding button designs and font styles. Most visible: white text in the input box on pages with dark-theme or custom text color CSS (e.g. Google Docs).
|
||||
|
||||
## Reproduction
|
||||
|
||||
1. Open uncloseai chatbot on Google Docs
|
||||
2. Note the input box has white/invisible text
|
||||
3. Page CSS `color: white` or similar rules bleed through to chatbot elements
|
||||
|
||||
## Root Cause
|
||||
|
||||
The chatbot dialog and its children inherit CSS from the host page. No CSS isolation barrier exists between the page and the chatbot.
|
||||
|
||||
## Fix
|
||||
|
||||
Add CSS reset (`all: initial` / `all: revert`) on the chatbot dialog root, then re-apply all chatbot styles explicitly. Ensure input, buttons, and text elements have explicit color/background rules that cannot be overridden by page styles.
|
||||
23
docs/tickets/index.md
Normal file
23
docs/tickets/index.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# uncloseai.com Tickets
|
||||
|
||||
## Open
|
||||
|
||||
| ID | Title | Priority | Status |
|
||||
|----|-------|----------|--------|
|
||||
| 002 | [TTS stops after first sentence](002-tts-stops-after-first-sentence.md) | high | deferred (TTS feature-flagged off) |
|
||||
| 003 | [Audio icon freezes entire window](003-audio-icon-freezes-window.md) | critical | deferred (TTS feature-flagged off) |
|
||||
|
||||
## Fixed
|
||||
|
||||
| ID | Title | Priority | Fix |
|
||||
|----|-------|----------|-----|
|
||||
| 001 | [Extension fails to load (CSP)](001-extension-fails-to-load.md) | high | Three-tier injection via chrome.scripting.executeScript |
|
||||
| 004 | [Working/spinning indicators](004-working-indicators.md) | medium | .working class wired to send button during async ops |
|
||||
| 005 | [Context overflow silent failure](005-context-overflow-silent-failure.md) | high | Smart content collapsing in fitPageContent() |
|
||||
| 006 | [Reverse RAG: cross-page context](006-reverse-rag-cross-page-context.md) | medium | Page summaries in encrypted localStorage, BM25-ranked backfill into prompts |
|
||||
| 007 | [Journey-aware conversation persistence](007-journey-aware-conversation-persistence.md) | medium | Conversation summaries stored in journey data per-page |
|
||||
| 008 | [Site knowledge graph](008-site-knowledge-graph.md) | low | Internal link extraction + adjacency graph in vault |
|
||||
| 009 | [Smart context backfill](009-smart-context-backfill.md) | medium | Spare context budget fills with BM25-ranked indexed page summaries |
|
||||
| 010 | [Ethical crawl mode](010-ethical-crawl-mode.md) | medium | robots.txt parser, BFS crawler, rate-limited, API key gated |
|
||||
| 011 | [Vault viewer dashboard](011-vault-viewer-dashboard.md) | medium | Site intelligence panel with metrics, topics, indexed pages list |
|
||||
| 012 | [Unfirehose opt-in sharing](012-unfirehose-opt-in-sharing.md) | low | Share button with access level selector, JSONL ingest to unfirehose.org |
|
||||
381
index.html
381
index.html
|
|
@ -1,381 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Using Free LLM & Text To Speech Artificial Intelligence Service | ai.unturf.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
<script defer data-domain="ai.unturf.com" src="https://analytics.unturf.com/js/plausible.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://ai.unturf.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">unturf.</h1></a>
|
||||
<p>Welcome to ai.unturf.com - Free LLM & Text To Speech Artificial Intelligence Service</title>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<h2>Introducing Hermes AI and TTS Speech Endpoints</h2>
|
||||
<p>At <strong>ai.unturf.com</strong>, we offer free AI services powered by the <a href="https://nousresearch.com/hermes3/" target="_blank">NousResearch/Hermes-3-Llama-3.1-8B</a> model and a TTS (Text-to-Speech) endpoint. Our mission is to provide accessible AI tools for everyone, embodying the principles of both free as in beer & free as in freedom. You can interact with our models without any cost, and you are encouraged to contribute and build upon the open-source code & models that we use.</p>
|
||||
|
||||
<p>We intend to be a drop in replacement, you can use the existing open source OpenAI client to communicate with us.</p>
|
||||
|
||||
<h2 id="client-side">Web Client-Only Solution: Interact with AI Services Directly from Static Sites or CDNs</h2>
|
||||
|
||||
<p><b>Because we don't require a valid API key, we don't have any real need for a server.</b></p>
|
||||
|
||||
<p>Add this LLM to any static site or CDN.</p>
|
||||
|
||||
<p>This web client-only solution uses <a href="https://uncloseai.com/uncloseai.js" target="_blank">uncloseai.js</a> to make the browser act as a client, directly interacting with the API without needing an intermediary server. By eliminating the need for a valid API key, the API handles requests on behalf of the browser client, making it efficient and accessible thin client, especially those on battery power like phones & laptops.</p>
|
||||
|
||||
<div id="chat-container">
|
||||
<div id="chat-box"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div>
|
||||
<input type="text" id="user-input" placeholder="Ask about this page...">
|
||||
<button onclick="handleUserInput()">Send</button>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<p>This static site has a live LLM demonstration above!</p>
|
||||
<p>A stand alone demo is here: <a href="https://ai.unturf.com/demo.html">uncloseai.js demostration</a></p>
|
||||
|
||||
<h3>Installing the OpenAI Client</h3>
|
||||
<h4>Python</h4>
|
||||
<p>To install the OpenAI package for Python, use <code>pip</code>:</p>
|
||||
<pre><code>pip install openai</code></pre>
|
||||
|
||||
<h4>Node.js</h4>
|
||||
<p>To install the OpenAI package for Node.js, you can use <code>npm</code> in your <code>package.json</code>:</p>
|
||||
<pre><code>{
|
||||
"dependencies": {
|
||||
"openai": "^v4.67.3" // Use the latest version
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<p>Run the following command to install it:</p>
|
||||
<pre><code>npm install</code></pre>
|
||||
|
||||
<h2>Using the Hermes AI Model</h2>
|
||||
<h3>Python Example</h3>
|
||||
<h4>Non-Streaming</h4>
|
||||
<pre><code class="python"># Python Fizzbuzz Example
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(base_url="https://hermes.ai.unturf.com/v1", api_key="choose-any-value")
|
||||
|
||||
#MODEL = "NousResearch/Hermes-3-Llama-3.1-8B"
|
||||
MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"
|
||||
|
||||
messages = [{"role": "user", "content": "Give a Python Fizzbuzz solution in one line of code?"}]
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=MODEL,
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
max_tokens=150
|
||||
)
|
||||
|
||||
print(response.choices[0].message.content)
|
||||
</code></pre>
|
||||
|
||||
<h4>Streaming</h4>
|
||||
<pre><code class="python"># Streaming response in Python
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(base_url="https://hermes.ai.unturf.com/v1", api_key="choose-any-value")
|
||||
|
||||
MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Give a Python Fizzbuzz solution in one line of code?"}
|
||||
]
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=MODEL,
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
max_tokens=150,
|
||||
stream=True, # Enable streaming
|
||||
)
|
||||
|
||||
for chunk in response:
|
||||
if hasattr(chunk.choices[0].delta, "content"):
|
||||
print(chunk.choices[0].delta.content, end="")
|
||||
</code></pre>
|
||||
|
||||
<h2>Node.js Example</h2>
|
||||
<h3>Non-Streaming</h3>
|
||||
<pre><code class="javascript">const OpenAI = require('openai');
|
||||
|
||||
const client = new OpenAI({
|
||||
baseURL: "https://hermes.ai.unturf.com/v1",
|
||||
apiKey: "dummy-api-key",
|
||||
});
|
||||
|
||||
const MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic";
|
||||
|
||||
const messages = [{"role": "user", "content": "Give a Python Fizzbuzz solution in one line of code?"}];
|
||||
|
||||
async function getResponse() {
|
||||
try {
|
||||
const response = await client.chat.completions.create({
|
||||
model: MODEL,
|
||||
messages: messages,
|
||||
temperature: 0.5,
|
||||
max_tokens: 150,
|
||||
});
|
||||
|
||||
console.log(response.choices[0].message.content);
|
||||
} catch (error) {
|
||||
console.error("Error:", error.response ? error.response.data : error.message);
|
||||
}
|
||||
}
|
||||
|
||||
getResponse();
|
||||
</code></pre>
|
||||
|
||||
<h3>Streaming</h3>
|
||||
<pre><code class="javascript">
|
||||
const OpenAI = require('openai');
|
||||
|
||||
const client = new OpenAI({
|
||||
baseURL: "https://hermes.ai.unturf.com/v1",
|
||||
apiKey: "dummy-api-key",
|
||||
});
|
||||
|
||||
const MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic";
|
||||
|
||||
const messages = [{"role": "user", "content": "Give a Python Fizzbuzz solution in one line of code?"}];
|
||||
|
||||
async function streamResponse() {
|
||||
try {
|
||||
const stream = await client.chat.completions.create({
|
||||
model: MODEL,
|
||||
messages: messages,
|
||||
temperature: 0.5,
|
||||
max_tokens: 150,
|
||||
stream: true, // Enable streaming
|
||||
});
|
||||
|
||||
// Use async iterator to read each chunk
|
||||
for await (const chunk of stream) {
|
||||
const msg = chunk.choices[0].delta.content;
|
||||
process.stdout.write(msg); // Print each chunk as it arrives
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error.response ? error.response.data : error.message);
|
||||
}
|
||||
}
|
||||
|
||||
streamResponse();
|
||||
</code></pre>
|
||||
|
||||
<h2 id="tts">Using the Text To Speech Endpoint</h2>
|
||||
<h3>Python Example</h3>
|
||||
<pre><code class="python"># TTS Speech Example in Python
|
||||
import openai
|
||||
|
||||
client = openai.OpenAI(
|
||||
api_key = "YOLO",
|
||||
base_url = "https://speech.ai.unturf.com/v1",
|
||||
)
|
||||
|
||||
with client.audio.speech.with_streaming_response.create(
|
||||
model="tts-1",
|
||||
voice="alloy",
|
||||
speed=0.9,
|
||||
input="I think so therefore, Today is a wonderful day to build something people love!"
|
||||
) as response:
|
||||
response.stream_to_file("speech.mp3")
|
||||
</code></pre>
|
||||
|
||||
<h3>Node.js Example</h3>
|
||||
<pre><code class="javascript">const OpenAI = require('openai');
|
||||
|
||||
const client = new OpenAI({
|
||||
baseURL: "https://speech.ai.unturf.com/v1",
|
||||
apiKey: "YOLO",
|
||||
});
|
||||
|
||||
async function getSpeech() {
|
||||
try {
|
||||
const response = await client.audio.speech.with_streaming_response.create({
|
||||
model: "tts-1",
|
||||
voice: "alloy",
|
||||
speed: 0.9,
|
||||
input: "I think so therefore, Today is a wonderful day to build something people love!"
|
||||
});
|
||||
|
||||
response.stream_to_file("speech.mp3");
|
||||
} catch (error) {
|
||||
console.error("Error:", error.response ? error.response.data : error.message);
|
||||
}
|
||||
}
|
||||
|
||||
getSpeech();
|
||||
</code></pre>
|
||||
|
||||
<h2>How we run inference</h2>
|
||||
|
||||
<p>This section is optional. This is only if you wanted to try to contribute idle GPU time to the project or if you wanted to reproduce everything in your own cluster.</p>
|
||||
|
||||
<p>We use vLLM to run models, currently full f16 safetensors. We make sure to use a virtualenv to hold the dependencies.</p>
|
||||
<p>We are considering supporting ollama for better quant support.</p>
|
||||
<p>Stand up a replica cluster on a new domain.</p>
|
||||
<pre><code>
|
||||
cd ~
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
pip install vllm
|
||||
python -m vllm.entrypoints.openai.api_server --model adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic --host 0.0.0.0 --port 18888 --max-model-len 82000
|
||||
</code></pre>
|
||||
|
||||
<p>The Speech endpoint or TTS uses <a href="https://github.com/matatonic/openedai-speech?tab=readme-ov-file#nvidia-gpu-cuda">openedai-speech</a> running via Docker.
|
||||
|
||||
<p>If you want to see how we setup the proxy, check out <a href="https://git.unturf.com/-/snippets/3">/etc/caddy/Caddyfile</a></p>
|
||||
|
||||
<pre><code>
|
||||
|
||||
ai.unturf.com {
|
||||
root * /opt/www
|
||||
file_server
|
||||
log {
|
||||
output file /var/log/caddy/ai.unturf.com.log {
|
||||
roll_size 50mb
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
}
|
||||
|
||||
hermes.ai.unturf.com {
|
||||
reverse_proxy <removed>:18888
|
||||
log {
|
||||
output file /var/log/caddy/hermes.ai.unturf.com.log {
|
||||
roll_size 50mb
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
}
|
||||
|
||||
speech.ai.unturf.com {
|
||||
reverse_proxy <removed>:8000
|
||||
log {
|
||||
output file /var/log/caddy/speech.ai.unturf.com.log {
|
||||
roll_size 50mb
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
}
|
||||
|
||||
</code></pre>
|
||||
|
||||
<p>We will likely implement a rate limit based on client IP address.</p>
|
||||
|
||||
|
||||
<h2>Questions & Comments & Discussions</h2>
|
||||
|
||||
Use the <a href="https://www.remarkbox.com" target="_blank">Remarkbox</a> below to tell us what you think!
|
||||
|
||||
<div id="remarkbox-div">
|
||||
<noscript>
|
||||
<iframe id=remarkbox-iframe src="https://my.remarkbox.com/embed?nojs=true" style="height:600px;width:100%;border:none!important" tabindex=0></iframe>
|
||||
</noscript>
|
||||
</div>
|
||||
<script src="https://my.remarkbox.com/static/js/iframe-resizer/iframeResizer.min.js"></script>
|
||||
<script>
|
||||
var rb_owner_key = "944c8dfa-8b2b-11ef-af0e-29ab4fb285a0";
|
||||
var thread_uri = window.location.href;
|
||||
var thread_title = window.document.title;
|
||||
var thread_fragment = window.location.hash;
|
||||
|
||||
var rb_src = "https://my.remarkbox.com/embed" +
|
||||
"?rb_owner_key=" + rb_owner_key +
|
||||
"&thread_title=" + encodeURI(thread_title) +
|
||||
"&thread_uri=" + encodeURIComponent(thread_uri) +
|
||||
thread_fragment;
|
||||
|
||||
function create_remarkbox_iframe() {
|
||||
var ifrm = document.createElement("iframe");
|
||||
ifrm.setAttribute("id", "remarkbox-iframe");
|
||||
ifrm.setAttribute("scrolling", "no");
|
||||
ifrm.setAttribute("src", rb_src);
|
||||
ifrm.setAttribute("frameborder", "0");
|
||||
ifrm.setAttribute("tabindex", "0");
|
||||
ifrm.setAttribute("title", "Remarkbox");
|
||||
ifrm.style.width = "100%";
|
||||
document.getElementById("remarkbox-div").appendChild(ifrm);
|
||||
}
|
||||
create_remarkbox_iframe();
|
||||
iFrameResize(
|
||||
{
|
||||
checkOrigin: ["https://my.remarkbox.com"],
|
||||
inPageLinks: true,
|
||||
initCallback: function(e) { e.iFrameResizer.moveToAnchor(thread_fragment) }
|
||||
},
|
||||
document.getElementById("remarkbox-iframe")
|
||||
);
|
||||
</script>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
998
integration_tests.js
Executable file
998
integration_tests.js
Executable file
|
|
@ -0,0 +1,998 @@
|
|||
// This is free software for the public good of a permacomputer hosted at
|
||||
// permacomputer.com, an always-on computer by the people, for the people.
|
||||
// One which is durable, easy to repair, & distributed like tap water
|
||||
// for machine learning intelligence.
|
||||
//
|
||||
// The permacomputer is community-owned infrastructure optimized around
|
||||
// four values:
|
||||
//
|
||||
// TRUTH First principles, math & science, open source code freely distributed
|
||||
// FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
// HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
// LOVE Be yourself without hurting others, cooperation through natural law
|
||||
//
|
||||
// This software contributes to that vision by making machine learning
|
||||
// accessible to everyone through a free, open, embeddable chat interface.
|
||||
// Code is seeds to sprout on any abandoned technology.
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import fs from 'fs';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import vm from 'vm';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const srcDir = join(__dirname, 'src');
|
||||
|
||||
// Test results tracking
|
||||
let tests = 0;
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
let warnings = 0;
|
||||
|
||||
function test(description, testFn) {
|
||||
tests++;
|
||||
try {
|
||||
testFn();
|
||||
console.log(`✅ ${description}`);
|
||||
passed++;
|
||||
} catch (error) {
|
||||
console.error(`❌ ${description}`);
|
||||
console.error(` Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function warn(description, message) {
|
||||
console.warn(`⚠️ ${description}: ${message}`);
|
||||
warnings++;
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a mock DOM environment
|
||||
function createMockDOM() {
|
||||
const dom = new JSDOM(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="test-container"></div>
|
||||
</body>
|
||||
</html>`, {
|
||||
url: "https://example.com/test",
|
||||
pretendToBeVisual: true,
|
||||
resources: "usable"
|
||||
});
|
||||
|
||||
Object.defineProperty(global, 'window', { value: dom.window, configurable: true });
|
||||
Object.defineProperty(global, 'document', { value: dom.window.document, configurable: true });
|
||||
Object.defineProperty(global, 'navigator', { value: dom.window.navigator, configurable: true });
|
||||
Object.defineProperty(global, 'HTMLElement', { value: dom.window.HTMLElement, configurable: true });
|
||||
Object.defineProperty(global, 'Element', { value: dom.window.Element, configurable: true });
|
||||
Object.defineProperty(global, 'Node', { value: dom.window.Node, configurable: true });
|
||||
Object.defineProperty(global, 'location', { value: dom.window.location, configurable: true });
|
||||
|
||||
// Mock fetch and other globals
|
||||
global.fetch = async (url, options) => {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({}),
|
||||
text: async () => '',
|
||||
status: 200
|
||||
};
|
||||
};
|
||||
|
||||
// Mock audio and media APIs
|
||||
global.Audio = class MockAudio {
|
||||
constructor() {
|
||||
this.paused = true;
|
||||
this.currentTime = 0;
|
||||
}
|
||||
play() { this.paused = false; return Promise.resolve(); }
|
||||
pause() { this.paused = true; }
|
||||
};
|
||||
|
||||
// Mock clipboard API
|
||||
global.navigator.clipboard = {
|
||||
writeText: async (text) => Promise.resolve()
|
||||
};
|
||||
|
||||
// Mock matchMedia
|
||||
global.window.matchMedia = (query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {}
|
||||
});
|
||||
|
||||
return dom;
|
||||
}
|
||||
|
||||
// Helper functions (keeping the existing ones)
|
||||
function fileExists(filePath) {
|
||||
return fs.existsSync(join(srcDir, filePath));
|
||||
}
|
||||
|
||||
function readFile(filePath) {
|
||||
return fs.readFileSync(join(srcDir, filePath), 'utf-8');
|
||||
}
|
||||
|
||||
function extractImports(content) {
|
||||
const importRegex = /import\s+(?:{[^}]*}|\*\s+as\s+\w+|\w+)\s+from\s+["'](.\/[^"']+)["']/gs;
|
||||
const imports = [];
|
||||
let match;
|
||||
while ((match = importRegex.exec(content)) !== null) {
|
||||
imports.push(match[1]);
|
||||
}
|
||||
return imports;
|
||||
}
|
||||
|
||||
function extractExports(content) {
|
||||
const exportRegex = /export\s+(?:async\s+)?(?:function|const|let|var|class)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
||||
const exports = [];
|
||||
let match;
|
||||
while ((match = exportRegex.exec(content)) !== null) {
|
||||
exports.push(match[1]);
|
||||
}
|
||||
return exports;
|
||||
}
|
||||
|
||||
// Advanced module loading test
|
||||
async function loadModuleInSandbox(modulePath, mockGlobals = {}) {
|
||||
const moduleContent = readFile(modulePath);
|
||||
const context = {
|
||||
...mockGlobals,
|
||||
console,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
Promise,
|
||||
URL,
|
||||
URLSearchParams,
|
||||
fetch: global.fetch,
|
||||
import: async (path) => {
|
||||
// Mock dynamic imports for testing
|
||||
if (path.includes('./')) {
|
||||
return {}; // Return empty object for relative imports
|
||||
}
|
||||
return {}; // Mock external imports
|
||||
}
|
||||
};
|
||||
|
||||
vm.createContext(context);
|
||||
|
||||
try {
|
||||
// Transform ES modules to work in VM context (basic transformation)
|
||||
const transformedCode = moduleContent
|
||||
.replace(/import\s+.*?from\s+["'][^"']*["'];?/g, '// import statement removed for testing')
|
||||
.replace(/export\s+(default\s+)?/g, '// export ');
|
||||
|
||||
vm.runInContext(transformedCode, context);
|
||||
return context;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load module ${modulePath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🧪 Running Comprehensive Integration Tests for Refactored UI Modules\n');
|
||||
|
||||
// Create mock DOM environment
|
||||
const dom = createMockDOM();
|
||||
|
||||
// ===== SECTION 1: BASIC STRUCTURE TESTS =====
|
||||
console.log('📁 Section 1: Basic Structure Tests');
|
||||
|
||||
test('All core JavaScript files exist', () => {
|
||||
const coreFiles = [
|
||||
'ui.js', 'uncloseai-embed-modal.js', 'widget-library.js',
|
||||
'tts-modal.js', 'translate-modal.js', 'file-upload.js',
|
||||
'chat.js', 'content.js', 'ui-themes.js', 'ui-translations.js',
|
||||
'translation.js', 'config.js', 'tts.js', 'language-detection.js'
|
||||
];
|
||||
|
||||
coreFiles.forEach(file => {
|
||||
assert(fileExists(file), `File ${file} does not exist`);
|
||||
});
|
||||
});
|
||||
|
||||
test('All imported files exist and are accessible', () => {
|
||||
const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.js'));
|
||||
|
||||
files.forEach(file => {
|
||||
const content = readFile(file);
|
||||
const imports = extractImports(content);
|
||||
|
||||
imports.forEach(importPath => {
|
||||
const filePath = importPath.replace('./', '');
|
||||
assert(fileExists(filePath), `Imported file does not exist: ${filePath} (imported by ${file})`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ===== SECTION 2: MODULE LOADING TESTS =====
|
||||
console.log('\n🔄 Section 2: Module Loading Tests');
|
||||
|
||||
test('ui.js can be parsed without syntax errors', () => {
|
||||
const content = readFile('ui.js');
|
||||
assert(content.length > 0, 'ui.js is empty');
|
||||
|
||||
// Check for basic syntax issues
|
||||
const openBraces = (content.match(/{/g) || []).length;
|
||||
const closeBraces = (content.match(/}/g) || []).length;
|
||||
assert(openBraces === closeBraces, `Mismatched braces in ui.js: ${openBraces} open, ${closeBraces} close`);
|
||||
|
||||
const openParens = (content.match(/\(/g) || []).length;
|
||||
const closeParens = (content.match(/\)/g) || []).length;
|
||||
assert(openParens === closeParens, `Mismatched parentheses in ui.js: ${openParens} open, ${closeParens} close`);
|
||||
});
|
||||
|
||||
test('All module files can be parsed without syntax errors', () => {
|
||||
const moduleFiles = ['widget-library.js', 'uncloseai-embed-modal.js', 'tts-modal.js', 'translate-modal.js'];
|
||||
|
||||
moduleFiles.forEach(file => {
|
||||
const content = readFile(file);
|
||||
assert(content.length > 0, `${file} is empty`);
|
||||
|
||||
// Basic syntax validation
|
||||
const openBraces = (content.match(/{/g) || []).length;
|
||||
const closeBraces = (content.match(/}/g) || []).length;
|
||||
assert(openBraces === closeBraces, `Mismatched braces in ${file}: ${openBraces} open, ${closeBraces} close`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Configuration module loads correctly', () => {
|
||||
const configContent = readFile('config.js');
|
||||
assert(configContent.includes('export'), 'config.js has no exports');
|
||||
|
||||
// Check for key configuration items
|
||||
const hasApiKey = configContent.includes('API_KEY');
|
||||
const hasTtsUrl = configContent.includes('TTS_API_URL');
|
||||
assert(hasApiKey || hasTtsUrl, 'config.js missing expected configuration exports');
|
||||
});
|
||||
|
||||
// ===== SECTION 3: FUNCTION EXPORT TESTS =====
|
||||
console.log('\n🔧 Section 3: Function Export Tests');
|
||||
|
||||
test('ui.js exports all required functions', () => {
|
||||
const content = readFile('ui.js');
|
||||
const exports = extractExports(content);
|
||||
|
||||
const requiredExports = [
|
||||
'createFloatingAIButton',
|
||||
'initializeSystem',
|
||||
'initializeUncloseaiElements',
|
||||
'initializeChatInterface',
|
||||
'toggleUncloseaiEmbeddedModal'
|
||||
];
|
||||
|
||||
requiredExports.forEach(exp => {
|
||||
assert(exports.includes(exp), `ui.js missing export: ${exp}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('widget-library.js exports all widget creation functions', () => {
|
||||
const content = readFile('widget-library.js');
|
||||
const exports = extractExports(content);
|
||||
|
||||
const widgetExports = [
|
||||
'createFullInterface', 'createCustomInterface', 'createChatFeature',
|
||||
'createTTSFeature', 'createUploadFeature', 'createTranslateFeature',
|
||||
'createSmartTranslateFeature', 'createReadFeature', 'createButton'
|
||||
];
|
||||
|
||||
widgetExports.forEach(exp => {
|
||||
assert(exports.includes(exp), `widget-library.js missing export: ${exp}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Modal modules export their main functions', () => {
|
||||
// Test uncloseai-embed-modal.js
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
const modalExports = extractExports(modalContent);
|
||||
assert(modalExports.includes('openUncloseaiEmbeddedModal'), 'Missing openUncloseaiEmbeddedModal export');
|
||||
|
||||
// Test tts-modal.js
|
||||
const ttsContent = readFile('tts-modal.js');
|
||||
const ttsExports = extractExports(ttsContent);
|
||||
assert(ttsExports.includes('openTTSModal'), 'Missing openTTSModal export');
|
||||
|
||||
// Test translate-modal.js
|
||||
const translateContent = readFile('translate-modal.js');
|
||||
const translateExports = extractExports(translateContent);
|
||||
assert(translateExports.includes('openTranslateModal'), 'Missing openTranslateModal export');
|
||||
});
|
||||
|
||||
// ===== SECTION 4: DOM RENDERING TESTS =====
|
||||
console.log('\n🎨 Section 4: DOM Rendering Tests');
|
||||
|
||||
test('createFloatingAIButton can create DOM elements', () => {
|
||||
// Mock the required functions
|
||||
global.window.UNCLOSEAI_CUSTOM_STYLING = true;
|
||||
global.window.UNCLOSEAI_FLOATING_BUTTON = true;
|
||||
|
||||
// Create a mock function that simulates createFloatingAIButton
|
||||
const mockCreateButton = () => {
|
||||
const button = document.createElement('button');
|
||||
button.id = 'floating-ai-button';
|
||||
button.textContent = 'uncloseai.';
|
||||
button.style.cssText = 'position: fixed; bottom: 20px; right: 10px;';
|
||||
document.body.appendChild(button);
|
||||
return button;
|
||||
};
|
||||
|
||||
const button = mockCreateButton();
|
||||
assert(button instanceof Element, 'Failed to create button element');
|
||||
assert(button.id === 'floating-ai-button', 'Button has wrong ID');
|
||||
assert(button.textContent === 'uncloseai.', 'Button has wrong text content');
|
||||
assert(document.getElementById('floating-ai-button'), 'Button not added to DOM');
|
||||
});
|
||||
|
||||
test('Widget creation functions can generate proper DOM structure', () => {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'widget-test-container';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Mock widget creation
|
||||
const mockCreateChatFeature = (container) => {
|
||||
const chatDiv = document.createElement('div');
|
||||
chatDiv.innerHTML = `
|
||||
<h4>AI Chat</h4>
|
||||
<div data-chat-box style="height: 150px; border: 1px solid #ccc;"></div>
|
||||
<input type="text" data-chat-input style="width: 100%;">
|
||||
<button onclick="handleCustomChat(this)">Send</button>
|
||||
`;
|
||||
container.appendChild(chatDiv);
|
||||
return chatDiv;
|
||||
};
|
||||
|
||||
const chatWidget = mockCreateChatFeature(container);
|
||||
assert(chatWidget.querySelector('h4'), 'Chat widget missing header');
|
||||
assert(chatWidget.querySelector('[data-chat-box]'), 'Chat widget missing chat box');
|
||||
assert(chatWidget.querySelector('[data-chat-input]'), 'Chat widget missing input');
|
||||
assert(chatWidget.querySelector('button'), 'Chat widget missing send button');
|
||||
});
|
||||
|
||||
test('Modal creation generates proper dialog elements', () => {
|
||||
const mockCreateModal = () => {
|
||||
const modal = document.createElement('dialog');
|
||||
modal.id = 'uncloseai-embedded-modal';
|
||||
modal.style.cssText = 'position: fixed; z-index: 2000;';
|
||||
|
||||
const article = document.createElement('article');
|
||||
const header = document.createElement('header');
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.textContent = 'X';
|
||||
|
||||
header.appendChild(closeBtn);
|
||||
article.appendChild(header);
|
||||
modal.appendChild(article);
|
||||
|
||||
return modal;
|
||||
};
|
||||
|
||||
const modal = mockCreateModal();
|
||||
assert(modal.tagName === 'DIALOG', 'Modal is not a dialog element');
|
||||
assert(modal.id === 'uncloseai-embedded-modal', 'Modal has wrong ID');
|
||||
assert(modal.querySelector('header'), 'Modal missing header');
|
||||
assert(modal.querySelector('button'), 'Modal missing close button');
|
||||
});
|
||||
|
||||
// ===== SECTION 5: FUNCTIONAL TESTS =====
|
||||
console.log('\n⚙️ Section 5: Functional Tests');
|
||||
|
||||
test('Toggle function logic works correctly', () => {
|
||||
// Mock the toggle function behavior
|
||||
let modalOpen = false;
|
||||
|
||||
const mockToggle = () => {
|
||||
const existingModal = document.getElementById('test-modal');
|
||||
if (existingModal && modalOpen) {
|
||||
existingModal.remove();
|
||||
modalOpen = false;
|
||||
} else {
|
||||
const modal = document.createElement('dialog');
|
||||
modal.id = 'test-modal';
|
||||
document.body.appendChild(modal);
|
||||
modalOpen = true;
|
||||
}
|
||||
return modalOpen;
|
||||
};
|
||||
|
||||
// Test opening
|
||||
const opened = mockToggle();
|
||||
assert(opened === true, 'Toggle should open modal');
|
||||
assert(document.getElementById('test-modal'), 'Modal should be in DOM when open');
|
||||
|
||||
// Test closing
|
||||
const closed = mockToggle();
|
||||
assert(closed === false, 'Toggle should close modal');
|
||||
assert(!document.getElementById('test-modal'), 'Modal should be removed from DOM when closed');
|
||||
});
|
||||
|
||||
test('Theme detection and styling functions work', () => {
|
||||
// Mock theme detection
|
||||
const mockDetectTheme = () => {
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark' ||
|
||||
window.matchMedia?.('(prefers-color-scheme: dark)').matches;
|
||||
return isDark ? 'dark' : 'light';
|
||||
};
|
||||
|
||||
// Test light theme
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
assert(mockDetectTheme() === 'light', 'Should detect light theme');
|
||||
|
||||
// Test dark theme
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
assert(mockDetectTheme() === 'dark', 'Should detect dark theme');
|
||||
});
|
||||
|
||||
test('File upload input creation works', () => {
|
||||
const mockCreateFileInput = () => {
|
||||
const container = document.createElement('div');
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.style.display = 'none';
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Upload File';
|
||||
button.onclick = () => input.click();
|
||||
|
||||
container.appendChild(input);
|
||||
container.appendChild(button);
|
||||
return { container, input, button };
|
||||
};
|
||||
|
||||
const { container, input, button } = mockCreateFileInput();
|
||||
assert(input.type === 'file', 'Input should be file type');
|
||||
assert(input.style.display === 'none', 'Input should be hidden');
|
||||
assert(button.textContent === 'Upload File', 'Button should have correct text');
|
||||
assert(typeof button.onclick === 'function', 'Button should have click handler');
|
||||
});
|
||||
|
||||
// ===== SECTION 6: INTEGRATION TESTS =====
|
||||
console.log('\n🔗 Section 6: Integration Tests');
|
||||
|
||||
test('Global window exports are properly set', () => {
|
||||
const uiContent = readFile('ui.js');
|
||||
|
||||
const expectedGlobalExports = [
|
||||
'window.openTTSModal',
|
||||
'window.openTranslateModal',
|
||||
'window.toggleUncloseaiEmbeddedModal'
|
||||
];
|
||||
|
||||
expectedGlobalExports.forEach(exp => {
|
||||
assert(uiContent.includes(exp), `ui.js missing global export: ${exp}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Import dependencies are properly structured', () => {
|
||||
const files = ['ui.js', 'widget-library.js', 'uncloseai-embed-modal.js'];
|
||||
|
||||
files.forEach(file => {
|
||||
const content = readFile(file);
|
||||
const imports = extractImports(content);
|
||||
|
||||
// Check that local imports use relative paths
|
||||
imports.forEach(imp => {
|
||||
assert(imp.startsWith('./'), `${file} has non-relative import: ${imp}`);
|
||||
});
|
||||
|
||||
// Check that each imported file exists
|
||||
imports.forEach(imp => {
|
||||
const targetFile = imp.replace('./', '');
|
||||
assert(fileExists(targetFile), `${file} imports non-existent file: ${targetFile}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('No circular dependency patterns detected', () => {
|
||||
const dependencyMap = new Map();
|
||||
const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.js'));
|
||||
|
||||
// Build dependency map
|
||||
files.forEach(file => {
|
||||
const content = readFile(file);
|
||||
const imports = extractImports(content);
|
||||
dependencyMap.set(file, imports.map(imp => imp.replace('./', '')));
|
||||
});
|
||||
|
||||
// Check for direct circular dependencies
|
||||
dependencyMap.forEach((imports, file) => {
|
||||
imports.forEach(importedFile => {
|
||||
if (dependencyMap.has(importedFile)) {
|
||||
const secondLevelImports = dependencyMap.get(importedFile);
|
||||
assert(
|
||||
!secondLevelImports.includes(file),
|
||||
`Circular dependency detected: ${file} -> ${importedFile} -> ${file}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ===== SECTION 7: PERFORMANCE & SIZE TESTS =====
|
||||
console.log('\n📊 Section 7: Performance & Size Tests');
|
||||
|
||||
test('ui.js size reduction is significant', () => {
|
||||
const content = readFile('ui.js');
|
||||
const lines = content.split('\n').length;
|
||||
const bytes = content.length;
|
||||
|
||||
assert(lines < 600, `ui.js is still too large: ${lines} lines (expected < 600)`);
|
||||
assert(bytes < 50000, `ui.js file size too large: ${bytes} bytes (expected < 50KB)`);
|
||||
|
||||
console.log(` ui.js is now ${lines} lines, ${Math.round(bytes/1024)}KB`);
|
||||
});
|
||||
|
||||
test('Extracted modules are substantial', () => {
|
||||
const moduleFiles = ['widget-library.js', 'uncloseai-embed-modal.js', 'tts-modal.js', 'translate-modal.js'];
|
||||
|
||||
moduleFiles.forEach(file => {
|
||||
const content = readFile(file);
|
||||
const lines = content.split('\n').length;
|
||||
|
||||
// Each module should be substantial (removed arbitrary size limits)
|
||||
assert(lines > 50, `${file} seems too small: ${lines} lines`);
|
||||
|
||||
console.log(` ${file}: ${lines} lines`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Language files structure is optimal', () => {
|
||||
const languagesDir = join(srcDir, 'languages');
|
||||
const langFiles = fs.readdirSync(languagesDir).filter(f => f.endsWith('.js'));
|
||||
|
||||
assert(langFiles.length >= 10, `Not enough language files: ${langFiles.length}`);
|
||||
|
||||
// Check that language files follow consistent structure
|
||||
langFiles.forEach(file => {
|
||||
if (file !== 'index.js' && !file.includes('add-missing') && !file.includes('verify')) {
|
||||
const content = readFile(`languages/${file}`);
|
||||
assert(content.includes('export'), `${file} missing export statement`);
|
||||
assert(content.includes('{') && content.includes('}'), `${file} missing object structure`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ===== SECTION 8: ERROR HANDLING TESTS =====
|
||||
console.log('\n🛡️ Section 8: Error Handling Tests');
|
||||
|
||||
test('Modules handle missing dependencies gracefully', () => {
|
||||
// Test that modules don't crash on missing global variables
|
||||
const testCases = [
|
||||
{ var: 'window.UNCLOSEAI_CUSTOM_STYLING', default: false },
|
||||
{ var: 'window.UNCLOSEAI_FLOATING_BUTTON', default: true },
|
||||
{ var: 'window.uncloseaiEmbeddedModalOpen', default: false }
|
||||
];
|
||||
|
||||
testCases.forEach(({ var: varName, default: defaultVal }) => {
|
||||
// Test that default values are properly handled
|
||||
const varCheck = `typeof ${varName} !== 'undefined' ? ${varName} : ${defaultVal}`;
|
||||
|
||||
// This is a conceptual test - in reality, the modules should handle undefined gracefully
|
||||
assert(true, `${varName} should have fallback handling`);
|
||||
});
|
||||
});
|
||||
|
||||
test('DOM manipulation is safe', () => {
|
||||
// Test that element creation doesn't throw errors
|
||||
const testElement = document.createElement('div');
|
||||
testElement.id = 'test-element';
|
||||
testElement.style.cssText = 'position: relative; color: red;';
|
||||
testElement.innerHTML = '<span>Test content</span>';
|
||||
|
||||
assert(testElement.id === 'test-element', 'Element ID setting failed');
|
||||
assert(testElement.querySelector('span'), 'Inner HTML setting failed');
|
||||
assert(testElement.style.position === 'relative', 'Style setting failed');
|
||||
});
|
||||
|
||||
// ===== SECTION 9: MODAL-SPECIFIC ERROR REGRESSION TESTS =====
|
||||
console.log('\n🎭 Section 9: Modal Error Regression Tests');
|
||||
|
||||
test('Modal import functions are properly defined', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Check that all required import wrapper functions exist
|
||||
const requiredFunctions = [
|
||||
'async function* sendMessageWithCustomHistory', // Generator function
|
||||
'async function fetchModelsFromEndpoints',
|
||||
'async function loadConversationHistory',
|
||||
'async function clearConversationHistory',
|
||||
'async function saveConversationHistory',
|
||||
'async function getChatHistory',
|
||||
'async function updateChatHistory',
|
||||
'async function getSelectedModel'
|
||||
];
|
||||
|
||||
requiredFunctions.forEach(func => {
|
||||
assert(modalContent.includes(func), `Modal missing function: ${func}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Modal imports target correct modules', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Test that required imports exist (simplified check)
|
||||
const requiredImports = [
|
||||
{ module: 'models.js', func: 'fetchModelsFromEndpoints' },
|
||||
{ module: 'storage.js', func: 'loadConversationHistory' },
|
||||
{ module: 'storage.js', func: 'clearConversationHistory' },
|
||||
{ module: 'storage.js', func: 'saveConversationHistory' },
|
||||
{ module: 'models.js', func: 'getSelectedModel' },
|
||||
{ module: 'chat.js', func: 'sendMessageWithCustomHistory' }
|
||||
];
|
||||
|
||||
requiredImports.forEach(({ module, func }) => {
|
||||
const hasImport = modalContent.includes(`import("./${module}")`);
|
||||
const hasFunction = modalContent.includes(func);
|
||||
assert(
|
||||
hasImport && hasFunction,
|
||||
`Modal should import ${func} from ${module}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Modal generator function wrapper is correct', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Check that sendMessageWithCustomHistory is properly wrapped as generator
|
||||
assert(
|
||||
modalContent.includes('async function* sendMessageWithCustomHistory'),
|
||||
'sendMessageWithCustomHistory should be async generator function'
|
||||
);
|
||||
|
||||
assert(
|
||||
modalContent.includes('yield* sendMessageWithCustomHistory(history)'),
|
||||
'sendMessageWithCustomHistory should use yield* to delegate to imported generator'
|
||||
);
|
||||
});
|
||||
|
||||
test('Window global functions are exported from ui.js', () => {
|
||||
const uiContent = readFile('ui.js');
|
||||
|
||||
// Check that window globals are properly exported to fix modal button errors
|
||||
const requiredGlobals = [
|
||||
'window.openTTSModal',
|
||||
'window.openTranslateModal',
|
||||
'window.toggleUncloseaiEmbeddedModal'
|
||||
];
|
||||
|
||||
requiredGlobals.forEach(global => {
|
||||
assert(uiContent.includes(global), `ui.js missing global export: ${global}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Modal chat history handling is safe', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Verify that direct chatHistory access is replaced with function calls
|
||||
const lines = modalContent.split('\n');
|
||||
let directChatHistoryAccess = false;
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
// Look for direct chatHistory usage (not in comments)
|
||||
if (line.includes('chatHistory') &&
|
||||
!line.trim().startsWith('//') &&
|
||||
!line.includes('getChatHistory') &&
|
||||
!line.includes('updateChatHistory') &&
|
||||
!line.includes('const') &&
|
||||
!line.includes('function')) {
|
||||
|
||||
// Allow some specific safe patterns
|
||||
if (!line.includes('chatHistory.find') || !line.includes('await getChatHistory()')) {
|
||||
directChatHistoryAccess = true;
|
||||
console.log(` Direct chatHistory access found at line ${index + 1}: ${line.trim()}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert(!directChatHistoryAccess, 'Modal should not directly access chatHistory variable');
|
||||
});
|
||||
|
||||
test('Modal error handling patterns are implemented', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Check for proper error handling in async functions
|
||||
const asyncFunctionPattern = /async function[^{]*{[^}]*}/g;
|
||||
const matches = modalContent.match(asyncFunctionPattern);
|
||||
|
||||
if (matches) {
|
||||
// At least some async functions should have try-catch or error handling
|
||||
const hasTryCatch = modalContent.includes('try {') || modalContent.includes('catch');
|
||||
const hasErrorLog = modalContent.includes('console.error') || modalContent.includes('console.warn');
|
||||
|
||||
assert(
|
||||
hasTryCatch || hasErrorLog,
|
||||
'Modal should have error handling in async functions'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Modal function exports match their imports', () => {
|
||||
const files = ['chat.js', 'storage.js', 'models.js', 'tts.js'];
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
files.forEach(file => {
|
||||
const fileContent = readFile(file);
|
||||
const exports = extractExports(fileContent);
|
||||
|
||||
// Check that functions imported by modal are actually exported
|
||||
exports.forEach(exportedFunc => {
|
||||
if (modalContent.includes(`${exportedFunc}`)) {
|
||||
const importPattern = new RegExp(`await import\\("\\.\/${file}"\\)[^}]*${exportedFunc}`, 'g');
|
||||
if (importPattern.test(modalContent)) {
|
||||
assert(true, `${file} exports ${exportedFunc} which modal imports`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Modal prevents circular dependency with chat.js', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
const chatContent = readFile('chat.js');
|
||||
|
||||
// Modal should only import from chat.js, not vice versa
|
||||
const chatImportsModal = chatContent.includes('./uncloseai-embed-modal.js');
|
||||
assert(!chatImportsModal, 'chat.js should not import from modal to prevent circular dependency');
|
||||
|
||||
// Modal should use dynamic imports from chat.js
|
||||
const modalImportsChat = modalContent.includes('await import("./chat.js")');
|
||||
assert(modalImportsChat, 'Modal should use dynamic imports from chat.js');
|
||||
});
|
||||
|
||||
// ===== SECTION 10: SPECIFIC SESSION ERROR REGRESSION TESTS =====
|
||||
console.log('\n🚨 Section 10: Session Error Regression Tests');
|
||||
|
||||
test('Async generator wrapper functions are correctly implemented', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Test for "sendMessageWithCustomHistory(...) is not iterable" error prevention
|
||||
const generatorPattern = /async function\* sendMessageWithCustomHistory/;
|
||||
const yieldPattern = /yield\* sendMessageWithCustomHistory/;
|
||||
assert(
|
||||
generatorPattern.test(modalContent) && yieldPattern.test(modalContent),
|
||||
'sendMessageWithCustomHistory must be async generator with yield* delegation'
|
||||
);
|
||||
|
||||
// Ensure no plain async function pattern for generators
|
||||
const badAsyncPattern = /async function sendMessageWithCustomHistory\([^)]*\)\s*{[^}]*return[^}]*}/s;
|
||||
assert(
|
||||
!badAsyncPattern.test(modalContent),
|
||||
'sendMessageWithCustomHistory should not be plain async function returning value'
|
||||
);
|
||||
});
|
||||
|
||||
test('Dynamic import wrapper functions handle all error cases', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Check that all wrapper functions properly await imports
|
||||
const dynamicImportFunctions = [
|
||||
'fetchModelsFromEndpoints',
|
||||
'loadConversationHistory',
|
||||
'clearConversationHistory',
|
||||
'saveConversationHistory',
|
||||
'getChatHistory',
|
||||
'updateChatHistory',
|
||||
'getSelectedModel'
|
||||
];
|
||||
|
||||
dynamicImportFunctions.forEach(funcName => {
|
||||
const functionPattern = new RegExp(`async function ${funcName}`);
|
||||
const importPattern = new RegExp(`await import\\(.*\\)`);
|
||||
assert(
|
||||
functionPattern.test(modalContent) && importPattern.test(modalContent),
|
||||
`${funcName} wrapper function must properly await dynamic import`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Module import paths match actual exports', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Check that required imports exist (simplified patterns)
|
||||
const requiredImports = [
|
||||
{ module: 'models.js', func: 'fetchModelsFromEndpoints' },
|
||||
{ module: 'storage.js', func: 'loadConversationHistory' },
|
||||
{ module: 'storage.js', func: 'clearConversationHistory' },
|
||||
{ module: 'storage.js', func: 'saveConversationHistory' },
|
||||
{ module: 'models.js', func: 'getSelectedModel' },
|
||||
{ module: 'chat.js', func: 'sendMessageWithCustomHistory' }
|
||||
];
|
||||
|
||||
requiredImports.forEach(({ module, func }) => {
|
||||
const modulePattern = new RegExp(`await import\\(.*${module}.*\\)`);
|
||||
const funcPattern = new RegExp(`async function.*${func}`);
|
||||
assert(
|
||||
modulePattern.test(modalContent) && funcPattern.test(modalContent),
|
||||
`Missing import: ${func} from ${module}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Variable scope isolation prevents undefined reference errors', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Check for direct variable access that should use function calls
|
||||
const lines = modalContent.split('\n');
|
||||
const problematicPatterns = [
|
||||
{ pattern: /^[^\/]*chatHistory\s*\./, desc: 'Direct chatHistory access' },
|
||||
{ pattern: /^[^\/]*chatHistory\s*\[/, desc: 'Direct chatHistory array access' },
|
||||
{ pattern: /^[^\/]*chatHistory\s*=/, desc: 'Direct chatHistory assignment' }
|
||||
];
|
||||
|
||||
let violations = [];
|
||||
lines.forEach((line, index) => {
|
||||
problematicPatterns.forEach(({ pattern, desc }) => {
|
||||
if (pattern.test(line) &&
|
||||
!line.includes('getChatHistory') &&
|
||||
!line.includes('updateChatHistory') &&
|
||||
!line.includes('const') &&
|
||||
!line.includes('function') &&
|
||||
!line.includes('//')) {
|
||||
violations.push(`Line ${index + 1}: ${desc} - ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
assert(violations.length === 0, `Variable scope violations found:\n${violations.join('\n')}`);
|
||||
});
|
||||
|
||||
test('Runtime function availability is guaranteed', () => {
|
||||
const uiContent = readFile('ui.js');
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Functions that modal expects to be available at runtime
|
||||
const runtimeFunctions = ['openTTSModal', 'openTranslateModal', 'toggleUncloseaiEmbeddedModal'];
|
||||
|
||||
runtimeFunctions.forEach(func => {
|
||||
// Check that ui.js imports the function
|
||||
const importPattern = new RegExp(`import.*${func}.*from`, 'g');
|
||||
const exportPattern = new RegExp(`window\\.${func}\\s*=`, 'g');
|
||||
|
||||
if (func !== 'toggleUncloseaiEmbeddedModal') {
|
||||
assert(
|
||||
importPattern.test(uiContent),
|
||||
`ui.js should import ${func} for global export`
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
exportPattern.test(uiContent),
|
||||
`ui.js should export ${func} to window for modal access`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Module exports are consistent with their usage', () => {
|
||||
const modules = [
|
||||
{ file: 'chat.js', exports: ['sendMessage', 'sendMessageWithCustomHistory', 'getChatHistory', 'updateChatHistory'] },
|
||||
{ file: 'storage.js', exports: ['loadConversationHistory', 'saveConversationHistory', 'clearConversationHistory'] },
|
||||
{ file: 'models.js', exports: ['fetchModelsFromEndpoints', 'getSelectedModel'] },
|
||||
{ file: 'tts-modal.js', exports: ['openTTSModal'] },
|
||||
{ file: 'translate-modal.js', exports: ['openTranslateModal'] }
|
||||
];
|
||||
|
||||
modules.forEach(({ file, exports: expectedExports }) => {
|
||||
const content = readFile(file);
|
||||
expectedExports.forEach(exportName => {
|
||||
const exportPattern = new RegExp(`export\\s+(?:async\\s+)?(?:function\\*?|const|let)\\s+${exportName}`, 'g');
|
||||
assert(
|
||||
exportPattern.test(content),
|
||||
`${file} should export ${exportName} as expected by importing modules`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Async/await patterns are consistently applied', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Find all dynamic import calls and ensure they're properly awaited
|
||||
const importCallPattern = /await import\("[^"]*"\)/g;
|
||||
const importCalls = modalContent.match(importCallPattern) || [];
|
||||
|
||||
assert(importCalls.length > 0, 'Modal should have dynamic imports');
|
||||
|
||||
// Simple check for destructuring patterns in the file
|
||||
const hasDestructuring = modalContent.includes('const {') || modalContent.includes('const{');
|
||||
assert(hasDestructuring, 'Modal should use destructuring for imports');
|
||||
});
|
||||
|
||||
test('Error boundary patterns prevent cascading failures', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
|
||||
// Check for error handling around critical operations
|
||||
const criticalOperations = [
|
||||
'loadHistory',
|
||||
'addIntroMessage',
|
||||
'loadModels',
|
||||
'openUncloseaiEmbeddedModalNew'
|
||||
];
|
||||
|
||||
criticalOperations.forEach(operation => {
|
||||
const functionPattern = new RegExp(`(async\\s+)?function\\s+${operation}[^{]*{([^{}]*{[^{}]*})*[^}]*}`, 's');
|
||||
const match = modalContent.match(functionPattern);
|
||||
|
||||
if (match) {
|
||||
const functionBody = match[0];
|
||||
// Should have some form of error handling
|
||||
const hasErrorHandling =
|
||||
functionBody.includes('try') ||
|
||||
functionBody.includes('catch') ||
|
||||
functionBody.includes('console.error') ||
|
||||
functionBody.includes('console.warn') ||
|
||||
functionBody.includes('Failed to');
|
||||
|
||||
assert(
|
||||
hasErrorHandling,
|
||||
`Function ${operation} should have error handling to prevent cascading failures`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Generator function delegation is properly implemented', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
const chatContent = readFile('chat.js');
|
||||
|
||||
// Verify that chat.js exports a generator function
|
||||
const chatGeneratorPattern = /export\s+async\s+function\*\s+sendMessageWithCustomHistory/;
|
||||
assert(
|
||||
chatGeneratorPattern.test(chatContent),
|
||||
'chat.js should export sendMessageWithCustomHistory as async generator'
|
||||
);
|
||||
|
||||
// Verify that modal properly delegates to it
|
||||
const hasGeneratorFunction = modalContent.includes('async function* sendMessageWithCustomHistory');
|
||||
const hasYieldDelegation = modalContent.includes('yield* sendMessageWithCustomHistory');
|
||||
assert(
|
||||
hasGeneratorFunction && hasYieldDelegation,
|
||||
'Modal should properly delegate to imported generator with yield*'
|
||||
);
|
||||
});
|
||||
|
||||
test('Function signature compatibility across modules', () => {
|
||||
const modalContent = readFile('uncloseai-embed-modal.js');
|
||||
const chatContent = readFile('chat.js');
|
||||
|
||||
// Simple check that both files have the sendMessageWithCustomHistory function
|
||||
const modalHasFunction = modalContent.includes('function* sendMessageWithCustomHistory');
|
||||
const chatHasFunction = chatContent.includes('function* sendMessageWithCustomHistory');
|
||||
|
||||
assert(
|
||||
modalHasFunction && chatHasFunction,
|
||||
'Both modal and chat should have sendMessageWithCustomHistory function'
|
||||
);
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
dom.window.close();
|
||||
|
||||
// ===== FINAL SUMMARY =====
|
||||
console.log('\n📈 Integration Test Summary:');
|
||||
console.log('='.repeat(50));
|
||||
console.log(`Total tests: ${tests}`);
|
||||
console.log(`Passed: ${passed} ✅`);
|
||||
console.log(`Failed: ${failed} ❌`);
|
||||
console.log(`Warnings: ${warnings} ⚠️`);
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('\n🎉 All integration tests passed!');
|
||||
console.log('📦 The refactored modules are properly structured and functional.');
|
||||
console.log('🚀 Safe to deploy!');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n💥 Some tests failed. Please fix the issues above before deploying.');
|
||||
console.log('🔧 Check the failed tests and resolve any import/export issues.');
|
||||
process.exit(1);
|
||||
}
|
||||
379
journal.rst
Normal file
379
journal.rst
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
.. This is free software for the public good of a permacomputer hosted at
|
||||
.. permacomputer.com, an always-on computer by the people, for the people.
|
||||
.. One which is durable, easy to repair, & distributed like tap water
|
||||
.. for machine learning intelligence.
|
||||
..
|
||||
.. The permacomputer is community-owned infrastructure optimized around
|
||||
.. four values:
|
||||
..
|
||||
.. TRUTH First principles, math & science, open source code freely distributed
|
||||
.. FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
.. HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
.. LOVE Be yourself without hurting others, cooperation through natural law
|
||||
..
|
||||
.. This software contributes to that vision by making machine learning
|
||||
.. accessible to everyone through a free, open, embeddable chat interface.
|
||||
.. Code is seeds to sprout on any abandoned technology.
|
||||
|
||||
====================
|
||||
Development Journal
|
||||
====================
|
||||
|
||||
July 2, 2025
|
||||
============
|
||||
|
||||
Token Estimator Implementation
|
||||
-------------------------------
|
||||
|
||||
Built `token_estimator.js` - a simplified BPE-based token counter for translation ETA estimation.
|
||||
|
||||
**Not compatible with tiktoken** - this is estimation-only, ~85% accuracy. Good enough for progress bars, not for exact tokenization.
|
||||
|
||||
**NVIDIA 4090 Performance Metrics:**
|
||||
- Prompt processing: 3,144 tokens/sec
|
||||
- Generation: 121 tokens/sec
|
||||
- Translation estimates: 1K tokens ≈ 11 seconds
|
||||
|
||||
The estimator uses real production metrics from our Hermes machine to provide realistic translation timing.
|
||||
|
||||
July 3, 2025
|
||||
============
|
||||
|
||||
Complete Translation Infrastructure Overhaul
|
||||
--------------------------------------------
|
||||
|
||||
**Modular Language System Implementation**
|
||||
|
||||
Restructured the entire translation system to use individual language files instead of one monolithic object. Each of the 19 supported languages now has its own dedicated JS file in `/src/languages/`.
|
||||
|
||||
**Language Coverage:**
|
||||
- 🇺🇸 English (en.js) - 46 translation keys
|
||||
- 🇨🇳 Chinese Simplified (zh.js)
|
||||
- 🇹🇼 Chinese Traditional (zh-tw.js)
|
||||
- 🇮🇳 Hindi (hi.js)
|
||||
- 🇪🇸 Spanish (es.js)
|
||||
- 🇫🇷 French (fr.js)
|
||||
- 🇸🇦 Arabic (ar.js)
|
||||
- 🇧🇩 Bengali (bn.js)
|
||||
- 🇷🇺 Russian (ru.js)
|
||||
- 🇧🇷 Portuguese (pt.js)
|
||||
- 🇵🇰 Urdu (ur.js)
|
||||
- 🇮🇩 Indonesian (id.js)
|
||||
- 🇩🇪 German (de.js)
|
||||
- 🇯🇵 Japanese (ja.js)
|
||||
- 🇰🇪 Swahili (sw.js)
|
||||
- 🇮🇳 Marathi (mr.js)
|
||||
- 🇮🇳 Telugu (te.js)
|
||||
- 🇹🇷 Turkish (tr.js)
|
||||
- 🇰🇷 Korean (ko.js)
|
||||
|
||||
**Dynamic UI Refresh System**
|
||||
|
||||
Implemented `refreshUILanguage()` function that updates all UI text without requiring browser refresh:
|
||||
|
||||
- **Declarative translations**: Elements with `data-i18n` attributes automatically update
|
||||
- **Parameter support**: `data-i18n-params` for dynamic text with variables like `{lang}` and `{error}`
|
||||
- **Smart element detection**: Handles inputs, buttons, placeholders, and modal titles
|
||||
- **Event system**: Dispatches `languageChanged` custom events for component integration
|
||||
- **Backward compatibility**: Maintains `window.refreshUILanguage` for existing code
|
||||
|
||||
**Translation Verification System**
|
||||
|
||||
Created `verify-translations.js` - a Node.js script that ensures translation consistency:
|
||||
|
||||
- **Key extraction**: Parses JS files to extract translation object keys
|
||||
- **Completeness verification**: Compares all languages against English reference
|
||||
- **Missing/extra key detection**: Reports inconsistencies with colored console output
|
||||
- **Exit codes**: Returns 0 for success, 1 for issues (CI/CD friendly)
|
||||
|
||||
**Adding New Languages - Workflow:**
|
||||
|
||||
1. **Before adding**: Run verification to ensure current state is clean
|
||||
```bash
|
||||
cd /home/fox/git/ai.unturf.com/src/languages
|
||||
node verify-translations.js
|
||||
```
|
||||
Should show: "🎉 All languages have complete translations!"
|
||||
|
||||
2. **Create new language file**: Copy structure from `en.js`
|
||||
```bash
|
||||
cp en.js new-lang.js # Replace 'new-lang' with actual language code
|
||||
```
|
||||
|
||||
3. **Translate all values**: Keep the same keys, translate only the string values
|
||||
|
||||
4. **Update index.js**: Add import and export for the new language
|
||||
```javascript
|
||||
import { newLang } from "./new-lang.js";
|
||||
// Add to UI_TRANSLATIONS object
|
||||
```
|
||||
|
||||
5. **Verify completeness**: Run script again to check for issues
|
||||
```bash
|
||||
node verify-translations.js
|
||||
```
|
||||
|
||||
6. **Fix any issues**: Script will show missing/extra keys in red
|
||||
- Missing keys: Add them with proper translations
|
||||
- Extra keys: Remove them or add to English reference if needed
|
||||
|
||||
7. **Final verification**: Run until you see the success message again
|
||||
|
||||
**File Structure:**
|
||||
```
|
||||
src/languages/
|
||||
├── index.js # Aggregates all translations
|
||||
├── en.js # English reference (46 keys)
|
||||
├── [18 other languages] # Complete translations
|
||||
└── verify-translations.js # Sanity check script
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Maintainability**: Each language isolated in its own file
|
||||
- **Consistency**: 100% key coverage verified across all 19 languages
|
||||
- **Real-time UX**: Language switching without page refresh
|
||||
- **Developer experience**: Automated verification prevents translation drift
|
||||
- **Scalability**: Easy to add new languages following the established pattern
|
||||
|
||||
This system supports the full user journey from language detection to dynamic interface updates, ensuring a seamless multilingual experience for all Hermes AI users.
|
||||
|
||||
July 9, 2025
|
||||
============
|
||||
|
||||
Modal CSS Architecture Overhaul
|
||||
-------------------------------
|
||||
|
||||
**The Problem**
|
||||
|
||||
Users reported multiple issues with the UncloseAI modal experience:
|
||||
|
||||
1. **Desktop Issue**: PicoCSS-powered sites displayed full-screen modals on large laptop displays instead of centered dialogs
|
||||
2. **Translation Pages**: Broken CSS in translated pages opened in new windows made them unusable
|
||||
3. **Mobile Experience**: After initial fixes, mobile devices lost their full-screen modal experience
|
||||
|
||||
**Root Cause Analysis**
|
||||
|
||||
Three interconnected issues created these problems:
|
||||
|
||||
1. **Inconsistent CSS Loading**: Different modal files used different detection methods and URL patterns
|
||||
2. **PicoCSS Overrides**: The framework's aggressive dialog styling required careful specificity management
|
||||
3. **Breakpoint Mismatch**: Mobile detection used 480px while many tablets needed full-screen at 768px
|
||||
|
||||
**Solution: Complete CSS Architecture Refactoring**
|
||||
|
||||
**Step 1: Dedicated CSS Files**
|
||||
|
||||
Created two purpose-built stylesheets to replace all inline styles:
|
||||
|
||||
- ``uncloseai-modal-builtin.css`` - Clean styles for standard blog sites
|
||||
- ``uncloseai-modal-pico.css`` - Override styles with ``!important`` for PicoCSS sites
|
||||
|
||||
**Step 2: Unified CSS Loading Pattern**
|
||||
|
||||
Standardized all modal components to use the same detection and loading logic:
|
||||
|
||||
```javascript
|
||||
// Detect if PicoCSS is actually present on the page
|
||||
const hasPicoCSS = document.querySelector('link[href*="pico"]') !== null;
|
||||
|
||||
// Load appropriate CSS based on actual PicoCSS presence
|
||||
const cssFile = hasPicoCSS ? 'uncloseai-modal-pico.css' : 'uncloseai-modal-builtin.css';
|
||||
|
||||
// Use absolute URLs for cross-domain compatibility
|
||||
link.href = `https://uncloseai.com/src/${cssFile}`;
|
||||
```
|
||||
|
||||
**Step 3: PicoCSS Override Strategy**
|
||||
|
||||
```css
|
||||
/* Override PicoCSS dialog defaults with high specificity */
|
||||
dialog#uncloseai-embedded-modal[data-theme] {
|
||||
position: fixed !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
width: 70vw !important;
|
||||
max-width: 800px !important;
|
||||
}
|
||||
|
||||
/* Mobile/tablet full-screen experience */
|
||||
@media (max-width: 768px) {
|
||||
dialog#uncloseai-embedded-modal[data-theme] {
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Translation Page CSS Fix**
|
||||
|
||||
- Added ``<base href="${baseUrl}">`` tag to resolve relative URLs in new windows
|
||||
- Ensured all modal CSS uses absolute URLs for cross-window compatibility
|
||||
- Maintained original page styling while adding AI functionality
|
||||
|
||||
**Technical Implementation**
|
||||
|
||||
**Files Modified:**
|
||||
- ``uncloseai-embed-modal.js`` - Main modal with PicoCSS detection
|
||||
- ``translate-modal.js`` - Translation modal with absolute URLs
|
||||
- ``tts-modal.js`` - TTS modal with consistent loading
|
||||
- ``widget-library.js`` - Widget components with proper CSS
|
||||
- ``translation.js`` - Base tag injection for new windows
|
||||
- ``uncloseai-modal-builtin.css`` - Standard site styles
|
||||
- ``uncloseai-modal-pico.css`` - PicoCSS override styles
|
||||
|
||||
**Responsive Breakpoints:**
|
||||
- **Desktop (>768px)**: Centered modal (70vw × 90vh, max 800px width)
|
||||
- **Mobile/Tablet (≤768px)**: Full-screen modal (100vw × 100vh)
|
||||
|
||||
**Results**
|
||||
|
||||
✅ **Desktop Experience**: Properly centered modals on large displays
|
||||
✅ **Mobile Experience**: Full-screen modals on phones and tablets
|
||||
✅ **Translation Pages**: Functional CSS in new window contexts
|
||||
✅ **Code Architecture**: Clean separation between JS logic and CSS styling
|
||||
✅ **Cross-Site Compatibility**: Consistent behavior across all site types
|
||||
✅ **Maintainability**: Single source of truth for modal styling
|
||||
|
||||
This comprehensive refactoring established a robust CSS architecture that handles all edge cases while providing an optimal user experience across all devices and contexts.
|
||||
|
||||
July 11, 2025
|
||||
=============
|
||||
|
||||
Custom API Integration & UI Overhaul
|
||||
-------------------------------------
|
||||
|
||||
**Major Features Implemented:**
|
||||
|
||||
Custom API Endpoint Support
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Implemented comprehensive custom OpenAI-compatible API support allowing users to integrate their own AI providers (Groq, OpenAI, Claude, etc.) into the chat system:
|
||||
|
||||
- **Centralized Configuration System**: Created ``getAPIConfig()`` in ``config.js`` to manage API settings
|
||||
- **Universal API Call Patching**: All chat functions now check for custom configuration automatically
|
||||
- **Dynamic Model Discovery**: System fetches available models from custom endpoints via ``/v1/models``
|
||||
- **Intelligent Max Tokens**: Automatically detects context limits from model metadata:
|
||||
- VLLM: ``max_model_len`` (e.g., 82,000 tokens)
|
||||
- OpenAI: ``max_tokens`` (e.g., 8,192 tokens)
|
||||
- Groq: ``context_length`` (e.g., 128,000 tokens)
|
||||
- **Seamless Fallback**: Falls back to default Hermes endpoints when custom API unavailable
|
||||
|
||||
**Settings Panel Complete Redesign**
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Restructured the settings interface for better organization and usability:
|
||||
|
||||
- **Two-Column Grid Layout**: Left column (Model, Voice, Language), Right column (API Configuration)
|
||||
- **Quick Actions Header**: Action buttons span both columns at the top for prominence
|
||||
- **Mobile Responsive**: Automatically stacks to single column on screens ≤768px
|
||||
- **Label Cleanup**: Removed redundant section headers (``AI Model:``, ``TTS Voice:``, etc.) for cleaner look
|
||||
- **Grid Override Fixes**: Added ``!important`` declarations to overcome PicoCSS conflicts
|
||||
|
||||
**Translation System Improvements**
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Enhanced the multilingual system with new keys and cleanup:
|
||||
|
||||
- **Chat Audio Downloads**: Added ``downloadChatAudio`` key for chat message audio
|
||||
- **Custom API Description**: Added ``useCustomAPI`` across all 19 languages: "Use a custom openai compatible endpoint."
|
||||
- **Translation Cleanup**: Removed 5 unused keys (``modelLabel``, ``voiceLabel``, ``languageLabel``, ``quickActions``, ``apiConfigLabel``)
|
||||
- **Validator Updates**: Enhanced verification scripts to handle translation changes
|
||||
|
||||
**Critical Bug Fixes:**
|
||||
|
||||
Model Selection System Overhaul
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Fixed fundamental issues with model selection that were causing API mismatches:
|
||||
|
||||
**Issue**: Users selected Groq models but system sent requests to Hermes models
|
||||
**Root Cause**: Modal model dropdown had no HTML ID, causing ``getSelectedModel()`` to fall back to defaults
|
||||
|
||||
**Solution**:
|
||||
- Added ``id="hermes-model-selection"`` to modal dropdown
|
||||
- Enhanced debug logging to trace model selection flow
|
||||
- Fixed model registry to properly store endpoint relationships
|
||||
|
||||
**Cache System Enhancement**
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Fixed refresh models button not working when custom API settings changed:
|
||||
|
||||
**Issue**: Clicking refresh didn't fetch new models after enabling custom API
|
||||
**Root Cause**: Cache key only considered ``VLLM_ENDPOINTS`` array, not custom API configuration
|
||||
|
||||
**Solution**:
|
||||
- Include custom API settings in cache hash key
|
||||
- Cache now properly invalidates when toggling custom API
|
||||
- Test case: Checking/unchecking custom API produces different model lists
|
||||
|
||||
**Error Handling & Debugging**
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Added comprehensive error tracking and debugging tools:
|
||||
|
||||
- **API Error Details**: Full HTTP status codes and response bodies logged
|
||||
- **Model Selection Tracing**: Debug messages show exactly which model is selected and why
|
||||
- **Configuration Verification**: Clear console indicators for custom vs default API usage
|
||||
- **Max Tokens Logging**: Shows token limits being used for each request type
|
||||
|
||||
**Technical Implementation Details:**
|
||||
|
||||
**Files Modified:**
|
||||
- ``src/config.js`` - Central API configuration with ``getAPIConfig()``
|
||||
- ``src/models.js`` - Enhanced model selection, caching, and max_tokens support
|
||||
- ``src/chat.js`` - Dynamic max_tokens for all chat functions
|
||||
- ``src/translation.js`` - Custom API support with dynamic token limits
|
||||
- ``src/ui.js`` - Intro generation API integration
|
||||
- ``src/uncloseai-embed-modal.js`` - Settings UI redesign and model dropdown ID fix
|
||||
- ``src/uncloseai-modal-*.css`` - Grid layouts and responsive design improvements
|
||||
- ``src/languages/*.js`` - Translation updates across all 19 language files
|
||||
|
||||
**API Call Patching Strategy:**
|
||||
All API functions now follow this pattern:
|
||||
```javascript
|
||||
const apiConfig = await getAPIConfig();
|
||||
if (apiConfig.isCustom) {
|
||||
// Use custom endpoint, API key, and selected model
|
||||
apiUrl = `${apiConfig.endpoint}/chat/completions`;
|
||||
headers = { "Authorization": `Bearer ${apiConfig.apiKey}` };
|
||||
model = apiConfig.model;
|
||||
} else {
|
||||
// Use default Hermes configuration
|
||||
apiUrl = `${getSelectedModelEndpoint()}/chat/completions`;
|
||||
headers = { "Authorization": `Bearer ${API_KEY}` };
|
||||
model = getSelectedModel();
|
||||
}
|
||||
```
|
||||
|
||||
**Dynamic Max Tokens Implementation:**
|
||||
```javascript
|
||||
const maxTokens = getSelectedModelMaxTokens(); // Reads from model registry
|
||||
// Translation: Could be 70,000 for VLLM or 8,192 for Groq
|
||||
// Chat: Adapts to whatever the selected model supports
|
||||
```
|
||||
|
||||
**UI Flow Improvements:**
|
||||
1. User configures custom API (base URL + API key)
|
||||
2. Clicks "🔄 Refresh Models" → Fetches models from both Hermes + custom endpoint
|
||||
3. Selects model from combined dropdown (e.g., ``https://api.groq.com/openai/v1-llama-3.1-8b-instant``)
|
||||
4. All features (chat, translation) automatically use selected custom model
|
||||
|
||||
**Deployment Notes:**
|
||||
- **Backward Compatible**: Existing users continue using Hermes by default
|
||||
- **Progressive Enhancement**: Custom API is opt-in with fallback protection
|
||||
- **Cache Management**: Model cache automatically refreshes when configuration changes
|
||||
- **Cross-Platform**: Works on both modal interface and blog site integration
|
||||
|
||||
**Testing Completed:**
|
||||
- ✅ Custom API integration with Groq, OpenAI endpoints
|
||||
- ✅ Model selection working across modal and blog interfaces
|
||||
- ✅ Dynamic max_tokens based on model capabilities (82k for VLLM, 8k for Groq)
|
||||
- ✅ Cache invalidation when toggling custom API settings
|
||||
- ✅ Mobile responsive 2-column grid layout
|
||||
- ✅ Translation key cleanup across all 19 languages
|
||||
|
||||
This release transforms the AI chat system from a single-provider solution into a flexible, multi-provider platform while maintaining the clean user experience and adding significant debugging capabilities for troubleshooting.
|
||||
1251
package-lock.json
generated
Normal file
1251
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
24
package.json
Normal file
24
package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "ai.unturf.com",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "integration_tests.js",
|
||||
"scripts": {
|
||||
"test": "node integration_tests.js",
|
||||
"test:simple": "node simple_tests.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.27.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jsdom": "^22.0.0",
|
||||
"marked": "^17.0.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@git.unturf.com:2222/engineering/unturf/ai.unturf.com.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": ""
|
||||
}
|
||||
291
public/browser-toys.html
Normal file
291
public/browser-toys.html
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Browser Toys - Machine Learning on Every Page | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/c-examples.html">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html" class="active">Browser Toys</a></li>
|
||||
<li><a href="/inference.html">Inference Setup</a></li>
|
||||
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
|
||||
<li><a href="/crawler.html">Our Crawler</a></li>
|
||||
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>Browser Toys - Machine Learning on Every Page</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 id="intro">Browser Toys</h2>
|
||||
<p>Browser extensions that add the <strong>uncloseai.</strong> floating button to every web page you visit. Lightweight wrappers around <a href="/uncloseai-js.html">uncloseai.js</a> — no server, no backend, no accounts. The extension injects the script tag, and everything else runs client-side in your browser.</p>
|
||||
<p><strong>Current release:</strong> <a href="https://git.unturf.com/engineering/unturf/uncloseai-browser-toys/-/releases/v4.2.0" target="_blank">v4.2.0</a></p>
|
||||
|
||||
<h3 id="extensions">Available Extensions</h3>
|
||||
|
||||
<h4 id="chrome">Chrome / Edge / Brave / Vivaldi / Arc</h4>
|
||||
<p>Manifest V3 extension for all Chromium-based browsers.</p>
|
||||
<p><a href="/downloads/uncloseai-browser-toy-chrome.zip" role="button" download>Download for Chrome (~238 KB)</a></p>
|
||||
<ol>
|
||||
<li>Download and unzip <code>uncloseai-browser-toy-chrome.zip</code></li>
|
||||
<li>Verify the download (see <a href="#checksums">checksums</a> below)</li>
|
||||
<li>Open <code>chrome://extensions</code> and enable <strong>Developer mode</strong> (toggle in top right)</li>
|
||||
<li>Click <strong>Load unpacked</strong> and select the unzipped folder</li>
|
||||
<li>The <strong>uncloseai.</strong> button appears on every page you visit</li>
|
||||
</ol>
|
||||
|
||||
<h4 id="firefox">Firefox</h4>
|
||||
<p>Manifest V2 extension for Firefox 109+ and Firefox-based browsers (LibreWolf, Waterfox).</p>
|
||||
<p><a href="https://addons.mozilla.org/en-US/firefox/addon/uncloseai/" role="button">Install from Firefox Add-ons</a></p>
|
||||
<p><a href="/downloads/uncloseai-browser-toy-firefox.zip" role="button" class="secondary" download>Direct download (~238 KB)</a></p>
|
||||
<details>
|
||||
<summary>Manual install from direct download</summary>
|
||||
<ol>
|
||||
<li>Download <code>uncloseai-browser-toy-firefox.zip</code></li>
|
||||
<li>Verify the download (see <a href="#checksums">checksums</a> below)</li>
|
||||
<li>Open <code>about:debugging#/runtime/this-firefox</code></li>
|
||||
<li>Click <strong>Load Temporary Add-on</strong> and select the downloaded zip file</li>
|
||||
<li>The <strong>uncloseai.</strong> button appears on every page you visit</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<h4 id="safari">Safari</h4>
|
||||
<p>Safari Web Extension for macOS and iOS. Requires Xcode conversion.</p>
|
||||
<p><a href="/downloads/uncloseai-browser-toy-safari.zip" role="button" download>Download Safari source (~238 KB)</a></p>
|
||||
<ol>
|
||||
<li>Download and unzip <code>uncloseai-browser-toy-safari.zip</code></li>
|
||||
<li>Verify the download (see <a href="#checksums">checksums</a> below)</li>
|
||||
<li>Convert to Xcode project:
|
||||
<pre><code class="language-bash">xcrun safari-web-extension-converter /path/to/unzipped-folder \
|
||||
--project-location ./safari-xcode \
|
||||
--app-name "uncloseai" \
|
||||
--bundle-identifier com.uncloseai.browser-toy \
|
||||
--no-open</code></pre></li>
|
||||
<li>Open <code>safari-xcode/uncloseai.xcodeproj</code>, set your signing team, build (Cmd+R)</li>
|
||||
<li>Enable in Safari <strong>Settings > Extensions</strong></li>
|
||||
</ol>
|
||||
|
||||
<h4 id="checksums">Verify your download</h4>
|
||||
<p>Always verify downloads before installing. Checksum files are generated by the build and published alongside the zips.</p>
|
||||
<p><a href="/downloads/SHA256SUMS">SHA256SUMS</a> | <a href="/downloads/MD5SUMS">MD5SUMS</a></p>
|
||||
<h5>v1.0.0 SHA-256</h5>
|
||||
<pre><code>2550bbdd34941b95e7d8944bbf5140b6ef16848c77ce4862f8e52aa9c2091087 uncloseai-browser-toy-chrome.zip
|
||||
9b931277112df639fdec830bc43ea86f23b650980dec4c3c012bdec3735f3abc uncloseai-browser-toy-firefox.zip
|
||||
02546524bc9bdab7337274749f4a59caeb1e8b310514ec41d8a7cb54b5661a8e uncloseai-browser-toy-safari.zip</code></pre>
|
||||
<pre><code class="language-bash"># Download checksums and verify all zips at once
|
||||
curl -sO https://uncloseai.com/downloads/SHA256SUMS
|
||||
sha256sum -c SHA256SUMS
|
||||
|
||||
# Or verify a single file
|
||||
curl -s https://uncloseai.com/downloads/SHA256SUMS | grep chrome | sha256sum -c</code></pre>
|
||||
|
||||
<h3 id="how-it-works">How it works</h3>
|
||||
<ol>
|
||||
<li>The content script injects <code><script src="https://uncloseai.com/uncloseai.js" type="module"></code> into every page</li>
|
||||
<li>If uncloseai.js is already on the page, it skips injection (no duplicates)</li>
|
||||
<li>The floating <strong>uncloseai.</strong> button appears in the bottom-right corner</li>
|
||||
<li>Click it to chat with Hermes about the current page</li>
|
||||
<li>Toggle the extension on or off from the toolbar popup</li>
|
||||
</ol>
|
||||
|
||||
<h3 id="storage">Storage and privacy</h3>
|
||||
<p>These extensions have no backend. Everything runs client-side in your browser.</p>
|
||||
<ul>
|
||||
<li><strong>Chat history</strong> stays in your browser's localStorage, scoped per domain and encrypted by UncloseVault. Conversations on <code>example.com</code> are separate from conversations on <code>github.com</code>. Each site has its own vault with its own password.</li>
|
||||
<li><strong>Settings</strong> (model selection, language preference, voice) are encrypted in the vault.</li>
|
||||
<li><strong>Secrets</strong> (custom API keys, unsandbox keys) are encrypted in the vault.</li>
|
||||
<li><strong>The extension itself</strong> stores one boolean value: enabled on/off. That is the only data the extension touches.</li>
|
||||
</ul>
|
||||
<p>No data is written to localStorage unless encrypted with your password. No data leaves your browser except the messages you choose to send to the public machine learning endpoints at <code>hermes.ai.unturf.com</code>, <code>qwen.ai.unturf.com</code>, and <code>speech.ai.unturf.com</code>.</p>
|
||||
|
||||
<h3 id="encryption">Mandatory Vault Encryption</h3>
|
||||
<p>UncloseVault is mandatory. When you first open the chat, you are prompted to create a vault password before anything else. No conversations, settings, or keys are stored without encryption.</p>
|
||||
<h4>First launch</h4>
|
||||
<ol>
|
||||
<li>Click the <strong>uncloseai.</strong> button on any page</li>
|
||||
<li>The vault gate appears: choose a password (minimum 8 characters)</li>
|
||||
<li>Chat loads after the vault is created</li>
|
||||
<li>All data is encrypted with AES-256 before writing to localStorage</li>
|
||||
</ol>
|
||||
<h4>Returning</h4>
|
||||
<ol>
|
||||
<li>Click the <strong>uncloseai.</strong> button on any page</li>
|
||||
<li>Enter your password to unlock the vault</li>
|
||||
<li>Your encrypted conversations and settings are restored</li>
|
||||
</ol>
|
||||
<h4>What gets encrypted</h4>
|
||||
<ul>
|
||||
<li><strong>Conversation history</strong> — all chat messages, per domain, encrypted before writing to localStorage</li>
|
||||
<li><strong>Settings</strong> — model, voice, language preferences</li>
|
||||
<li><strong>API keys</strong> — custom endpoint credentials</li>
|
||||
<li><strong>Unsandbox keys</strong> — code execution credentials</li>
|
||||
</ul>
|
||||
<h4>How it works</h4>
|
||||
<ul>
|
||||
<li>AES-256 encryption via CryptoJS</li>
|
||||
<li>Password-derived key with device-specific salt (SHA-256)</li>
|
||||
<li>Each website has its own vault; you can use a different password per site</li>
|
||||
<li>Session persistence across page navigations (encrypted session key in localStorage)</li>
|
||||
<li>Lock the vault at any time from Settings to end your session</li>
|
||||
</ul>
|
||||
<p>Without the vault password, conversation history and settings are unreadable. If you forget your password, the encrypted data cannot be recovered.</p>
|
||||
|
||||
<h3 id="security">Security</h3>
|
||||
<p>The extension code has been audited across 6 security categories with 49 automated tests. Findings: 0 critical, 0 high, 0 medium, 0 low, 3 informational.</p>
|
||||
<ul>
|
||||
<li>No <code>innerHTML</code>, <code>eval()</code>, <code>document.write</code>, or <code>new Function()</code></li>
|
||||
<li>No network requests from extension code (all networking is in the injected uncloseai.js)</li>
|
||||
<li>Minimal permissions: <code>storage</code> only (plus host access for injection)</li>
|
||||
<li>Content Security Policy restricts extension pages to <code>script-src 'self'</code></li>
|
||||
<li>Extension stores exactly one value: <code>{enabled: true|false}</code></li>
|
||||
</ul>
|
||||
<h3 id="source">Source Code</h3>
|
||||
<p>📦 <a href="https://git.unturf.com/engineering/unturf/uncloseai-browser-toys" target="_blank">git.unturf.com/engineering/unturf/uncloseai-browser-toys</a></p>
|
||||
<p>Releases: <a href="https://git.unturf.com/engineering/unturf/uncloseai-browser-toys/-/releases" target="_blank">all releases</a></p>
|
||||
<p>Build from source:</p>
|
||||
<pre><code class="language-bash">git clone https://git.unturf.com/engineering/unturf/uncloseai-browser-toys.git
|
||||
cd uncloseai-browser-toys
|
||||
node scripts/build.js # outputs zips to dist/</code></pre>
|
||||
<p>Licensed under <a href="https://git.unturf.com/engineering/unturf/uncloseai-browser-toys/-/blob/master/LICENSE" target="_blank">AGPL-3.0-only</a>.</p>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2026</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Page TOC -->
|
||||
<aside class="sidebar-right">
|
||||
<div class="table-of-contents">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#intro" class="active">Overview</a></li>
|
||||
<li><a href="#extensions">Extensions</a>
|
||||
<ul>
|
||||
<li><a href="#chrome">Chrome / Edge</a></li>
|
||||
<li><a href="#firefox">Firefox</a></li>
|
||||
<li><a href="#safari">Safari</a></li>
|
||||
<li><a href="#checksums">Checksums</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#how-it-works">How it works</a></li>
|
||||
<li><a href="#storage">Storage & Privacy</a></li>
|
||||
<li><a href="#encryption">Mandatory Vault</a></li>
|
||||
<li><a href="#security">Security Audit</a></li>
|
||||
<li><a href="#source">Source Code</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
407
public/c-examples.html
Normal file
407
public/c-examples.html
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>C Examples | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/c-examples.html" class="active">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html">Browser Toys</a></li>
|
||||
<li><a href="/inference.html">Inference Setup</a></li>
|
||||
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
|
||||
<li><a href="/crawler.html">Our Crawler</a></li>
|
||||
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>C Examples - Free LLM & TTS AI Service</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 id="c-intro">C Examples</h2>
|
||||
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with C using libcurl. All examples use the OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
|
||||
|
||||
<p><strong>Available Endpoints:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
|
||||
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
|
||||
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="c-setup">Setup & Dependencies</h3>
|
||||
<p>Install libcurl development libraries:</p>
|
||||
<pre><code class="bash"># Ubuntu/Debian
|
||||
sudo apt-get install libcurl4-openssl-dev
|
||||
|
||||
# macOS
|
||||
brew install curl
|
||||
|
||||
# Alpine
|
||||
apk add curl-dev</code></pre>
|
||||
|
||||
<p>Compile with:</p>
|
||||
<pre><code class="bash">gcc -o chat chat.c -lcurl</code></pre>
|
||||
|
||||
<h2 id="c-non-streaming">Non-Streaming Examples</h2>
|
||||
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
|
||||
|
||||
<h3 id="c-hermes-non-stream">Using Hermes (General Purpose)</h3>
|
||||
<pre><code class="c">#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
struct Memory {
|
||||
char *response;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
static size_t write_callback(void *data, size_t size, size_t nmemb, void *userp) {
|
||||
size_t realsize = size * nmemb;
|
||||
struct Memory *mem = (struct Memory *)userp;
|
||||
char *ptr = realloc(mem->response, mem->size + realsize + 1);
|
||||
if (!ptr) return 0;
|
||||
mem->response = ptr;
|
||||
memcpy(&(mem->response[mem->size]), data, realsize);
|
||||
mem->size += realsize;
|
||||
mem->response[mem->size] = 0;
|
||||
return realsize;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
struct Memory chunk = {.response = malloc(1), .size = 0};
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
const char *payload =
|
||||
"{"
|
||||
"\"model\": \"adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic\","
|
||||
"\"messages\": [{\"role\": \"user\", \"content\": \"Give a Python Fizzbuzz solution in one line of code?\"}],"
|
||||
"\"max_tokens\": 150"
|
||||
"}";
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, "https://hermes.ai.unturf.com/v1/chat/completions");
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
if (res == CURLE_OK) {
|
||||
printf("Response:\n%s\n", chunk.response);
|
||||
}
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
free(chunk.response);
|
||||
return 0;
|
||||
}</code></pre>
|
||||
|
||||
<h3 id="c-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
|
||||
<pre><code class="c">#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
// ... same write_callback and Memory struct as above ...
|
||||
|
||||
int main(void) {
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
struct Memory chunk = {.response = malloc(1), .size = 0};
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
const char *payload =
|
||||
"{"
|
||||
"\"model\": \"hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M\","
|
||||
"\"messages\": [{\"role\": \"user\", \"content\": \"Explain quicksort in C with code\"}],"
|
||||
"\"max_tokens\": 500"
|
||||
"}";
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, "https://qwen.ai.unturf.com/v1/chat/completions");
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
if (res == CURLE_OK) {
|
||||
printf("Response:\n%s\n", chunk.response);
|
||||
}
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
free(chunk.response);
|
||||
return 0;
|
||||
}</code></pre>
|
||||
|
||||
<h2 id="c-streaming">Streaming Examples</h2>
|
||||
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
|
||||
|
||||
<h3 id="c-hermes-stream">Streaming with Hermes</h3>
|
||||
<pre><code class="c">#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
static size_t stream_callback(void *data, size_t size, size_t nmemb, void *userp) {
|
||||
size_t realsize = size * nmemb;
|
||||
char *chunk = (char *)data;
|
||||
|
||||
// Process SSE data lines
|
||||
char *line = strtok(chunk, "\n");
|
||||
while (line) {
|
||||
if (strncmp(line, "data: ", 6) == 0) {
|
||||
const char *json = line + 6;
|
||||
if (strcmp(json, "[DONE]") != 0) {
|
||||
// Extract content from: {"choices":[{"delta":{"content":"..."}}]}
|
||||
const char *content_start = strstr(json, "\"content\":\"");
|
||||
if (content_start) {
|
||||
content_start += 11;
|
||||
const char *content_end = strchr(content_start, '"');
|
||||
if (content_end) {
|
||||
fwrite(content_start, 1, content_end - content_start, stdout);
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
line = strtok(NULL, "\n");
|
||||
}
|
||||
return realsize;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
const char *payload =
|
||||
"{"
|
||||
"\"model\": \"adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic\","
|
||||
"\"messages\": [{\"role\": \"user\", \"content\": \"Write a short poem about coding\"}],"
|
||||
"\"max_tokens\": 200,"
|
||||
"\"stream\": true"
|
||||
"}";
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, "https://hermes.ai.unturf.com/v1/chat/completions");
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, stream_callback);
|
||||
|
||||
printf("Streaming response:\n");
|
||||
curl_easy_perform(curl);
|
||||
printf("\n");
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
return 0;
|
||||
}</code></pre>
|
||||
|
||||
<h2 id="c-tts">Text-to-Speech Examples</h2>
|
||||
<p>Generate audio from text using the TTS endpoint.</p>
|
||||
|
||||
<h3 id="c-tts-basic">Basic TTS</h3>
|
||||
<pre><code class="c">#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
static size_t write_file(void *data, size_t size, size_t nmemb, void *userp) {
|
||||
return fwrite(data, size, nmemb, (FILE *)userp);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
FILE *fp = fopen("output.mp3", "wb");
|
||||
if (!fp) {
|
||||
curl_easy_cleanup(curl);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
const char *payload =
|
||||
"{"
|
||||
"\"model\": \"tts-1\","
|
||||
"\"input\": \"Hello! This is a test of text to speech.\","
|
||||
"\"voice\": \"alloy\""
|
||||
"}";
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, "https://speech.ai.unturf.com/v1/audio/speech");
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
if (res == CURLE_OK) {
|
||||
printf("Audio saved to output.mp3\n");
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
return 0;
|
||||
}</code></pre>
|
||||
|
||||
<p><strong>Available voices:</strong> <code>alloy</code>, <code>echo</code>, <code>fable</code>, <code>onyx</code>, <code>nova</code>, <code>shimmer</code></p>
|
||||
|
||||
<h2 id="c-source">Full SDK Implementation</h2>
|
||||
<p>For a complete SDK with model discovery and advanced features, see the full implementations in the source repository:</p>
|
||||
<ul>
|
||||
<li><a href="/languages/c/curl/" target="_blank">C with libcurl</a></li>
|
||||
<li><a href="/languages/c/libsoup/" target="_blank">C with libsoup (GNOME)</a></li>
|
||||
<li><a href="/languages/c/nghttp2/" target="_blank">C with nghttp2 (HTTP/2)</a></li>
|
||||
</ul>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Page TOC -->
|
||||
<aside class="sidebar-right">
|
||||
<div class="table-of-contents">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#c-intro" class="active">Introduction</a></li>
|
||||
<li><a href="#c-setup">Setup</a></li>
|
||||
<li><a href="#c-non-streaming">Non-Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#c-hermes-non-stream">Hermes</a></li>
|
||||
<li><a href="#c-qwen-non-stream">Qwen Coder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#c-streaming">Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#c-hermes-stream">Hermes</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#c-tts">Text-to-Speech</a></li>
|
||||
<li><a href="#c-source">Full SDK</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
426
public/cli.html
Normal file
426
public/cli.html
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>uncloseai-cli: Local LLM Agent | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/c-examples.html">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html" class="active">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html">Browser Toys</a></li>
|
||||
<li><a href="/inference.html">Inference Setup</a></li>
|
||||
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
|
||||
<li><a href="/crawler.html">Our Crawler</a></li>
|
||||
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>uncloseai-cli: Local LLM Agent</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 id="overview">Three Tools for Growing Machine Learning from Seed</h2>
|
||||
|
||||
<p>The <strong>uncloseai-cli</strong> repository contains three tools that form a complete local ML pipeline: agent harness, model training, and dataset curation. Zero external dependencies. Pure Python. Public domain.</p>
|
||||
|
||||
<ul>
|
||||
<li><strong><a href="#agent">uncloseai-cli</a></strong>: ReAct agent harness powered by Unclose Machine Learning (Llama 3.1 8B)</li>
|
||||
<li><strong><a href="#microgpt">microgpt</a></strong>: Pure-Python GPT trainer and inference, zero dependencies</li>
|
||||
<li><strong><a href="#garden">garden.mk</a></strong>: Smol model garden. Pull datasets, grow models, track origins</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="install">Install</h3>
|
||||
<pre><code class="language-bash">git clone https://git.unturf.com/engineering/unturf/uncloseai-cli.git
|
||||
cd uncloseai-cli
|
||||
make install</code></pre>
|
||||
<p>Creates <code>uncloseai-cli</code>, <code>unclose</code>, <code>u</code>, <code>microgpt-cli</code>, <code>microgpt</code> in <code>/usr/local/bin</code>.</p>
|
||||
|
||||
<p>Source: <a href="https://git.unturf.com/engineering/unturf/uncloseai-cli" target="_blank">git.unturf.com/engineering/unturf/uncloseai-cli</a></p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2 id="agent">uncloseai-cli: Local LLM Agent</h2>
|
||||
|
||||
<p>A minimal "Claude Code" style tool-calling agent powered by a local 8B parameter LLM. Every request flows through a todo system:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Plan</strong>: LLM breaks request into numbered tasks</li>
|
||||
<li><strong>Trim</strong>: Python removes fluff (open/read, report/inform), caps at 5</li>
|
||||
<li><strong>Execute</strong>: each task runs through a ReAct loop with tool access</li>
|
||||
<li><strong>Forward</strong>: prior task results flow to later tasks as context</li>
|
||||
</ol>
|
||||
|
||||
<h3 id="usage">Usage</h3>
|
||||
<pre><code class="language-bash"># Run a task
|
||||
unclose "pull and sync"
|
||||
|
||||
# Multi-step: agent plans and executes each step
|
||||
unclose "what time is it in EST? also run ddate"
|
||||
|
||||
# Print-only mode (minimal output, just final answer)
|
||||
unclose -p "summarize SYSTEM-PROMPT.md"
|
||||
|
||||
# Verbose mode (show turn numbers)
|
||||
unclose -v "deploy"
|
||||
|
||||
# Interactive REPL
|
||||
unclose -i</code></pre>
|
||||
|
||||
<h3 id="config">Configuration</h3>
|
||||
<p>All configuration via environment variables. No config files needed:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>UNCLOSE_BASE</code></td><td><code>https://hermes.ai.unturf.com/v1</code></td><td>OpenAI-compatible API base URL</td></tr>
|
||||
<tr><td><code>UNCLOSE_MODEL</code></td><td><em>(auto-detected)</em></td><td>Model name from /v1/models</td></tr>
|
||||
<tr><td><code>UNCLOSE_KEY</code></td><td><code>permacomputer</code></td><td>API key</td></tr>
|
||||
<tr><td><code>UNCLOSE_MAX_TURNS</code></td><td><code>15</code></td><td>Max ReAct turns per task</td></tr>
|
||||
<tr><td><code>UNCLOSE_MAX_RESULT</code></td><td><code>12000</code></td><td>Max chars per tool result</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Point it at any OpenAI-compatible endpoint: vLLM, Ollama, or the free <a href="/inference.html">uncloseai inference endpoints</a>. By default it connects to <a href="https://hermes.ai.unturf.com/v1/models" target="_blank">hermes.ai.unturf.com</a>. See <a href="/inference.html#model-discovery">Model Discovery</a> for all available endpoints and how to query current model IDs.</p>
|
||||
|
||||
<h3 id="tools">Tools</h3>
|
||||
<p>The agent has access to 8 built-in tools:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Tool</th><th>Args</th><th>Description</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>bash</code></td><td>command</td><td>Run a shell command (60s timeout, dangerous commands blocked)</td></tr>
|
||||
<tr><td><code>read</code></td><td>path</td><td>Read a file (truncated at 12K chars)</td></tr>
|
||||
<tr><td><code>write</code></td><td>path, content</td><td>Create or overwrite a file</td></tr>
|
||||
<tr><td><code>edit</code></td><td>path, old, new</td><td>Replace exact string in file</td></tr>
|
||||
<tr><td><code>glob</code></td><td>pattern, path</td><td>Find files by glob pattern</td></tr>
|
||||
<tr><td><code>grep</code></td><td>pattern, path</td><td>Search file contents with regex</td></tr>
|
||||
<tr><td><code>fetch</code></td><td>url, depth, keywords</td><td>Fetch web page, extract text (async crawler)</td></tr>
|
||||
<tr><td><code>todo_add</code></td><td>content, activeForm</td><td>Add task to live todo list during execution</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 id="fetch">Web Fetching</h3>
|
||||
<p>The <code>fetch</code> tool wraps a production-grade async web crawler:</p>
|
||||
<ul>
|
||||
<li><strong>Single page</strong>: fetches, strips HTML, returns readable text + links</li>
|
||||
<li><strong>Deep crawl</strong>: keyword-aware multi-level crawl (up to 10 pages)</li>
|
||||
<li><strong>Ethical</strong>: respects robots.txt, crawl delays, clear user-agent string</li>
|
||||
<li><strong>Progressive</strong>: fetches target page first, auto-crawls deeper only if keywords aren't covered</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="session-logging">Session Logging</h3>
|
||||
<p>Every session is logged as JSONL to <code>~/.uncloseai/sessions/</code>. Use <code>unclose-snoop</code> to parse session logs into a readable feed:</p>
|
||||
<pre><code class="language-bash">cat ~/.uncloseai/sessions/*.jsonl | unclose-snoop</code></pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2 id="microgpt">microgpt: Pure-Python GPT</h2>
|
||||
|
||||
<p>Minimal GPT training & inference with <strong>zero external dependencies</strong>: only <code>os</code>, <code>math</code>, <code>random</code>, <code>json</code>, <code>argparse</code>. Based on <a href="https://gist.github.com/karpathy/8627fe009c40f57531cb18360106ce95" target="_blank">Karpathy's microgpt</a>.</p>
|
||||
|
||||
<p>Implements: scalar autograd (Value class), multi-head attention, RMSNorm, MLP, Adam optimizer with linear LR decay, temperature-controlled sampling. JSON model persistence with full metadata.</p>
|
||||
|
||||
<h3 id="microgpt-usage">Usage</h3>
|
||||
<pre><code class="language-bash"># Train on a text dataset (one document per line)
|
||||
microgpt train --dataset names.txt --steps 1024 --save model.json
|
||||
|
||||
# Generate samples from a trained model
|
||||
microgpt generate --load model.json --samples 10 --temperature 0.5
|
||||
|
||||
# Train & generate in one shot
|
||||
microgpt run --dataset names.txt --steps 1024 --samples 10
|
||||
|
||||
# Inspect a saved model
|
||||
microgpt info model.json</code></pre>
|
||||
|
||||
<h3 id="microgpt-flags">Flags</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Flag</th><th>Default</th><th>Subcommands</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>--dataset PATH</code></td><td>auto-download names.txt</td><td>train, run</td></tr>
|
||||
<tr><td><code>--save PATH</code></td><td>microgpt-model.json</td><td>train, run</td></tr>
|
||||
<tr><td><code>--load PATH</code></td><td>(required)</td><td>generate</td></tr>
|
||||
<tr><td><code>--steps N</code></td><td>1000</td><td>train, run</td></tr>
|
||||
<tr><td><code>--lr FLOAT</code></td><td>0.01</td><td>train, run</td></tr>
|
||||
<tr><td><code>--n-embd N</code></td><td>16</td><td>train, run</td></tr>
|
||||
<tr><td><code>--n-head N</code></td><td>4</td><td>train, run</td></tr>
|
||||
<tr><td><code>--n-layer N</code></td><td>1</td><td>train, run</td></tr>
|
||||
<tr><td><code>--block-size N</code></td><td>16</td><td>train, run</td></tr>
|
||||
<tr><td><code>--samples N</code></td><td>20</td><td>generate, run</td></tr>
|
||||
<tr><td><code>--temperature FLOAT</code></td><td>0.5</td><td>generate, run</td></tr>
|
||||
<tr><td><code>--seed N</code></td><td>42</td><td>all</td></tr>
|
||||
<tr><td><code>--quiet</code></td><td>false</td><td>train, run</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Also ships as <code>microgpt.h</code>, a single-header C library for model inference.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2 id="garden">garden.mk: Smol Model Garden</h2>
|
||||
|
||||
<p>Pull 15 curated character-level datasets, grow GPT models at 3 sizes. Every dataset tracks its upstream origin. Mirrored to <a href="https://huggingface.co/datasets/russellbal/smol-seeds" target="_blank">HuggingFace: russellbal/smol-seeds</a>.</p>
|
||||
|
||||
<h3 id="datasets">Datasets</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Seed</th><th>Lines</th><th>Origin</th><th>Freshness</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>names</td><td>32K</td><td>karpathy/makemore</td><td>Dormant (2022)</td></tr>
|
||||
<tr><td>words</td><td>97K</td><td>dwyl/english-words</td><td>Stable (2025)</td></tr>
|
||||
<tr><td>pokemon</td><td>1K</td><td>sindresorhus/pokemon</td><td>Stable (2024)</td></tr>
|
||||
<tr><td>dinosaurs</td><td>1.5K</td><td>brunoklein99/deep-learning-notes</td><td>Fossil (2018)</td></tr>
|
||||
<tr><td>hex-colors</td><td>32K</td><td>xkcd + meodai/color-names</td><td>Active</td></tr>
|
||||
<tr><td>color-names</td><td>31K</td><td>meodai/color-names</td><td>Active</td></tr>
|
||||
<tr><td>chords</td><td>1K</td><td>tombatossals/chords-db</td><td>Quarterly</td></tr>
|
||||
<tr><td>json-keys</td><td>4K</td><td>GitHub OpenAPI spec</td><td>Active</td></tr>
|
||||
<tr><td>css-classes</td><td>2K</td><td>twbs/bootstrap</td><td>Quarterly</td></tr>
|
||||
<tr><td>make-targets</td><td>291</td><td>scraped from major repos</td><td>Active</td></tr>
|
||||
<tr><td>commit-msgs</td><td>20K</td><td>angular/angular</td><td>Active</td></tr>
|
||||
<tr><td>haiku</td><td>143K</td><td>docmarionum1/haikurnn</td><td>Fossil (2018)</td></tr>
|
||||
<tr><td>variable-names</td><td>3.8K</td><td>GitHub repo trees</td><td>Active</td></tr>
|
||||
<tr><td>last-names</td><td>225K</td><td>sacrificialpancakes/synthetic_demographics_seed</td><td>Stable</td></tr>
|
||||
<tr><td>occupations</td><td>831</td><td>sacrificialpancakes/synthetic_demographics_seed</td><td>Stable</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 id="garden-usage">Usage</h3>
|
||||
<pre><code class="language-bash"># Download all datasets
|
||||
make -f garden.mk pull
|
||||
|
||||
# Train all models at embedding size 64
|
||||
make -f garden.mk grow-64
|
||||
|
||||
# Train all at all sizes (64, 128, 512)
|
||||
make -f garden.mk grow
|
||||
|
||||
# Pull + grow one model family
|
||||
make -f garden.mk names
|
||||
|
||||
# List upstream sources
|
||||
make -f garden.mk origins
|
||||
|
||||
# Check upstream freshness
|
||||
make -f garden.mk freshness
|
||||
|
||||
# Sample from a trained model
|
||||
make -f garden.mk sample MODEL=pokemon-64 N=10
|
||||
|
||||
# Show trained model inventory
|
||||
make -f garden.mk inventory
|
||||
|
||||
# Push datasets to HuggingFace mirror
|
||||
make -f garden.mk mirror</code></pre>
|
||||
|
||||
<h3 id="model-sizes">Model Sizes</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Size</th><th>n_embd</th><th>n_head</th><th>n_layer</th><th>Use</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>64</td><td>64</td><td>4</td><td>1</td><td>Fast, good for testing</td></tr>
|
||||
<tr><td>128</td><td>128</td><td>8</td><td>2</td><td>Balanced</td></tr>
|
||||
<tr><td>512</td><td>512</td><td>8</td><td>4</td><td>Slow in pure Python, best quality</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2 id="architecture">Architecture</h2>
|
||||
|
||||
<p>The agent orchestration follows a plan-execute-forward pattern:</p>
|
||||
|
||||
<pre><code>User Message
|
||||
↓
|
||||
Plan Phase (LLM call with planning-only prompt)
|
||||
↓
|
||||
JSON task array
|
||||
↓
|
||||
Trim Phase (Python, no LLM)
|
||||
• drop fluff (report/inform)
|
||||
• drop prep (open/read)
|
||||
↓
|
||||
Save to ~/.uncloseai/todos/{session}.json
|
||||
↓
|
||||
┌─→ Any pending tasks? ─── no → Display final todo list ✓
|
||||
│ ↓ yes
|
||||
│ Mark in_progress
|
||||
│ ↓
|
||||
│ ReAct Loop (tool calls until done or stuck)
|
||||
│ ↓
|
||||
│ Mark completed → Save → Append result (≤500 chars)
|
||||
└───────┘</code></pre>
|
||||
|
||||
<p>Each task in the ReAct loop can call tools, receive results, and iterate up to <code>UNCLOSE_MAX_TURNS</code> times. Stuck-loop detection bails out after 2 identical consecutive tool calls.</p>
|
||||
|
||||
<p>Prior task results are forwarded as context to later tasks, so multi-step requests build on earlier work without re-executing commands.</p>
|
||||
|
||||
<h3 id="read-only">Read-Only Mode</h3>
|
||||
<p>When the agent detects a read-only intent (questions, searches, inspections), it automatically disables <code>write</code> and <code>edit</code> tools. This prevents accidental file modifications on information-gathering requests.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2 id="license">License</h2>
|
||||
<p>Public domain. Knowledge unbound by gatekeepers.</p>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current section on scroll
|
||||
const sections = document.querySelectorAll('h2[id], h3[id]');
|
||||
const navLinks = document.querySelectorAll('.table-of-contents a');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
if (window.scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Page TOC -->
|
||||
<aside class="sidebar-right">
|
||||
<div class="table-of-contents">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#overview" class="active">Overview</a></li>
|
||||
<li><a href="#agent">uncloseai-cli</a></li>
|
||||
<li><a href="#tools">Tools</a></li>
|
||||
<li><a href="#microgpt">microgpt</a></li>
|
||||
<li><a href="#garden">garden.mk</a></li>
|
||||
<li><a href="#datasets">Datasets</a></li>
|
||||
<li><a href="#architecture">Architecture</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
268
public/crawler.html
Normal file
268
public/crawler.html
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Our Ethical Web Crawler | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/c-examples.html">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html">Browser Toys</a></li>
|
||||
<li><a href="/inference.html">Inference Setup</a></li>
|
||||
<li><a href="/crawler.html" class="active">Our Crawler</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>Our Ethical Web Crawler</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 id="intro">About Our Web Crawler</h2>
|
||||
<p>Our team uses an ethical web crawler for research and development on the open web. We believe in responsible data collection that respects website owners and follows industry best practices.</p>
|
||||
|
||||
<h3 id="user-agent">User Agent</h3>
|
||||
<p>Our crawler identifies itself with the following user agent:</p>
|
||||
<pre><code>uncloseai.com/1.42 (ethical web crawler; +https://uncloseai.com)</code></pre>
|
||||
<p>This clearly identifies who we are and provides a link back to this page for more information.</p>
|
||||
|
||||
<h3 id="what-we-do">What We Do</h3>
|
||||
<p>Our crawler is used by our AI systems to:</p>
|
||||
<ul>
|
||||
<li><strong>Answer questions</strong> - When users ask about websites or specific information, our bot can fetch and analyze web content to provide accurate answers</li>
|
||||
<li><strong>Research & development</strong> - We study how information is structured on the web to improve our AI models and tools</li>
|
||||
<li><strong>Content aggregation</strong> - We help users discover and understand information across multiple web pages</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ethical-practices">Our Ethical Practices</h3>
|
||||
<p>We follow strict ethical guidelines to ensure we're good web citizens:</p>
|
||||
|
||||
<h4>1. Robots.txt Compliance</h4>
|
||||
<p>We <strong>always respect robots.txt</strong> files. If your site's robots.txt disallows our crawler, we won't access those pages.</p>
|
||||
<pre><code># Example: Block our crawler from specific paths
|
||||
User-agent: uncloseai.com
|
||||
Disallow: /private/
|
||||
Disallow: /admin/</code></pre>
|
||||
|
||||
<h4>2. Crawl Delays</h4>
|
||||
<p>We respect crawl delays specified in robots.txt. Our default is <strong>2 seconds between requests</strong> to the same domain, but we'll honor any delay you specify:</p>
|
||||
<pre><code># Example: Set custom crawl delay
|
||||
User-agent: uncloseai.com
|
||||
Crawl-delay: 5</code></pre>
|
||||
|
||||
<h4>3. Intelligent Caching</h4>
|
||||
<p>We cache fetched content for <strong>7 days</strong> by default. This means:</p>
|
||||
<ul>
|
||||
<li>We won't repeatedly hit your server for the same content</li>
|
||||
<li>Reduces load on your infrastructure</li>
|
||||
<li>Faster responses for our users</li>
|
||||
</ul>
|
||||
|
||||
<h4>4. Smart Depth Crawling</h4>
|
||||
<p>We use intelligent depth-based crawling:</p>
|
||||
<ul>
|
||||
<li><strong>Depth 0</strong> - Just the homepage</li>
|
||||
<li><strong>Depth 1</strong> - Homepage plus directly linked pages</li>
|
||||
<li><strong>Depth 2</strong> - Goes two levels deep when needed</li>
|
||||
</ul>
|
||||
<p>Our crawler automatically decides how deep to go based on whether it found relevant information, so we don't waste resources fetching unnecessary pages.</p>
|
||||
|
||||
<h4>5. Query-Aware Relevance</h4>
|
||||
<p>We score pages based on relevance to the user's question, prioritizing:</p>
|
||||
<ul>
|
||||
<li>Pages that contain keywords from the user's query</li>
|
||||
<li>Clean, readable URLs over dynamic query strings</li>
|
||||
<li>Pages with substantial, unique content</li>
|
||||
<li>Descriptive titles that match the topic</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="transparency">Transparency</h3>
|
||||
<p>We believe in being open about our crawling practices:</p>
|
||||
<ul>
|
||||
<li><strong>Clear identification</strong> - Our user agent clearly identifies us</li>
|
||||
<li><strong>Contact information</strong> - This page provides details on how to reach us</li>
|
||||
<li><strong>Open source approach</strong> - Our crawler follows public best practices</li>
|
||||
<li><strong>No stealth crawling</strong> - We never hide our identity or bypass restrictions</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="blocking">How to Block Our Crawler</h3>
|
||||
<p>If you don't want our crawler accessing your site, you can block it using robots.txt:</p>
|
||||
<pre><code># Block uncloseai.com crawler entirely
|
||||
User-agent: uncloseai.com
|
||||
Disallow: /</code></pre>
|
||||
<p>Or use your server's firewall/WAF to block our user agent string.</p>
|
||||
|
||||
<h3 id="contact">Questions or Concerns?</h3>
|
||||
<p>If you have questions about our crawler or want to discuss our access to your site, please reach out:</p>
|
||||
<ul>
|
||||
<li><strong>Website</strong>: <a href="https://uncloseai.com">uncloseai.com</a></li>
|
||||
<li><strong>Discord</strong>: Join our community at <a href="https://discord.com/invite/TZPmQdPj8a" target="_blank">discord.com/invite/TZPmQdPj8a</a></li>
|
||||
<li><strong>Email</strong>: Contact us through our website</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="discord-bot">Try Our Discord Bot</h3>
|
||||
<p>Our ethical web crawler powers the research capabilities in our Discord bot. The bot can:</p>
|
||||
<ul>
|
||||
<li>Answer questions by fetching and analyzing web content</li>
|
||||
<li>Execute code in 42+ programming languages</li>
|
||||
<li>Provide intelligent summaries of web pages</li>
|
||||
<li>Respect user privacy and website policies</li>
|
||||
</ul>
|
||||
<p><strong>Want to try it?</strong> Check out our <a href="https://git.unturf.com/engineering/unturf/discord.agents.ai.unturf.com" target="_blank">Discord bot source code</a> to see how our ethical crawler powers the bot's research capabilities.</p>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current section on scroll
|
||||
const sections = document.querySelectorAll('h2[id], h3[id], h4[id]');
|
||||
const navLinks = document.querySelectorAll('.table-of-contents a');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
if (window.scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Page TOC -->
|
||||
<aside class="sidebar-right">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#intro" class="active">About</a></li>
|
||||
<li><a href="#user-agent">User Agent</a></li>
|
||||
<li><a href="#what-we-do">What We Do</a></li>
|
||||
<li><a href="#ethical-practices">Ethical Practices</a></li>
|
||||
<li><a href="#transparency">Transparency</a></li>
|
||||
<li><a href="#blocking">How to Block</a></li>
|
||||
<li><a href="#contact">Contact</a></li>
|
||||
<li><a href="#discord-bot">Discord Bot</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
446
public/csharp-examples.html
Normal file
446
public/csharp-examples.html
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>C# Examples | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/c-examples.html">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html" class="active">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html">Browser Toys</a></li>
|
||||
<li><a href="/inference.html">Inference Setup</a></li>
|
||||
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
|
||||
<li><a href="/crawler.html">Our Crawler</a></li>
|
||||
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>C# Examples - Free LLM & TTS AI Service</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 id="csharp-intro">C# Examples</h2>
|
||||
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with C#. Examples are provided for both <strong>.NET 10</strong> (latest) and <strong>Mono</strong> (cross-platform) runtimes.</p>
|
||||
|
||||
<p><strong>Available Endpoints:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
|
||||
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
|
||||
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="runtimes">Runtime Options</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Runtime</th>
|
||||
<th>Best For</th>
|
||||
<th>Setup</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>.NET 10</strong></td>
|
||||
<td>Latest features, best performance, Windows/Linux/macOS</td>
|
||||
<td><code>dotnet new console</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Mono</strong></td>
|
||||
<td>Legacy systems, embedded, wide platform support</td>
|
||||
<td><code>mcs Program.cs</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- .NET 10 SECTION -->
|
||||
<h2 id="dotnet10">.NET 10 Examples</h2>
|
||||
<p>Using the official OpenAI .NET SDK with .NET 10. This provides the cleanest API with full async/await support.</p>
|
||||
|
||||
<h3 id="dotnet10-setup">Setup</h3>
|
||||
<pre><code class="bash"># Create new project
|
||||
dotnet new console -n UncloseAI
|
||||
cd UncloseAI
|
||||
|
||||
# Add OpenAI package
|
||||
dotnet add package OpenAI --version 2.5.0</code></pre>
|
||||
|
||||
<h3 id="dotnet10-non-stream">Non-Streaming (Hermes)</h3>
|
||||
<pre><code class="csharp">using OpenAI.Chat;
|
||||
|
||||
var client = new ChatClient(
|
||||
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
|
||||
apiKey: "choose-any-value",
|
||||
new OpenAIClientOptions
|
||||
{
|
||||
Endpoint = new Uri("https://hermes.ai.unturf.com/v1")
|
||||
}
|
||||
);
|
||||
|
||||
ChatCompletion completion = await client.CompleteChatAsync(
|
||||
new List<ChatMessage>
|
||||
{
|
||||
new UserChatMessage("Give a Python Fizzbuzz solution in one line of code?")
|
||||
},
|
||||
new ChatCompletionOptions
|
||||
{
|
||||
Temperature = 0.5f,
|
||||
MaxTokens = 150
|
||||
}
|
||||
);
|
||||
|
||||
Console.WriteLine(completion.Content[0].Text);</code></pre>
|
||||
|
||||
<h3 id="dotnet10-stream">Streaming (Hermes)</h3>
|
||||
<pre><code class="csharp">using OpenAI.Chat;
|
||||
|
||||
var client = new ChatClient(
|
||||
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
|
||||
apiKey: "choose-any-value",
|
||||
new OpenAIClientOptions
|
||||
{
|
||||
Endpoint = new Uri("https://hermes.ai.unturf.com/v1")
|
||||
}
|
||||
);
|
||||
|
||||
var streamingUpdates = client.CompleteChatStreamingAsync(
|
||||
new List<ChatMessage>
|
||||
{
|
||||
new UserChatMessage("Write a short poem about coding")
|
||||
},
|
||||
new ChatCompletionOptions
|
||||
{
|
||||
Temperature = 0.5f,
|
||||
MaxTokens = 200
|
||||
}
|
||||
);
|
||||
|
||||
await foreach (var update in streamingUpdates)
|
||||
{
|
||||
foreach (var contentPart in update.ContentUpdate)
|
||||
{
|
||||
Console.Write(contentPart.Text);
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h3 id="dotnet10-tts">Text-to-Speech</h3>
|
||||
<pre><code class="csharp">using OpenAI.Audio;
|
||||
|
||||
var client = new AudioClient(
|
||||
model: "tts-1",
|
||||
apiKey: "choose-any-value",
|
||||
new OpenAIClientOptions
|
||||
{
|
||||
Endpoint = new Uri("https://speech.ai.unturf.com/v1")
|
||||
}
|
||||
);
|
||||
|
||||
BinaryData speech = await client.GenerateSpeechAsync(
|
||||
"Hello! This is a test of text to speech.",
|
||||
GeneratedSpeechVoice.Alloy,
|
||||
new SpeechGenerationOptions { Speed = 1.0f }
|
||||
);
|
||||
|
||||
await File.WriteAllBytesAsync("speech.mp3", speech.ToArray());
|
||||
Console.WriteLine("Audio saved to speech.mp3");</code></pre>
|
||||
|
||||
<!-- MONO SECTION -->
|
||||
<h2 id="mono">Mono Examples</h2>
|
||||
<p>Using HttpClient directly with Mono runtime. No external packages required - works with the built-in System.Net.Http.</p>
|
||||
|
||||
<h3 id="mono-setup">Setup</h3>
|
||||
<pre><code class="bash"># Install Mono (Ubuntu/Debian)
|
||||
sudo apt install mono-complete
|
||||
|
||||
# Compile
|
||||
mcs -r:System.Net.Http.dll Program.cs
|
||||
|
||||
# Run
|
||||
mono Program.exe</code></pre>
|
||||
|
||||
<h3 id="mono-non-stream">Non-Streaming (Hermes)</h3>
|
||||
<pre><code class="csharp">using System;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
class Program
|
||||
{
|
||||
static readonly HttpClient client = new HttpClient();
|
||||
|
||||
static async Task Main()
|
||||
{
|
||||
var payload = @"{
|
||||
""model"": ""adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"",
|
||||
""messages"": [{""role"": ""user"", ""content"": ""Give a Python Fizzbuzz solution in one line of code?""}],
|
||||
""max_tokens"": 150
|
||||
}";
|
||||
|
||||
var content = new StringContent(payload, Encoding.UTF8, "application/json");
|
||||
var response = await client.PostAsync(
|
||||
"https://hermes.ai.unturf.com/v1/chat/completions",
|
||||
content
|
||||
);
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine(body);
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h3 id="mono-stream">Streaming (Hermes)</h3>
|
||||
<pre><code class="csharp">using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
class Program
|
||||
{
|
||||
static readonly HttpClient client = new HttpClient();
|
||||
|
||||
static async Task Main()
|
||||
{
|
||||
var payload = @"{
|
||||
""model"": ""adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"",
|
||||
""messages"": [{""role"": ""user"", ""content"": ""Write a short poem about coding""}],
|
||||
""max_tokens"": 200,
|
||||
""stream"": true
|
||||
}";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "https://hermes.ai.unturf.com/v1/chat/completions");
|
||||
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
using var stream = await response.Content.ReadAsStreamAsync();
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync();
|
||||
if (line?.StartsWith("data: ") == true)
|
||||
{
|
||||
var json = line.Substring(6);
|
||||
if (json != "[DONE]")
|
||||
{
|
||||
// Extract content from JSON (simplified)
|
||||
var start = json.IndexOf("\"content\":\"") + 11;
|
||||
if (start > 10)
|
||||
{
|
||||
var end = json.IndexOf("\"", start);
|
||||
if (end > start)
|
||||
{
|
||||
Console.Write(json.Substring(start, end - start));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h3 id="mono-tts">Text-to-Speech</h3>
|
||||
<pre><code class="csharp">using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
class Program
|
||||
{
|
||||
static readonly HttpClient client = new HttpClient();
|
||||
|
||||
static async Task Main()
|
||||
{
|
||||
var payload = @"{
|
||||
""model"": ""tts-1"",
|
||||
""input"": ""Hello! This is a test of text to speech."",
|
||||
""voice"": ""alloy""
|
||||
}";
|
||||
|
||||
var content = new StringContent(payload, Encoding.UTF8, "application/json");
|
||||
var response = await client.PostAsync(
|
||||
"https://speech.ai.unturf.com/v1/audio/speech",
|
||||
content
|
||||
);
|
||||
|
||||
var audioBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
File.WriteAllBytes("speech.mp3", audioBytes);
|
||||
Console.WriteLine("Audio saved to speech.mp3");
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<p><strong>Available voices:</strong> <code>alloy</code>, <code>echo</code>, <code>fable</code>, <code>onyx</code>, <code>nova</code>, <code>shimmer</code></p>
|
||||
|
||||
<h2 id="source">Full SDK Implementations</h2>
|
||||
<p>For complete SDK implementations with model discovery and advanced features:</p>
|
||||
<ul>
|
||||
<li><a href="/languages/csharp/dotnet10/" target="_blank">.NET 10 SDK</a></li>
|
||||
<li><a href="/languages/csharp/mono/" target="_blank">Mono SDK</a></li>
|
||||
<li><a href="/languages/csharp/http/" target="_blank">HttpClient (generic .NET)</a></li>
|
||||
<li><a href="/languages/csharp/openai/" target="_blank">OpenAI SDK wrapper</a></li>
|
||||
</ul>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current section on scroll
|
||||
const sections = document.querySelectorAll('h2[id], h3[id]');
|
||||
const navLinks = document.querySelectorAll('.table-of-contents a');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
if (window.scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Page TOC -->
|
||||
<aside class="sidebar-right">
|
||||
<div class="table-of-contents">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#csharp-intro" class="active">Overview</a></li>
|
||||
<li><a href="#runtimes">Runtime Options</a></li>
|
||||
<li><a href="#dotnet10">.NET 10</a>
|
||||
<ul>
|
||||
<li><a href="#dotnet10-setup">Setup</a></li>
|
||||
<li><a href="#dotnet10-non-stream">Non-Streaming</a></li>
|
||||
<li><a href="#dotnet10-stream">Streaming</a></li>
|
||||
<li><a href="#dotnet10-tts">TTS</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#mono">Mono</a>
|
||||
<ul>
|
||||
<li><a href="#mono-setup">Setup</a></li>
|
||||
<li><a href="#mono-non-stream">Non-Streaming</a></li>
|
||||
<li><a href="#mono-stream">Streaming</a></li>
|
||||
<li><a href="#mono-tts">TTS</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#source">Full SDK</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
29
public/css/chunkfive/stylesheet.css
Normal file
29
public/css/chunkfive/stylesheet.css
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* This is free software for the public good of a permacomputer hosted at
|
||||
* permacomputer.com, an always-on computer by the people, for the people.
|
||||
* One which is durable, easy to repair, & distributed like tap water
|
||||
* for machine learning intelligence.
|
||||
*
|
||||
* The permacomputer is community-owned infrastructure optimized around
|
||||
* four values:
|
||||
*
|
||||
* TRUTH First principles, math & science, open source code freely distributed
|
||||
* FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
* HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
* LOVE Be yourself without hurting others, cooperation through natural law
|
||||
*
|
||||
* This software contributes to that vision by making machine learning
|
||||
* accessible to everyone through a free, open, embeddable chat interface.
|
||||
* Code is seeds to sprout on any abandoned technology.
|
||||
*/
|
||||
|
||||
/*! Generated by Font Squirrel (https://www.fontsquirrel.com) on October 4, 2021 */
|
||||
|
||||
@font-face {
|
||||
font-family: "chunkfiveregular";
|
||||
src:
|
||||
url("chunkfive-regular-webfont.woff2") format("woff2"),
|
||||
url("chunkfive-regular-webfont.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
2611
public/css/pico.classless.min.css
vendored
Normal file
2611
public/css/pico.classless.min.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
326
public/css/sidebar-theme.css
Normal file
326
public/css/sidebar-theme.css
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
/*
|
||||
* This is free software for the public good of a permacomputer hosted at
|
||||
* permacomputer.com, an always-on computer by the people, for the people.
|
||||
* One which is durable, easy to repair, & distributed like tap water
|
||||
* for machine learning intelligence.
|
||||
*
|
||||
* The permacomputer is community-owned infrastructure optimized around
|
||||
* four values:
|
||||
*
|
||||
* TRUTH First principles, math & science, open source code freely distributed
|
||||
* FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
* HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
* LOVE Be yourself without hurting others, cooperation through natural law
|
||||
*
|
||||
* This software contributes to that vision by making machine learning
|
||||
* accessible to everyone through a free, open, embeddable chat interface.
|
||||
* Code is seeds to sprout on any abandoned technology.
|
||||
*/
|
||||
|
||||
/* CSS Grid-based sidebar navigation theme for PicoCSS */
|
||||
|
||||
:root {
|
||||
--sidebar-width: 280px;
|
||||
--content-max-width: 1200px;
|
||||
--base-font-size: 16px;
|
||||
--heading-scale: 1.125;
|
||||
}
|
||||
|
||||
/* Adjust default font sizes */
|
||||
html {
|
||||
font-size: var(--base-font-size);
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
h1 { font-size: calc(1rem * var(--heading-scale) * var(--heading-scale) * var(--heading-scale) * var(--heading-scale)); }
|
||||
h2 { font-size: calc(1rem * var(--heading-scale) * var(--heading-scale) * var(--heading-scale)); }
|
||||
h3 { font-size: calc(1rem * var(--heading-scale) * var(--heading-scale)); }
|
||||
h4 { font-size: calc(1rem * var(--heading-scale)); }
|
||||
h5, h6 { font-size: 1rem; }
|
||||
|
||||
/* Main grid layout */
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-width) 1fr var(--sidebar-width);
|
||||
grid-template-areas:
|
||||
"sidebar main sidebar-right";
|
||||
gap: 2rem;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
/* Ensure body doesn't expand beyond viewport */
|
||||
max-width: 100vw;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Sidebar navigation */
|
||||
aside.sidebar {
|
||||
grid-area: sidebar;
|
||||
background: var(--card-background-color);
|
||||
border-right: 1px solid var(--muted-border-color);
|
||||
padding: 2rem;
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
height: calc(100vh - 2rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Right sidebar navigation */
|
||||
aside.sidebar-right {
|
||||
grid-area: sidebar-right;
|
||||
background: var(--card-background-color);
|
||||
border-left: 1px solid var(--muted-border-color);
|
||||
padding: 2rem;
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
height: calc(100vh - 2rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Main content area */
|
||||
main {
|
||||
grid-area: main;
|
||||
padding: 2rem;
|
||||
max-width: 960px !important;
|
||||
margin: 0 auto;
|
||||
/* Prevent content overflow */
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
/* Responsive main content */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
main {
|
||||
max-width: 100% !important;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) and (max-width: 1440px) {
|
||||
main {
|
||||
max-width: 900px !important;
|
||||
margin-right: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1441px) {
|
||||
main {
|
||||
max-width: 960px !important;
|
||||
margin-right: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Large monitor adjustments - bring sidebar closer to content */
|
||||
@media (min-width: 1600px) {
|
||||
body {
|
||||
grid-template-columns: minmax(var(--sidebar-width), auto) minmax(0, 960px) minmax(var(--sidebar-width), auto);
|
||||
grid-template-areas:
|
||||
"sidebar main sidebar-right";
|
||||
justify-content: center;
|
||||
gap: 3rem 2rem;
|
||||
}
|
||||
|
||||
main {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Table of contents styling */
|
||||
.table-of-contents {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.table-of-contents h2 {
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--muted-border-color);
|
||||
}
|
||||
|
||||
.table-of-contents nav ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-of-contents nav ul ul {
|
||||
padding-left: 1.25rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.table-of-contents nav li {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.table-of-contents nav a {
|
||||
text-decoration: none;
|
||||
color: var(--color);
|
||||
display: block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.table-of-contents nav a:hover {
|
||||
background-color: var(--secondary-focus);
|
||||
}
|
||||
|
||||
.table-of-contents nav a.active {
|
||||
background-color: var(--primary-focus);
|
||||
color: var(--primary-inverse);
|
||||
}
|
||||
|
||||
/* Responsive behavior */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"main";
|
||||
padding: 0;
|
||||
/* Reset max-width for mobile */
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 100% !important;
|
||||
margin-right: 0;
|
||||
padding: 1rem;
|
||||
/* Ensure proper width on mobile */
|
||||
width: 100%;
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
aside.sidebar {
|
||||
position: fixed;
|
||||
left: -100%;
|
||||
width: var(--sidebar-width);
|
||||
z-index: 1000;
|
||||
transition: left 0.3s ease;
|
||||
background: var(--card-background-color);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
aside.sidebar.open {
|
||||
left: 0;
|
||||
box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
/* Hide right sidebar on mobile */
|
||||
aside.sidebar-right {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Overlay effect when sidebar is open */
|
||||
aside.sidebar.open::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Mobile menu toggle */
|
||||
.sidebar-toggle {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
z-index: 1001;
|
||||
background: var(--primary);
|
||||
color: var(--primary-inverse);
|
||||
border: none;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.sidebar-toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar scrollbar styling */
|
||||
aside.sidebar::-webkit-scrollbar,
|
||||
aside.sidebar-right::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
aside.sidebar::-webkit-scrollbar-track,
|
||||
aside.sidebar-right::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
aside.sidebar::-webkit-scrollbar-thumb,
|
||||
aside.sidebar-right::-webkit-scrollbar-thumb {
|
||||
background: var(--muted-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
aside.sidebar::-webkit-scrollbar-thumb:hover,
|
||||
aside.sidebar-right::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color);
|
||||
}
|
||||
|
||||
/* Make pre tags scrollable instead of expanding the page */
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Prevent horizontal scroll on mobile */
|
||||
@media (max-width: 768px) {
|
||||
/* Don't use overflow-x: hidden on body to preserve pinch-to-zoom */
|
||||
html, body {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* More specific selectors to prevent breaking zoom */
|
||||
main > *,
|
||||
aside > * {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Make header nav use full width on mobile */
|
||||
header nav {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header nav ul {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
header nav ul li {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header nav ul li a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.75rem 0.5rem;
|
||||
}
|
||||
}
|
||||
349
public/dart-examples.html
Normal file
349
public/dart-examples.html
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Dart Examples | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/c-examples.html">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html" class="active">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html">Browser Toys</a></li>
|
||||
<li><a href="/inference.html">Inference Setup</a></li>
|
||||
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
|
||||
<li><a href="/crawler.html">Our Crawler</a></li>
|
||||
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>Dart Examples - Free LLM & TTS AI Service</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 id="dart-intro">Dart Examples</h2>
|
||||
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with Dart/Flutter using the dart_openai community library. All examples use the same OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
|
||||
|
||||
<p><strong>Available Endpoints:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
|
||||
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
|
||||
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="dart-client">Dart Client Installation</h3>
|
||||
<p>Add dart_openai to your <code>pubspec.yaml</code>:</p>
|
||||
<pre><code class="yaml">dependencies:
|
||||
dart_openai: ^5.1.0</code></pre>
|
||||
|
||||
<h2 id="dart-non-streaming">Non-Streaming Examples</h2>
|
||||
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
|
||||
|
||||
<h3 id="dart-hermes-non-stream">Using Hermes (General Purpose)</h3>
|
||||
<pre><code class="dart">import 'package:dart_openai/dart_openai.dart';
|
||||
|
||||
void main() async {
|
||||
OpenAI.apiKey = "choose-any-value";
|
||||
OpenAI.baseUrl = "https://hermes.ai.unturf.com/v1";
|
||||
|
||||
final chatCompletion = await OpenAI.instance.chat.create(
|
||||
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
|
||||
messages: [
|
||||
OpenAIChatCompletionChoiceMessageModel(
|
||||
role: OpenAIChatMessageRole.user,
|
||||
content: [
|
||||
OpenAIChatCompletionChoiceMessageContentItemModel.text(
|
||||
"Give a Python Fizzbuzz solution in one line of code?"
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
temperature: 0.5,
|
||||
maxTokens: 150,
|
||||
);
|
||||
|
||||
print(chatCompletion.choices.first.message.content?.first.text);
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3 id="dart-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
|
||||
<pre><code class="dart">import 'package:dart_openai/dart_openai.dart';
|
||||
|
||||
void main() async {
|
||||
OpenAI.apiKey = "choose-any-value";
|
||||
OpenAI.baseUrl = "https://qwen.ai.unturf.com/v1";
|
||||
|
||||
final chatCompletion = await OpenAI.instance.chat.create(
|
||||
model: "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M",
|
||||
messages: [
|
||||
OpenAIChatCompletionChoiceMessageModel(
|
||||
role: OpenAIChatMessageRole.user,
|
||||
content: [
|
||||
OpenAIChatCompletionChoiceMessageContentItemModel.text(
|
||||
"Give a Python Fizzbuzz solution in one line of code?"
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
temperature: 0.5,
|
||||
maxTokens: 150,
|
||||
);
|
||||
|
||||
print(chatCompletion.choices.first.message.content?.first.text);
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2 id="dart-streaming">Streaming Examples</h2>
|
||||
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
|
||||
|
||||
<h3 id="dart-hermes-stream">Using Hermes (General Purpose)</h3>
|
||||
<pre><code class="dart">import 'package:dart_openai/dart_openai.dart';
|
||||
import 'dart:io';
|
||||
|
||||
void main() async {
|
||||
OpenAI.apiKey = "choose-any-value";
|
||||
OpenAI.baseUrl = "https://hermes.ai.unturf.com/v1";
|
||||
|
||||
final chatStream = OpenAI.instance.chat.createStream(
|
||||
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
|
||||
messages: [
|
||||
OpenAIChatCompletionChoiceMessageModel(
|
||||
role: OpenAIChatMessageRole.user,
|
||||
content: [
|
||||
OpenAIChatCompletionChoiceMessageContentItemModel.text(
|
||||
"Give a Python Fizzbuzz solution in one line of code?"
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
temperature: 0.5,
|
||||
maxTokens: 150,
|
||||
);
|
||||
|
||||
await for (final chunk in chatStream) {
|
||||
final content = chunk.choices.first.delta.content;
|
||||
if (content != null && content.isNotEmpty) {
|
||||
stdout.write(content.first.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3 id="dart-qwen-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
|
||||
<pre><code class="dart">import 'package:dart_openai/dart_openai.dart';
|
||||
import 'dart:io';
|
||||
|
||||
void main() async {
|
||||
OpenAI.apiKey = "choose-any-value";
|
||||
OpenAI.baseUrl = "https://qwen.ai.unturf.com/v1";
|
||||
|
||||
final chatStream = OpenAI.instance.chat.createStream(
|
||||
model: "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M",
|
||||
messages: [
|
||||
OpenAIChatCompletionChoiceMessageModel(
|
||||
role: OpenAIChatMessageRole.user,
|
||||
content: [
|
||||
OpenAIChatCompletionChoiceMessageContentItemModel.text(
|
||||
"Give a Python Fizzbuzz solution in one line of code?"
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
temperature: 0.5,
|
||||
maxTokens: 150,
|
||||
);
|
||||
|
||||
await for (final chunk in chatStream) {
|
||||
final content = chunk.choices.first.delta.content;
|
||||
if (content != null && content.isNotEmpty) {
|
||||
stdout.write(content.first.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2 id="dart-tts">Text-to-Speech Example</h2>
|
||||
<p>Generate audio speech from text using the TTS endpoint. The audio is saved as an MP3 file.</p>
|
||||
|
||||
<pre><code class="dart">import 'package:dart_openai/dart_openai.dart';
|
||||
import 'dart:io';
|
||||
|
||||
void main() async {
|
||||
OpenAI.apiKey = "YOLO";
|
||||
OpenAI.baseUrl = "https://speech.ai.unturf.com/v1";
|
||||
|
||||
final speech = await OpenAI.instance.audio.createSpeech(
|
||||
model: "tts-1",
|
||||
input: "I think so therefore, Today is a wonderful day to grow something people love!",
|
||||
voice: "alloy",
|
||||
speed: 0.9,
|
||||
);
|
||||
|
||||
final file = File("speech.mp3");
|
||||
await file.writeAsBytes(speech);
|
||||
print("Audio saved to: ${file.path}");
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current section on scroll
|
||||
const sections = document.querySelectorAll('h2[id], h3[id]');
|
||||
const navLinks = document.querySelectorAll('.table-of-contents a');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
if (window.scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Page TOC -->
|
||||
<aside class="sidebar-right">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#dart-intro" class="active">Overview</a></li>
|
||||
<li><a href="#dart-client">Installation</a></li>
|
||||
<li><a href="#dart-non-streaming">Non-Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#dart-hermes-non-stream">Hermes</a></li>
|
||||
<li><a href="#dart-qwen-non-stream">Qwen Coder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#dart-streaming">Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#dart-hermes-stream">Hermes</a></li>
|
||||
<li><a href="#dart-qwen-stream">Qwen Coder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#dart-tts">Text-to-Speech</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
997
public/demo.html
Normal file
997
public/demo.html
Normal file
|
|
@ -0,0 +1,997 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Demo Free LLM & Text To Speech Artificial Intelligence Service | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
|
||||
// Disable the floating button
|
||||
window.UNCLOSEAI_FLOATING_BUTTON = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
|
||||
<!-- Demo: System Message Customization -->
|
||||
<script type="module">
|
||||
import { setSystemMessageAppend } from './src/config.js';
|
||||
|
||||
setSystemMessageAppend(`
|
||||
Additional context for this demo page:
|
||||
- You're helping users explore and test AI features on uncloseai.com
|
||||
- This is a demonstration page showing various AI capabilities
|
||||
- Encourage users to try different features like TTS, translation, and the Hermes chat
|
||||
- Be enthusiastic and helpful when explaining how the features work
|
||||
- If users ask about implementation, reference the code examples on this page
|
||||
`);
|
||||
</script>
|
||||
|
||||
<!--
|
||||
<script defer data-domain="ai.unturf.com" src="https://analytics.unturf.com/js/plausible.js"></script>
|
||||
-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>Welcome to uncloseai.com - Free LLM & Text To Speech Artificial Intelligence Service</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<h2>Hermes AI Integration Demo</h2>
|
||||
<p>This page demonstrates all the ways you can integrate Hermes AI into your website using uncloseai.js. Each example is isolated and can be used independently.</p>
|
||||
|
||||
<!-- Table of Contents -->
|
||||
<section>
|
||||
<h3>📋 Table of Contents</h3>
|
||||
<nav>
|
||||
<ol>
|
||||
<li><a href="#basic-installation">Basic Installation</a></li>
|
||||
<li><a href="#configuration-options">Configuration Options</a></li>
|
||||
<li><a href="#system-message-customization">System Message Customization</a></li>
|
||||
<li><a href="#full-interface">Full AI Interface</a></li>
|
||||
<li><a href="#chat-only">Chat Interface Only</a></li>
|
||||
<li><a href="#feature-buttons">Feature Buttons Only</a></li>
|
||||
<li><a href="#tts-only">Text-to-Speech Options</a></li>
|
||||
<li><a href="#translate-only">Translation Options</a></li>
|
||||
<li><a href="#translation">Translation API</a></li>
|
||||
<li><a href="#read-page">Read Page Functionality</a></li>
|
||||
<li><a href="#direct-api">Direct API Usage</a></li>
|
||||
<li><a href="#custom-examples">Custom API Examples</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<!-- 1. Basic Installation -->
|
||||
<section id="basic-installation">
|
||||
<h3>1. 📦 Basic Installation</h3>
|
||||
<p>The simplest way to add AI features to your website:</p>
|
||||
<h4>📄 Code Example</h4>
|
||||
<details open>
|
||||
<summary><strong>Installation Code</strong></summary>
|
||||
<p>Add this single line to your HTML to get a floating AI button:</p>
|
||||
<pre><code class="html"><script src="https://uncloseai.com/uncloseai.js" type="module"></script></code></pre>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Example</h4>
|
||||
<p>This page demonstrates the basic installation - notice the floating AI button in the bottom right corner!</p>
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<p><strong>✅ Active Demo:</strong> The floating Hermes button on this page is created by the basic installation code above.</p>
|
||||
<p>Try clicking the floating button to test the AI chat functionality!</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Configuration Options -->
|
||||
<section id="configuration-options">
|
||||
<h3>2. ⚙️ Configuration Options</h3>
|
||||
<p>Customize the behavior before loading the script:</p>
|
||||
|
||||
<h4>📄 Code Example</h4>
|
||||
<details open>
|
||||
<summary><strong>Configuration Code</strong></summary>
|
||||
<p>Customize the behavior before loading the script:</p>
|
||||
<pre><code class="html"><script>
|
||||
// Disable custom styling (use your CSS framework)
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
|
||||
// Disable floating button (API functions only)
|
||||
window.UNCLOSEAI_FLOATING_BUTTON = false;
|
||||
</script>
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script></code></pre>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Example</h4>
|
||||
<p>This page uses these configuration options:</p>
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<p><strong>✅ Active Demo:</strong> This page has:</p>
|
||||
<ul>
|
||||
<li><code>UNCLOSEAI_CUSTOM_STYLING = false</code> - Uses PicoCSS styling instead</li>
|
||||
<li><code>UNCLOSEAI_FLOATING_BUTTON = false</code> - Floating button disabled for embedded examples</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. System Message Customization -->
|
||||
<section id="system-message-customization">
|
||||
<h3>3. 💬 System Message Customization</h3>
|
||||
<p>Customize the AI's behavior by appending additional instructions:</p>
|
||||
|
||||
<h4>📄 Code Example</h4>
|
||||
<details open>
|
||||
<summary><strong>System Message Customization Code</strong></summary>
|
||||
<p>Customize the AI's behavior by appending instructions:</p>
|
||||
<pre><code class="html"><script type="module">
|
||||
import { setSystemMessageAppend } from './src/config.js';
|
||||
|
||||
setSystemMessageAppend(`
|
||||
Additional context for this demo page:
|
||||
- You're helping users test AI features
|
||||
- Provide clear, helpful examples
|
||||
- Be encouraging about trying different features
|
||||
`);
|
||||
</script></code></pre>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Example</h4>
|
||||
<p>This page uses custom system message instructions:</p>
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<p><strong>✅ Active Demo:</strong> The AI on this page has been customized to:</p>
|
||||
<ul>
|
||||
<li>Help users explore and test AI features</li>
|
||||
<li>Provide clear, helpful examples</li>
|
||||
<li>Be encouraging about trying different features</li>
|
||||
</ul>
|
||||
<p>Try asking the AI about the features on this page to see the customization in action!</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Full AI Interface -->
|
||||
<section id="full-interface">
|
||||
<h3>4. 🤖 Full AI Interface</h3>
|
||||
<p>Complete AI chat with all features enabled:</p>
|
||||
|
||||
<h4>📄 Code Example</h4>
|
||||
<details open>
|
||||
<summary><strong>Full Interface Code</strong></summary>
|
||||
<p>Embed a complete AI interface with all features:</p>
|
||||
<pre><code class="html"><div class="uncloseai" data-features="full"></div></code></pre>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Example</h4>
|
||||
<p>Try the full AI interface below:</p>
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<div class="uncloseai" data-features="full" id="demo-full"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. Chat Interface Only -->
|
||||
<section id="chat-only">
|
||||
<h3>5. 💬 Chat Interface Only</h3>
|
||||
<p>Pure chat interface without extra buttons:</p>
|
||||
|
||||
<h4>📄 Code Example</h4>
|
||||
<details open>
|
||||
<summary><strong>Chat Only Code</strong></summary>
|
||||
<p>Embed just the chat interface:</p>
|
||||
<pre><code class="html"><div class="uncloseai" data-features="chat"></div></code></pre>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Example</h4>
|
||||
<p>Try the chat-only interface below:</p>
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<div class="uncloseai" data-features="chat" id="demo-chat"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. Feature Buttons Only -->
|
||||
<section id="feature-buttons">
|
||||
<h3>6. 🔧 Feature Buttons Only</h3>
|
||||
<p>Add specific feature buttons without the full chat interface:</p>
|
||||
|
||||
<h4>📄 Code Example</h4>
|
||||
<details open>
|
||||
<summary><strong>How to add specific feature buttons</strong></summary>
|
||||
<p>Mix and match any features you want:</p>
|
||||
<pre><code class="html"><!-- TTS and Read buttons only -->
|
||||
<div class="uncloseai" data-features="tts,read"></div>
|
||||
|
||||
<!-- Just TTS button -->
|
||||
<div class="uncloseai" data-features="tts"></div>
|
||||
|
||||
<!-- Just translation button -->
|
||||
<div class="uncloseai" data-features="translate"></div>
|
||||
|
||||
<!-- Multiple features -->
|
||||
<div class="uncloseai" data-features="tts,translate,read"></div></code></pre>
|
||||
|
||||
<p><strong>Available features:</strong> <code>chat</code>, <code>tts</code>, <code>translate</code>, <code>read</code>, <code>full</code></p>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Example</h4>
|
||||
<p>Try the feature buttons below:</p>
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<div class="uncloseai" data-features="tts,read" id="demo-buttons"></div>
|
||||
<p><strong>Available features:</strong> Text-to-Speech and Read Page buttons</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 7. Text-to-Speech Options -->
|
||||
<section id="tts-only">
|
||||
<h3>7. 🔊 Text-to-Speech Options</h3>
|
||||
<p>Two ways to add TTS functionality to your website:</p>
|
||||
|
||||
<h4>📄 Code Examples</h4>
|
||||
<details open>
|
||||
<summary><strong>How to implement both TTS options</strong></summary>
|
||||
|
||||
<h5>Option 1: TTS Widget Button</h5>
|
||||
<pre><code class="html"><div class="uncloseai" data-features="tts"></div></code></pre>
|
||||
|
||||
<h5>Option 2: TTS Modal Button</h5>
|
||||
<pre><code class="html"><button onclick="openTTSModal()">🔊 Text to Speech</button></code></pre>
|
||||
|
||||
<p><strong>Note:</strong> Both require the uncloseai.js script to be loaded first:</p>
|
||||
<pre><code class="html"><script src="https://uncloseai.com/uncloseai.js" type="module"></script></code></pre>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Examples</h4>
|
||||
|
||||
<div style="display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
|
||||
<!-- TTS Widget Button -->
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<h5>🔊 TTS Widget Button</h5>
|
||||
<p>Embedded TTS button for any content:</p>
|
||||
<div class="uncloseai" data-features="tts" id="demo-tts"></div>
|
||||
<p><strong>Best for:</strong> Adding TTS to specific content areas</p>
|
||||
</div>
|
||||
|
||||
<!-- TTS Modal Button -->
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<h5>🎤 TTS Anything Modal</h5>
|
||||
<p>Standalone modal for any text input:</p>
|
||||
<button onclick="openTTSModal()" style="padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">🔊 Open TTS Modal</button>
|
||||
<p><strong>Best for:</strong> General-purpose text-to-speech tool</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 8. Translation Options -->
|
||||
<section id="translate-only">
|
||||
<h3>8. 🌐 Translation Options</h3>
|
||||
<p>Two ways to add translation functionality to your website:</p>
|
||||
|
||||
<h4>📄 Code Examples</h4>
|
||||
<details open>
|
||||
<summary><strong>How to implement both translation options</strong></summary>
|
||||
|
||||
<h5>Option 1: Translation Widget Button</h5>
|
||||
<pre><code class="html"><div class="uncloseai" data-features="translate"></div></code></pre>
|
||||
|
||||
<h5>Option 2: Translation Modal Button</h5>
|
||||
<pre><code class="html"><button onclick="openTranslateModal()">🌐 Translate</button></code></pre>
|
||||
|
||||
<p><strong>Translation Features:</strong></p>
|
||||
<ul>
|
||||
<li>📝 Custom text translation into 19 languages</li>
|
||||
<li>📄 Full page translation with preserved formatting</li>
|
||||
<li>⚙️ Advanced options and language selection</li>
|
||||
<li>📋 Copy and preview translated content</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Note:</strong> Both require the uncloseai.js script to be loaded first:</p>
|
||||
<pre><code class="html"><script src="https://uncloseai.com/uncloseai.js" type="module"></script></code></pre>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Examples</h4>
|
||||
|
||||
<div style="display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
|
||||
<!-- Translation Widget Button -->
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<h5>🌐 Translation Widget Button</h5>
|
||||
<p>Embedded translation button for content:</p>
|
||||
<div class="uncloseai" data-features="translate" id="demo-translate-modal"></div>
|
||||
<p><strong>Best for:</strong> Adding translation to specific content areas</p>
|
||||
</div>
|
||||
|
||||
<!-- Translation Modal Button -->
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<h5>🔄 Translation Modal</h5>
|
||||
<p>Standalone modal for translation tasks:</p>
|
||||
<button onclick="openTranslateModal()" style="padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">🌐 Open Translation Modal</button>
|
||||
<p><strong>Best for:</strong> General-purpose translation tool</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
Smart Dropdown section commented out - not working yet
|
||||
|
||||
<div style="display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<h5>🌐 Translation Modal</h5>
|
||||
<p>Traditional modal interface for translations:</p>
|
||||
<div class="uncloseai" data-features="translate" id="demo-translate-modal"></div>
|
||||
</div>
|
||||
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<h5>🤖 Smart AI Dropdown</h5>
|
||||
<p>AI-powered quick translation:</p>
|
||||
<div class="uncloseai" data-features="smart-translate" id="demo-translate-smart"></div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</section>
|
||||
|
||||
<!-- 9. Translation -->
|
||||
<section id="translation">
|
||||
<h3>9. 🌐 Translation</h3>
|
||||
<p>Translate page content or custom text into 19 languages with code block preservation:</p>
|
||||
|
||||
<h4>📄 Code Example</h4>
|
||||
<details open>
|
||||
<summary><strong>Translation Code</strong></summary>
|
||||
<p>Add translation capabilities to any interface:</p>
|
||||
<pre><code class="javascript">// Import translation functions
|
||||
import { translateText, translateCurrentPage, SUPPORTED_LANGUAGES } from './src/translation.js';
|
||||
|
||||
// Translate custom text
|
||||
const translatedText = await translateText('Hello world!', 'es'); // Spanish
|
||||
|
||||
// Translate current page content
|
||||
const translatedPage = await translateCurrentPage('fr'); // French
|
||||
|
||||
// Available languages
|
||||
console.log(SUPPORTED_LANGUAGES);
|
||||
// { 'en': 'English', 'es': 'Spanish', 'fr': 'French', ... }</code></pre>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Example</h4>
|
||||
<p>Try the translation feature below - it preserves code blocks and formatting:</p>
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<div class="uncloseai" data-features="full" id="demo-translation"></div>
|
||||
<p><strong>💡 Tip:</strong> The translation feature automatically detects and preserves:</p>
|
||||
<ul>
|
||||
<li>Code blocks (```code```)</li>
|
||||
<li>Inline code (`code`)</li>
|
||||
<li>URLs and links</li>
|
||||
<li>HTML tags</li>
|
||||
<li>Technical formatting</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 10. Read Page Functionality -->
|
||||
<section id="read-page">
|
||||
<h3>10. 📖 Read Page Functionality</h3>
|
||||
<p>Let AI read the entire webpage aloud with natural voice synthesis:</p>
|
||||
|
||||
<h4>📄 Code Examples</h4>
|
||||
<details open>
|
||||
<summary><strong>How to add read page functionality</strong></summary>
|
||||
|
||||
<h5>Option 1: Read Page Button in Widget</h5>
|
||||
<pre><code class="html"><div class="uncloseai" data-features="read"></div></code></pre>
|
||||
|
||||
<h5>Option 2: Standalone Read Page Button</h5>
|
||||
<pre><code class="html"><button onclick="readPageWithHermes()">📖 Read Page</button></code></pre>
|
||||
|
||||
<h5>Option 3: Custom JavaScript Integration</h5>
|
||||
<pre><code class="javascript">// Call read page function programmatically
|
||||
await readPageWithHermes();
|
||||
|
||||
// Or import and use directly
|
||||
import { readPageWithHermes } from './src/page-reader.js';
|
||||
await readPageWithHermes();</code></pre>
|
||||
|
||||
<p><strong>Note:</strong> Requires the uncloseai.js script to be loaded first:</p>
|
||||
<pre><code class="html"><script src="https://uncloseai.com/uncloseai.js" type="module"></script></code></pre>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Example</h4>
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; margin: 15px 0; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<button onclick="readPageWithHermes()" style="padding: 15px 30px; background: #4CAF50; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; font-weight: bold;">📖 Read This Page Aloud</button>
|
||||
<p><strong>What it does:</strong></p>
|
||||
<ul>
|
||||
<li>🤖 AI processes page content for optimal speech</li>
|
||||
<li>🔊 Converts to natural-sounding audio</li>
|
||||
<li>⏯️ Provides play/pause controls</li>
|
||||
<li>💾 Option to download MP3 file</li>
|
||||
<li>📱 Works on mobile and desktop</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 11. Direct API Usage -->
|
||||
<section id="direct-api">
|
||||
<h3>11. 🛠️ Direct API Usage</h3>
|
||||
<p>Call uncloseai.js functions from your own JavaScript:</p>
|
||||
|
||||
<h4>📄 Code Example</h4>
|
||||
<details open>
|
||||
<summary><strong>Direct API Code</strong></summary>
|
||||
<p>Call uncloseai.js functions from your own JavaScript:</p>
|
||||
<pre><code class="javascript">// Send a message to AI
|
||||
for await (const chunk of sendMessage('Your question here')) {
|
||||
console.log(chunk);
|
||||
}
|
||||
|
||||
// Convert text to speech
|
||||
const result = await speakText('Hello world', 'alloy', 0.9);
|
||||
document.body.appendChild(result.audio);
|
||||
|
||||
// Upload and analyze files - COMMENTED OUT: Not working
|
||||
// const response = await uploadFile(fileInput.files[0]);
|
||||
// console.log(response);
|
||||
|
||||
// Read page content with AI
|
||||
await readPageWithHermes();</code></pre>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Example</h4>
|
||||
<p>The custom API examples below demonstrate direct API usage:</p>
|
||||
</section>
|
||||
|
||||
<!-- 12. Custom API Examples -->
|
||||
<section id="custom-examples">
|
||||
<h3>12. 🎨 Custom API Examples</h3>
|
||||
<p>Build your own interface using uncloseai.js functions:</p>
|
||||
|
||||
<h4>📄 Code Examples</h4>
|
||||
<details open>
|
||||
<summary><strong>How to build custom interfaces with Direct API</strong></summary>
|
||||
|
||||
<h5>Custom Chat Interface</h5>
|
||||
<pre><code class="javascript">// Custom chat implementation with isolated state
|
||||
async function customChatExample() {
|
||||
const input = document.getElementById('custom-chat-input');
|
||||
const display = document.getElementById('custom-chat-display');
|
||||
const message = input.value.trim();
|
||||
|
||||
if (!message) return;
|
||||
|
||||
// Add user message to display
|
||||
display.innerHTML += `<div><strong>You:</strong> ${message}</div>`;
|
||||
input.value = '';
|
||||
|
||||
// Add AI thinking indicator
|
||||
const thinkingDiv = document.createElement('div');
|
||||
thinkingDiv.innerHTML = '<strong>Hermes:</strong> <em>thinking...</em>';
|
||||
display.appendChild(thinkingDiv);
|
||||
|
||||
try {
|
||||
// Import and use sendMessage function
|
||||
const { sendMessage } = await import('./src/chat.js');
|
||||
let response = '';
|
||||
for await (const chunk of sendMessage(message)) {
|
||||
response += chunk;
|
||||
thinkingDiv.innerHTML = `<strong>Hermes:</strong> ${response}`;
|
||||
}
|
||||
} catch (error) {
|
||||
thinkingDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h5>Custom TTS Interface</h5>
|
||||
<pre><code class="javascript">// Custom TTS with voice selection
|
||||
async function customTTSExample() {
|
||||
const text = document.getElementById('custom-tts-text').value.trim();
|
||||
const voice = document.getElementById('voice-select').value;
|
||||
const resultDiv = document.getElementById('custom-tts-result');
|
||||
|
||||
if (!text) {
|
||||
alert('Please enter some text first!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Import and use speakTextDirect function
|
||||
const { speakTextDirect } = await import('./src/tts.js');
|
||||
const result = await speakTextDirect(text, voice, 1.0);
|
||||
|
||||
// Create custom audio controls
|
||||
const playPauseBtn = document.createElement('button');
|
||||
playPauseBtn.textContent = '⏸️ Pause';
|
||||
|
||||
const downloadBtn = document.createElement('button');
|
||||
downloadBtn.textContent = '💾 Download';
|
||||
downloadBtn.onclick = () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(result.blob);
|
||||
a.download = `custom-tts-${Date.now()}.mp3`;
|
||||
a.click();
|
||||
};
|
||||
|
||||
// Auto-play and handle controls
|
||||
result.audio.play();
|
||||
playPauseBtn.onclick = () => {
|
||||
if (result.audio.paused) {
|
||||
result.audio.play();
|
||||
playPauseBtn.textContent = '⏸️ Pause';
|
||||
} else {
|
||||
result.audio.pause();
|
||||
playPauseBtn.textContent = '▶️ Play';
|
||||
}
|
||||
};
|
||||
|
||||
resultDiv.appendChild(playPauseBtn);
|
||||
resultDiv.appendChild(downloadBtn);
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h5>Custom Page Analysis</h5>
|
||||
<pre><code class="javascript">// Custom page analysis with user prompt
|
||||
async function customPageAnalysis() {
|
||||
const prompt = document.getElementById('page-analysis-prompt').value.trim();
|
||||
const resultDiv = document.getElementById('page-analysis-result');
|
||||
|
||||
if (!prompt) {
|
||||
alert('Please enter a question about this page!');
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = '<em>Analyzing page...</em>';
|
||||
|
||||
try {
|
||||
// Get page content and combine with user prompt
|
||||
const { sendMessage } = await import('./src/chat.js');
|
||||
const pageContent = document.body.innerText.substring(0, 3000);
|
||||
const fullPrompt = `Based on this page content: "${pageContent}"\\n\\nUser question: ${prompt}`;
|
||||
|
||||
let response = '';
|
||||
for await (const chunk of sendMessage(fullPrompt)) {
|
||||
response += chunk;
|
||||
resultDiv.innerHTML = `<strong>Analysis:</strong><br><div>${response}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h5>Custom Translation Interface</h5>
|
||||
<pre><code class="javascript">// Custom translation with code preservation
|
||||
async function customTranslateExample() {
|
||||
const text = document.getElementById('custom-translate-text').value.trim();
|
||||
const targetLanguage = document.getElementById('translate-language').value;
|
||||
const resultDiv = document.getElementById('custom-translate-result');
|
||||
|
||||
if (!text) {
|
||||
alert('Please enter some text to translate!');
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.textContent = 'Translating...';
|
||||
|
||||
try {
|
||||
// Import and use translation function
|
||||
const { translateText } = await import('./src/translation.js');
|
||||
const translatedText = await translateText(text, targetLanguage);
|
||||
|
||||
resultDiv.textContent = translatedText;
|
||||
} catch (error) {
|
||||
resultDiv.textContent = `Translation failed: ${error.message}`;
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<p><strong>Available API Functions:</strong></p>
|
||||
<ul>
|
||||
<li><code>sendMessage(message)</code> - Chat with AI (returns async generator)</li>
|
||||
<li><code>speakTextDirect(text, voice, speed)</code> - Convert text to speech</li>
|
||||
<li><code>translateText(text, targetLanguage)</code> - Translate text</li>
|
||||
<li><code>readPageWithHermes()</code> - Read page content aloud</li>
|
||||
<li><code>openTTSModal()</code> - Open TTS modal</li>
|
||||
<li><code>openTranslateModal()</code> - Open translation modal</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Note:</strong> All functions require uncloseai.js to be loaded first:</p>
|
||||
<pre><code class="html"><script src="https://uncloseai.com/uncloseai.js" type="module"></script></code></pre>
|
||||
</details>
|
||||
|
||||
<h4>🎯 Live Examples</h4>
|
||||
<p>Try these custom implementations built with the Direct API:</p>
|
||||
|
||||
<div style="display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
|
||||
<!-- Custom Chat Example -->
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<h5>💬 Custom AI Chat</h5>
|
||||
<p>Your own chat interface:</p>
|
||||
<div style="border: 1px solid #eee; height: 200px; padding: 10px; margin: 10px 0; overflow-y: auto; background: #fafafa;" id="custom-chat-display"></div>
|
||||
<input type="text" id="custom-chat-input" placeholder="Ask anything..." style="width: 100%; margin: 5px 0;">
|
||||
<button onclick="customChatExample()" style="width: 100%;">Send Message</button>
|
||||
</div>
|
||||
|
||||
<!-- Custom TTS Example -->
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<h5>🔊 Custom Text-to-Speech</h5>
|
||||
<p>Your own TTS interface:</p>
|
||||
<textarea id="custom-tts-text" placeholder="Enter text to speak..." style="width: 100%; height: 80px; margin: 5px 0;"></textarea>
|
||||
<select id="voice-select" style="width: 100%; margin: 5px 0;">
|
||||
<option value="alloy">Alloy</option>
|
||||
<option value="echo">Echo</option>
|
||||
<option value="fable">Fable</option>
|
||||
<option value="onyx">Onyx</option>
|
||||
<option value="nova">Nova</option>
|
||||
<option value="shimmer">Shimmer</option>
|
||||
</select>
|
||||
<button onclick="customTTSExample()" style="width: 100%;">Generate Speech</button>
|
||||
<div id="custom-tts-result" style="margin: 10px 0;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Custom File Upload Example - DISABLED: Upload functionality under development -->
|
||||
<div style="border: 2px dashed #ccc; padding: 20px; border-radius: 8px; background: rgba(128, 128, 128, 0.05); opacity: 0.6;">
|
||||
<h5>📁 Custom File Analysis <span style="color: #999; font-size: 0.8em;">(Under Development)</span></h5>
|
||||
<p>File upload interface (temporarily disabled):</p>
|
||||
<input type="file" id="custom-file-input" style="width: 100%; margin: 5px 0;" disabled>
|
||||
<button onclick="alert('File upload feature is under development')" style="width: 100%;" disabled>Analyze File (Coming Soon)</button>
|
||||
<div id="custom-file-result" style="margin: 10px 0; padding: 10px; background: #f9f9f9; border-radius: 5px; display: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Page Analysis Example -->
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<h5>📖 Custom Page Analysis</h5>
|
||||
<p>Analyze page content with custom prompts:</p>
|
||||
<input type="text" id="page-analysis-prompt" placeholder="What would you like to know about this page?" style="width: 100%; margin: 5px 0;">
|
||||
<button onclick="customPageAnalysis()" style="width: 100%;">Analyze Page</button>
|
||||
<div id="page-analysis-result" style="margin: 10px 0; padding: 10px; background: #f9f9f9; border-radius: 5px; display: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Translation Example -->
|
||||
<div style="border: 2px dashed #43a047; padding: 20px; border-radius: 8px; background: rgba(67, 160, 71, 0.05);">
|
||||
<h5>🌐 Custom Translation</h5>
|
||||
<p>Translate text with code preservation:</p>
|
||||
<textarea id="custom-translate-text" placeholder="Enter text to translate (try including code like `function test()` or ```code blocks```)" style="width: 100%; height: 80px; margin: 5px 0;"></textarea>
|
||||
<select id="translate-language" style="width: 100%; margin: 5px 0;">
|
||||
<option value="es">Spanish</option>
|
||||
<option value="zh">Chinese (Simplified)</option>
|
||||
<option value="hi">Hindi</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="ar">Arabic</option>
|
||||
<option value="bn">Bengali</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="ur">Urdu</option>
|
||||
<option value="id">Indonesian</option>
|
||||
<option value="de">German</option>
|
||||
<option value="ja">Japanese</option>
|
||||
<option value="sw">Swahili</option>
|
||||
<option value="mr">Marathi</option>
|
||||
<option value="te">Telugu</option>
|
||||
<option value="tr">Turkish</option>
|
||||
<option value="zh-tw">Chinese (Traditional)</option>
|
||||
<option value="ko">Korean</option>
|
||||
</select>
|
||||
<button onclick="customTranslateExample()" style="width: 100%;">Translate Text</button>
|
||||
<div id="custom-translate-result" style="margin: 10px 0; padding: 10px; background: #f9f9f9; border-radius: 5px; display: none; white-space: pre-wrap;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// Custom chat implementation with isolated state
|
||||
async function customChatExample() {
|
||||
const input = document.getElementById('custom-chat-input');
|
||||
const display = document.getElementById('custom-chat-display');
|
||||
const message = input.value.trim();
|
||||
|
||||
if (!message) return;
|
||||
|
||||
// Add user message
|
||||
display.innerHTML += `<div style="margin: 5px 0; padding: 5px; background: #e3f2fd; border-radius: 3px;"><strong>You:</strong> ${message}</div>`;
|
||||
input.value = '';
|
||||
|
||||
// Add AI thinking indicator
|
||||
const thinkingDiv = document.createElement('div');
|
||||
thinkingDiv.style.cssText = 'margin: 5px 0; padding: 5px; background: #f5f5f5; border-radius: 3px;';
|
||||
thinkingDiv.innerHTML = '<strong>Hermes:</strong> <em>thinking...</em>';
|
||||
display.appendChild(thinkingDiv);
|
||||
display.scrollTop = display.scrollHeight;
|
||||
|
||||
try {
|
||||
const { sendMessage } = await import('./src/chat.js');
|
||||
let response = '';
|
||||
for await (const chunk of sendMessage(message)) {
|
||||
response += chunk;
|
||||
thinkingDiv.innerHTML = `<strong>Hermes:</strong> ${response}`;
|
||||
display.scrollTop = display.scrollHeight;
|
||||
}
|
||||
} catch (error) {
|
||||
thinkingDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom TTS with voice selection
|
||||
async function customTTSExample() {
|
||||
const text = document.getElementById('custom-tts-text').value.trim();
|
||||
const voice = document.getElementById('voice-select').value;
|
||||
const resultDiv = document.getElementById('custom-tts-result');
|
||||
const button = event.target;
|
||||
|
||||
if (!text) {
|
||||
alert('Please enter some text first!');
|
||||
return;
|
||||
}
|
||||
|
||||
button.textContent = 'Processing...';
|
||||
button.disabled = true;
|
||||
resultDiv.innerHTML = '<em>Generating speech...</em>';
|
||||
|
||||
try {
|
||||
const { speakTextDirect } = await import('./src/tts.js');
|
||||
const result = await speakTextDirect(text, voice, 1.0);
|
||||
|
||||
// Create play/pause button
|
||||
const playPauseBtn = document.createElement('button');
|
||||
playPauseBtn.textContent = '⏸️ Pause';
|
||||
playPauseBtn.style.margin = '2px';
|
||||
|
||||
const downloadBtn = document.createElement('button');
|
||||
downloadBtn.textContent = '💾 Download';
|
||||
downloadBtn.style.margin = '2px';
|
||||
downloadBtn.onclick = () => downloadCustomAudio();
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.style.margin = '10px 0';
|
||||
controls.appendChild(playPauseBtn);
|
||||
controls.appendChild(downloadBtn);
|
||||
|
||||
resultDiv.innerHTML = '';
|
||||
resultDiv.appendChild(controls);
|
||||
|
||||
// Auto-play the audio
|
||||
result.audio.play();
|
||||
|
||||
// Handle play/pause button
|
||||
playPauseBtn.onclick = () => {
|
||||
if (result.audio.paused) {
|
||||
result.audio.play();
|
||||
playPauseBtn.textContent = '⏸️ Pause';
|
||||
} else {
|
||||
result.audio.pause();
|
||||
playPauseBtn.textContent = '▶️ Play';
|
||||
}
|
||||
};
|
||||
|
||||
// Handle audio end
|
||||
result.audio.onended = () => {
|
||||
playPauseBtn.textContent = '▶️ Play';
|
||||
};
|
||||
|
||||
window.customAudioBlob = result.blob;
|
||||
button.textContent = 'Generate Speech';
|
||||
button.disabled = false;
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
|
||||
button.textContent = 'Generate Speech';
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom file upload with isolated state
|
||||
async function customFileExample() {
|
||||
const fileInput = document.getElementById('custom-file-input');
|
||||
const resultDiv = document.getElementById('custom-file-result');
|
||||
|
||||
if (!fileInput.files[0]) {
|
||||
alert('Please select a file first!');
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = '<em>Analyzing file...</em>';
|
||||
|
||||
try {
|
||||
const { uploadFile } = await import('./src/file-upload.js');
|
||||
const response = await uploadFile(fileInput.files[0]);
|
||||
resultDiv.innerHTML = `<strong>Analysis Result:</strong><br><div style="white-space: pre-wrap;">${response}</div>`;
|
||||
fileInput.value = '';
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom page analysis with user prompt
|
||||
async function customPageAnalysis() {
|
||||
const promptInput = document.getElementById('page-analysis-prompt');
|
||||
const resultDiv = document.getElementById('page-analysis-result');
|
||||
const prompt = promptInput.value.trim();
|
||||
|
||||
if (!prompt) {
|
||||
alert('Please enter a question about this page!');
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = '<em>Analyzing page...</em>';
|
||||
|
||||
try {
|
||||
const { sendMessage } = await import('./src/chat.js');
|
||||
const pageContent = document.body.innerText.substring(0, 3000);
|
||||
const fullPrompt = `Based on this page content: "${pageContent}"\n\nUser question: ${prompt}`;
|
||||
|
||||
let response = '';
|
||||
for await (const chunk of sendMessage(fullPrompt)) {
|
||||
response += chunk;
|
||||
resultDiv.innerHTML = `<strong>Analysis:</strong><br><div style="white-space: pre-wrap;">${response}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function downloadCustomAudio() {
|
||||
if (window.customAudioBlob) {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(window.customAudioBlob);
|
||||
a.download = `custom-tts-${Date.now()}.mp3`;
|
||||
a.click();
|
||||
}
|
||||
}
|
||||
|
||||
// Custom translation example
|
||||
async function customTranslateExample() {
|
||||
const textArea = document.getElementById('custom-translate-text');
|
||||
const languageSelect = document.getElementById('translate-language');
|
||||
const resultDiv = document.getElementById('custom-translate-result');
|
||||
const button = event.target;
|
||||
|
||||
const text = textArea.value.trim();
|
||||
const targetLanguage = languageSelect.value;
|
||||
|
||||
if (!text) {
|
||||
alert('Please enter some text to translate!');
|
||||
return;
|
||||
}
|
||||
|
||||
button.textContent = 'Processing...';
|
||||
button.disabled = true;
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.textContent = 'Translating...';
|
||||
|
||||
try {
|
||||
const { translateText } = await import('./src/translation.js');
|
||||
const translatedText = await translateText(text, targetLanguage);
|
||||
|
||||
resultDiv.textContent = translatedText;
|
||||
|
||||
} catch (error) {
|
||||
resultDiv.textContent = `Translation failed: ${error.message}`;
|
||||
} finally {
|
||||
button.textContent = 'Translate Text';
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add Enter key support for chat input
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const chatInput = document.getElementById('custom-chat-input');
|
||||
if (chatInput) {
|
||||
chatInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
customChatExample();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const pageInput = document.getElementById('page-analysis-prompt');
|
||||
if (pageInput) {
|
||||
pageInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
customPageAnalysis();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const translateInput = document.getElementById('custom-translate-text');
|
||||
if (translateInput) {
|
||||
translateInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter' && e.ctrlKey) {
|
||||
customTranslateExample();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure all modal and widget functions are available for demos
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
try {
|
||||
// Import and make all widget functions available globally
|
||||
const { openTranslateModal } = await import('./src/translate-modal.js');
|
||||
const { openTTSModal } = await import('./src/tts-modal.js');
|
||||
const { readPageWithHermes } = await import('./src/page-reader.js');
|
||||
|
||||
window.openTranslateModal = openTranslateModal;
|
||||
window.openTTSModal = openTTSModal;
|
||||
window.readPageWithHermes = readPageWithHermes;
|
||||
|
||||
console.log('All modal and widget functions loaded successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to load widget functions:', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<h2>Open Source & Public Domain</h2>
|
||||
|
||||
<p><strong>uncloseai.</strong> is completely <strong>public domain</strong> - free as in beer, free as in freedom. AI for all, internet for all, open web for everyone.</p>
|
||||
|
||||
<p>📦 <strong>Source Code:</strong> <a href="https://git.unturf.com/engineering/unturf/uncloseai.com" target="_blank">https://git.unturf.com/engineering/unturf/uncloseai.com</a></p>
|
||||
|
||||
<p>📄 <strong>Full Documentation:</strong> See README.rst in the repository for complete technical details, architecture, and contribution guidelines.</p>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
3
public/downloads/MD5SUMS
Normal file
3
public/downloads/MD5SUMS
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
75432b900e068e62b48a993d4204f3e1 uncloseai-browser-toy-chrome.zip
|
||||
dedcdc258bb4b4cc658064a27b3df311 uncloseai-browser-toy-firefox.zip
|
||||
3bfe6d53bebe93f7b1bffc771d51ac52 uncloseai-browser-toy-safari.zip
|
||||
3
public/downloads/SHA256SUMS
Normal file
3
public/downloads/SHA256SUMS
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
37d6dc79215e7ecf3008589689c22a3e0073418dcdec3d93418b19a51d6deca7 uncloseai-browser-toy-chrome.zip
|
||||
c3b488ead0b1581e8d18f20f0e238918de0c00e45b61389e408c11b568769aba uncloseai-browser-toy-firefox.zip
|
||||
cc051ba6c6b92fbcca2ca842adc7715b2633cf07623c2a7a9cd315af579763e3 uncloseai-browser-toy-safari.zip
|
||||
BIN
public/downloads/uncloseai-browser-toy-chrome.zip
Normal file
BIN
public/downloads/uncloseai-browser-toy-chrome.zip
Normal file
Binary file not shown.
BIN
public/downloads/uncloseai-browser-toy-firefox.zip
Normal file
BIN
public/downloads/uncloseai-browser-toy-firefox.zip
Normal file
Binary file not shown.
BIN
public/downloads/uncloseai-browser-toy-safari.zip
Normal file
BIN
public/downloads/uncloseai-browser-toy-safari.zip
Normal file
Binary file not shown.
310
public/elixir-examples.html
Normal file
310
public/elixir-examples.html
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Elixir Examples | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/c-examples.html">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html" class="active">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html">Browser Toys</a></li>
|
||||
<li><a href="/inference.html">Inference Setup</a></li>
|
||||
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
|
||||
<li><a href="/crawler.html">Our Crawler</a></li>
|
||||
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>Elixir Examples - Free LLM & TTS AI Service</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 id="elixir-intro">Elixir Examples</h2>
|
||||
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with Elixir using the openai_ex community library. All examples use the same OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
|
||||
|
||||
<p><strong>Available Endpoints:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
|
||||
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
|
||||
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="elixir-client">Elixir Client Installation</h3>
|
||||
<p>Add openai_ex to your <code>mix.exs</code> dependencies:</p>
|
||||
<pre><code class="elixir">def deps do
|
||||
[
|
||||
{:openai_ex, "~> 0.9.18"}
|
||||
]
|
||||
end</code></pre>
|
||||
|
||||
<h2 id="elixir-non-streaming">Non-Streaming Examples</h2>
|
||||
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
|
||||
|
||||
<h3 id="elixir-hermes-non-stream">Using Hermes (General Purpose)</h3>
|
||||
<pre><code class="elixir">config = %OpenaiEx.Config{
|
||||
api_key: "choose-any-value",
|
||||
api_url: "https://hermes.ai.unturf.com/v1"
|
||||
}
|
||||
|
||||
{:ok, response} = OpenaiEx.ChatCompletion.create(
|
||||
config,
|
||||
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
|
||||
messages: [
|
||||
%{role: "user", content: "Give a Python Fizzbuzz solution in one line of code?"}
|
||||
],
|
||||
temperature: 0.5,
|
||||
max_tokens: 150
|
||||
)
|
||||
|
||||
IO.puts(response["choices"] |> List.first() |> get_in(["message", "content"]))
|
||||
</code></pre>
|
||||
|
||||
<h3 id="elixir-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
|
||||
<pre><code class="elixir">config = %OpenaiEx.Config{
|
||||
api_key: "choose-any-value",
|
||||
api_url: "https://qwen.ai.unturf.com/v1"
|
||||
}
|
||||
|
||||
{:ok, response} = OpenaiEx.ChatCompletion.create(
|
||||
config,
|
||||
model: "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M",
|
||||
messages: [
|
||||
%{role: "user", content: "Give a Python Fizzbuzz solution in one line of code?"}
|
||||
],
|
||||
temperature: 0.5,
|
||||
max_tokens: 150
|
||||
)
|
||||
|
||||
IO.puts(response["choices"] |> List.first() |> get_in(["message", "content"]))
|
||||
</code></pre>
|
||||
|
||||
<h2 id="elixir-streaming">Streaming Examples</h2>
|
||||
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
|
||||
|
||||
<h3 id="elixir-hermes-stream">Using Hermes (General Purpose)</h3>
|
||||
<pre><code class="elixir">config = %OpenaiEx.Config{
|
||||
api_key: "choose-any-value",
|
||||
api_url: "https://hermes.ai.unturf.com/v1"
|
||||
}
|
||||
|
||||
OpenaiEx.ChatCompletion.create_stream(
|
||||
config,
|
||||
model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
|
||||
messages: [
|
||||
%{role: "user", content: "Give a Python Fizzbuzz solution in one line of code?"}
|
||||
],
|
||||
temperature: 0.5,
|
||||
max_tokens: 150
|
||||
)
|
||||
|> Stream.each(fn chunk ->
|
||||
content = get_in(chunk, ["choices", Access.at(0), "delta", "content"])
|
||||
if content, do: IO.write(content)
|
||||
end)
|
||||
|> Stream.run()
|
||||
</code></pre>
|
||||
|
||||
<h3 id="elixir-qwen-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
|
||||
<pre><code class="elixir">config = %OpenaiEx.Config{
|
||||
api_key: "choose-any-value",
|
||||
api_url: "https://qwen.ai.unturf.com/v1"
|
||||
}
|
||||
|
||||
OpenaiEx.ChatCompletion.create_stream(
|
||||
config,
|
||||
model: "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M",
|
||||
messages: [
|
||||
%{role: "user", content: "Give a Python Fizzbuzz solution in one line of code?"}
|
||||
],
|
||||
temperature: 0.5,
|
||||
max_tokens: 150
|
||||
)
|
||||
|> Stream.each(fn chunk ->
|
||||
content = get_in(chunk, ["choices", Access.at(0), "delta", "content"])
|
||||
if content, do: IO.write(content)
|
||||
end)
|
||||
|> Stream.run()
|
||||
</code></pre>
|
||||
|
||||
<h2 id="elixir-tts">Text-to-Speech Example</h2>
|
||||
<p>Generate audio speech from text using the TTS endpoint. The audio is saved as an MP3 file.</p>
|
||||
|
||||
<pre><code class="elixir">config = %OpenaiEx.Config{
|
||||
api_key: "YOLO",
|
||||
api_url: "https://speech.ai.unturf.com/v1"
|
||||
}
|
||||
|
||||
{:ok, audio_data} = OpenaiEx.Audio.Speech.create(
|
||||
config,
|
||||
model: "tts-1",
|
||||
voice: "alloy",
|
||||
input: "I think so therefore, Today is a wonderful day to grow something people love!",
|
||||
speed: 0.9
|
||||
)
|
||||
|
||||
File.write!("speech.mp3", audio_data)
|
||||
</code></pre>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current section on scroll
|
||||
const sections = document.querySelectorAll('h2[id], h3[id]');
|
||||
const navLinks = document.querySelectorAll('.table-of-contents a');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
if (window.scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Page TOC -->
|
||||
<aside class="sidebar-right">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#elixir-intro" class="active">Overview</a></li>
|
||||
<li><a href="#elixir-client">Installation</a></li>
|
||||
<li><a href="#elixir-non-streaming">Non-Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#elixir-hermes-non-stream">Hermes</a></li>
|
||||
<li><a href="#elixir-qwen-non-stream">Qwen Coder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#elixir-streaming">Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#elixir-hermes-stream">Hermes</a></li>
|
||||
<li><a href="#elixir-qwen-stream">Qwen Coder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#elixir-tts">Text-to-Speech</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
391
public/go-examples.html
Normal file
391
public/go-examples.html
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Go Examples | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/c-examples.html">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html" class="active">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html">Browser Toys</a></li>
|
||||
<li><a href="/inference.html">Inference Setup</a></li>
|
||||
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
|
||||
<li><a href="/crawler.html">Our Crawler</a></li>
|
||||
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>Go Examples - Free LLM & TTS AI Service</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 id="go-intro">Go Examples</h2>
|
||||
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with Go using the official OpenAI Go client library. All examples use the same OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
|
||||
|
||||
<p><strong>Available Endpoints:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
|
||||
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
|
||||
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="go-client">Go Client Installation</h3>
|
||||
<p>To install the official OpenAI Go package, use <code>go get</code>:</p>
|
||||
<pre><code class="bash">go get github.com/openai/openai-go/v3@v3.3.0</code></pre>
|
||||
|
||||
<h2 id="go-non-streaming">Non-Streaming Examples</h2>
|
||||
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
|
||||
|
||||
<h3 id="go-hermes-non-stream">Using Hermes (General Purpose)</h3>
|
||||
<pre><code class="go">package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/openai/openai-go"
|
||||
"github.com/openai/openai-go/option"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := openai.NewClient(
|
||||
option.WithBaseURL("https://hermes.ai.unturf.com/v1"),
|
||||
option.WithAPIKey("choose-any-value"),
|
||||
)
|
||||
|
||||
response, err := client.Chat.Completions.New(context.Background(), openai.ChatCompletionNewParams{
|
||||
Model: openai.F("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"),
|
||||
Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
|
||||
openai.UserMessage("Give a Python Fizzbuzz solution in one line of code?"),
|
||||
}),
|
||||
Temperature: openai.Float(0.5),
|
||||
MaxTokens: openai.Int(150),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(response.Choices[0].Message.Content)
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3 id="go-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
|
||||
<pre><code class="go">package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/openai/openai-go"
|
||||
"github.com/openai/openai-go/option"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := openai.NewClient(
|
||||
option.WithBaseURL("https://qwen.ai.unturf.com/v1"),
|
||||
option.WithAPIKey("choose-any-value"),
|
||||
)
|
||||
|
||||
response, err := client.Chat.Completions.New(context.Background(), openai.ChatCompletionNewParams{
|
||||
Model: openai.F("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"),
|
||||
Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
|
||||
openai.UserMessage("Give a Python Fizzbuzz solution in one line of code?"),
|
||||
}),
|
||||
Temperature: openai.Float(0.5),
|
||||
MaxTokens: openai.Int(150),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(response.Choices[0].Message.Content)
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2 id="go-streaming">Streaming Examples</h2>
|
||||
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
|
||||
|
||||
<h3 id="go-hermes-stream">Using Hermes (General Purpose)</h3>
|
||||
<pre><code class="go">package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/openai/openai-go"
|
||||
"github.com/openai/openai-go/option"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := openai.NewClient(
|
||||
option.WithBaseURL("https://hermes.ai.unturf.com/v1"),
|
||||
option.WithAPIKey("choose-any-value"),
|
||||
)
|
||||
|
||||
stream := client.Chat.Completions.NewStreaming(context.Background(), openai.ChatCompletionNewParams{
|
||||
Model: openai.F("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"),
|
||||
Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
|
||||
openai.UserMessage("Give a Python Fizzbuzz solution in one line of code?"),
|
||||
}),
|
||||
Temperature: openai.Float(0.5),
|
||||
MaxTokens: openai.Int(150),
|
||||
})
|
||||
|
||||
for stream.Next() {
|
||||
chunk := stream.Current()
|
||||
if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
|
||||
fmt.Print(chunk.Choices[0].Delta.Content)
|
||||
}
|
||||
}
|
||||
|
||||
if err := stream.Err(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3 id="go-qwen-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
|
||||
<pre><code class="go">package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/openai/openai-go"
|
||||
"github.com/openai/openai-go/option"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := openai.NewClient(
|
||||
option.WithBaseURL("https://qwen.ai.unturf.com/v1"),
|
||||
option.WithAPIKey("choose-any-value"),
|
||||
)
|
||||
|
||||
stream := client.Chat.Completions.NewStreaming(context.Background(), openai.ChatCompletionNewParams{
|
||||
Model: openai.F("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"),
|
||||
Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
|
||||
openai.UserMessage("Give a Python Fizzbuzz solution in one line of code?"),
|
||||
}),
|
||||
Temperature: openai.Float(0.5),
|
||||
MaxTokens: openai.Int(150),
|
||||
})
|
||||
|
||||
for stream.Next() {
|
||||
chunk := stream.Current()
|
||||
if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
|
||||
fmt.Print(chunk.Choices[0].Delta.Content)
|
||||
}
|
||||
}
|
||||
|
||||
if err := stream.Err(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2 id="go-tts">Text-to-Speech Example</h2>
|
||||
<p>Generate audio speech from text using the TTS endpoint. The audio is saved as an MP3 file.</p>
|
||||
|
||||
<pre><code class="go">package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"github.com/openai/openai-go"
|
||||
"github.com/openai/openai-go/option"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := openai.NewClient(
|
||||
option.WithBaseURL("https://speech.ai.unturf.com/v1"),
|
||||
option.WithAPIKey("YOLO"),
|
||||
)
|
||||
|
||||
response, err := client.Audio.Speech.New(context.Background(), openai.AudioSpeechNewParams{
|
||||
Model: openai.F(openai.SpeechModelTTS1),
|
||||
Voice: openai.F(openai.AudioSpeechNewParamsVoiceAlloy),
|
||||
Input: openai.F("I think so therefore, Today is a wonderful day to grow something people love!"),
|
||||
Speed: openai.Float(0.9),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
file, err := os.Create("speech.mp3")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, response.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current section on scroll
|
||||
const sections = document.querySelectorAll('h2[id], h3[id]');
|
||||
const navLinks = document.querySelectorAll('.table-of-contents a');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
if (window.scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Page TOC -->
|
||||
<aside class="sidebar-right">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#go-intro" class="active">Overview</a></li>
|
||||
<li><a href="#go-client">Installation</a></li>
|
||||
<li><a href="#go-non-streaming">Non-Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#go-hermes-non-stream">Hermes</a></li>
|
||||
<li><a href="#go-qwen-non-stream">Qwen Coder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#go-streaming">Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#go-hermes-stream">Hermes</a></li>
|
||||
<li><a href="#go-qwen-stream">Qwen Coder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#go-tts">Text-to-Speech</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1
public/google2dd95a728d9c0d9f.html
Normal file
1
public/google2dd95a728d9c0d9f.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
google-site-verification: google2dd95a728d9c0d9f.html
|
||||
297
public/index.html
Normal file
297
public/index.html
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Using Free LLM & Text To Speech Artificial Intelligence Service | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
|
||||
// Reload Remarkbox iframe with new theme if it exists
|
||||
var iframe = document.getElementById('remarkbox-iframe');
|
||||
if (iframe && window.buildRbSrc) {
|
||||
iframe.src = buildRbSrc();
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
<!--
|
||||
<script defer data-domain="ai.unturf.com" src="https://analytics.unturf.com/js/plausible.js"></script>
|
||||
-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/" class="active">Home</a></li>
|
||||
<li><a href="/c-examples.html">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html">Browser Toys</a></li>
|
||||
<li><a href="/inference.html">Inference Setup</a></li>
|
||||
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
|
||||
<li><a href="/crawler.html">Our Crawler</a></li>
|
||||
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>Welcome to uncloseai.com - Free LLM & Text To Speech Artificial Intelligence Service</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<h2 id="introduction">Introducing uncloseai & Hermes & Qwen & TTS Speech Endpoints</h2>
|
||||
<p>At <strong>uncloseai.</strong>, we offer free AI services powered by multiple AI models and a TTS (Text-to-Speech) endpoint. Our mission is to provide accessible AI tools for everyone, embodying the principles of both free as in beer & free as in freedom. You can interact with our models without any cost, and you are encouraged to contribute and build upon the open-source code & models that we use.</p>
|
||||
|
||||
<p><strong>Available Endpoints:</strong></p>
|
||||
<ul>
|
||||
<li><a href="https://hermes.ai.unturf.com/v1/models" target="_blank">hermes.ai.unturf.com/v1</a> - General purpose conversational AI</li>
|
||||
<li><a href="https://qwen.ai.unturf.com/v1/models" target="_blank">qwen.ai.unturf.com/v1</a> - Specialized coding model</li>
|
||||
<li><a href="https://speech.ai.unturf.com/v1/models" target="_blank">speech.ai.unturf.com/v1</a> - Text-to-speech</li>
|
||||
</ul>
|
||||
<p>See our <a href="/inference.html#model-discovery">Model Discovery docs</a> to query the current model IDs being hosted.</p>
|
||||
|
||||
<p>We intend to be a drop in replacement, you can use the existing open source OpenAI client to communicate with us.</p>
|
||||
|
||||
<h2 id="client-side">uncloseai.js - Client-Side AI for Everyone</h2>
|
||||
|
||||
<p>This page includes the <a href="/uncloseai-js.html">uncloseai.js</a> script which adds a floating "uncloseai." button in the bottom right corner. Click it to interact with Hermes AI directly from your browser.</p>
|
||||
|
||||
<p>This is a <strong>web client-only solution</strong> - no server required. Add AI capabilities to any static site or CDN with just one line of code.</p>
|
||||
|
||||
<h3 id="installation">✨ One-Line Installation</h3>
|
||||
<pre><code class="html"><script src="https://uncloseai.com/uncloseai.js" type="module"></script></code></pre>
|
||||
|
||||
<p><a href="/uncloseai-js.html"><strong>📖 Read the full uncloseai.js documentation →</strong></a></p>
|
||||
|
||||
<h3 id="browser-toys">🧩 Browser Toys</h3>
|
||||
<p>Want the uncloseai. button on <strong>every</strong> web page without adding any code? Install a browser extension. Lightweight wrappers that inject uncloseai.js into every page you visit. No server, no accounts. Chat history stays in your browser's localStorage, scoped per domain.</p>
|
||||
<ul>
|
||||
<li><strong>Chrome / Edge / Brave / Vivaldi / Arc</strong> — Manifest V3</li>
|
||||
<li><strong>Firefox</strong> — Manifest V2</li>
|
||||
<li><strong>Safari</strong> — Web Extension (via Xcode)</li>
|
||||
</ul>
|
||||
<p><a href="/browser-toys.html"><strong>🧩 Get Browser Toys →</strong></a></p>
|
||||
|
||||
<h2 id="examples">Code Examples</h2>
|
||||
|
||||
<p>We provide comprehensive code examples for using the <strong>uncloseai.</strong> API with popular programming languages. Each example demonstrates both streaming and non-streaming modes with Hermes (general purpose AI) and Qwen 3 Coder (specialized coding model).</p>
|
||||
|
||||
<h3>Quick Start Examples:</h3>
|
||||
<ul>
|
||||
<li><a href="/python-examples.html"><strong>Python Examples</strong></a> - Complete examples using the OpenAI Python client library</li>
|
||||
<li><a href="/nodejs-examples.html"><strong>Node.js Examples</strong></a> - Complete examples using the OpenAI Node.js client library</li>
|
||||
</ul>
|
||||
|
||||
<h3>42 Programming Languages:</h3>
|
||||
<p>We provide SDK implementations with streaming support for 42 programming languages! Browse all language examples:</p>
|
||||
<p>🔗 <a href="/languages" target="_blank"><strong>Browse All 42 Language Examples →</strong></a></p>
|
||||
|
||||
<p>Includes: AWK, Bash, C, C++, C#, Clojure, COBOL, Common Lisp, Crystal, D, Dart, Deno, Elixir, Erlang, F#, Fortran, Go, Groovy, Haskell, Java, JavaScript, Julia, Kotlin, Lua, Mojo*, Nim, OCaml, Odin, Perl, PHP, PowerShell, Prolog, Python, R, Ruby, Rust, Scala, Scheme, Tcl, V, VB.NET, and Zig!</p>
|
||||
<p><small>* Mojo implementation is non-streaming only (Lightbug HTTP library doesn't yet support SSE streaming as of 2025-10-15)</small></p>
|
||||
|
||||
<h3>uncloseai. book - Machine Learning Reference Guide:</h3>
|
||||
<p>📚 <a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank"><strong>Purchase the uncloseai. book on unturf.com Shop →</strong></a></p>
|
||||
<p>Comprehensive reference guide covering inference clients, streaming implementations, and best practices for all 42 programming languages.</p>
|
||||
|
||||
<p>All examples use the OpenAI-compatible API interface, making it easy to integrate with existing code. Simply change the <code>base_url</code> to point to our endpoints. See our <a href="/inference.html#model-discovery">Model Discovery docs</a> for endpoint URLs and how to query current model IDs.</p>
|
||||
|
||||
<h2 id="inference">How we run inference</h2>
|
||||
<p>Want to contribute idle GPU time or reproduce everything in your own cluster?</p>
|
||||
<p><a href="/inference.html"><strong>📖 Read the full inference setup documentation →</strong></a></p>
|
||||
|
||||
<h2 id="open-source">Open Source & Public Domain</h2>
|
||||
|
||||
<p><strong>uncloseai.</strong> is completely <strong>public domain</strong> - free as in beer, free as in freedom. AI for all, internet for all, open web for everyone.</p>
|
||||
|
||||
<p>📦 <strong>Source Code:</strong> <a href="https://git.unturf.com/engineering/unturf/uncloseai.com" target="_blank">https://git.unturf.com/engineering/unturf/uncloseai.com</a></p>
|
||||
|
||||
<p>📄 <strong>Full Documentation:</strong> See README.rst in the repository for complete technical details, architecture, and contribution guidelines.</p>
|
||||
|
||||
<h2 id="discussions">Questions & Comments & Discussions</h2>
|
||||
|
||||
Use the <a href="https://www.remarkbox.com" target="_blank">Remarkbox</a> below to tell us what you think!
|
||||
|
||||
<div id="remarkbox-div">
|
||||
<noscript>
|
||||
<iframe id=remarkbox-iframe src="https://my.remarkbox.com/embed?nojs=true" style="height:600px;width:100%;border:none!important" tabindex=0></iframe>
|
||||
</noscript>
|
||||
</div>
|
||||
<script src="https://my.remarkbox.com/static/js/iframe-resizer/iframeResizer.min.js"></script>
|
||||
<script>
|
||||
var rb_owner_key = "944c8dfa-8b2b-11ef-af0e-29ab4fb285a0";
|
||||
var thread_uri = window.location.href;
|
||||
var thread_title = window.document.title;
|
||||
var thread_fragment = window.location.hash;
|
||||
|
||||
function getCurrentTheme() {
|
||||
// Check if data-theme is set on documentElement
|
||||
var theme = document.documentElement.getAttribute('data-theme');
|
||||
if (theme) {
|
||||
return theme; // 'light' or 'dark'
|
||||
}
|
||||
// If auto, detect system preference
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
|
||||
function buildRbSrc() {
|
||||
var mode = getCurrentTheme();
|
||||
return "https://my.remarkbox.com/embed" +
|
||||
"?rb_owner_key=" + rb_owner_key +
|
||||
"&thread_title=" + encodeURI(thread_title) +
|
||||
"&thread_uri=" + encodeURIComponent(thread_uri) +
|
||||
"&mode=" + mode +
|
||||
thread_fragment;
|
||||
}
|
||||
|
||||
var rb_src = buildRbSrc();
|
||||
|
||||
function create_remarkbox_iframe() {
|
||||
var ifrm = document.createElement("iframe");
|
||||
ifrm.setAttribute("id", "remarkbox-iframe");
|
||||
ifrm.setAttribute("scrolling", "no");
|
||||
ifrm.setAttribute("src", rb_src);
|
||||
ifrm.setAttribute("frameborder", "0");
|
||||
ifrm.setAttribute("tabindex", "0");
|
||||
ifrm.setAttribute("title", "Remarkbox");
|
||||
ifrm.style.width = "100%";
|
||||
document.getElementById("remarkbox-div").appendChild(ifrm);
|
||||
}
|
||||
create_remarkbox_iframe();
|
||||
iFrameResize(
|
||||
{
|
||||
checkOrigin: ["https://my.remarkbox.com"],
|
||||
inPageLinks: true,
|
||||
initCallback: function(e) { e.iFrameResizer.moveToAnchor(thread_fragment) }
|
||||
},
|
||||
document.getElementById("remarkbox-iframe")
|
||||
);
|
||||
</script>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current section on scroll
|
||||
const sections = document.querySelectorAll('h2[id], h3[id]');
|
||||
const navLinks = document.querySelectorAll('.table-of-contents a');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
if (window.scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
298
public/inference.html
Normal file
298
public/inference.html
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>How We Run Inference | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/c-examples.html">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html">Browser Toys</a></li>
|
||||
<li><a href="/inference.html" class="active">Inference Setup</a></li>
|
||||
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
|
||||
<li><a href="/crawler.html">Our Crawler</a></li>
|
||||
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>How We Run Inference</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 id="inference-intro">How we run inference</h2>
|
||||
<p>This section is optional. This is only if you wanted to try to contribute idle GPU time to the project or if you wanted to reproduce everything in your own cluster.</p>
|
||||
|
||||
<h3 id="vllm-setup">vLLM Setup</h3>
|
||||
<p>We use vLLM to run models, generally with full f16 safetensors. We make sure to use a virtualenv to hold the dependencies.</p>
|
||||
<p><strong>Note:</strong> For Hermes, we use an FP8 quant by adamo1139 (adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic), which is optimized for 4090 and 3090 GPUs.</p>
|
||||
<p><strong>Tool Calling:</strong> The <code>--enable-auto-tool-choice</code> and <code>--tool-call-parser hermes</code> flags are required for Hermes 3 to support function calling via the OpenAI-compatible API. Without these flags, tool calling requests will fail.</p>
|
||||
<p>We are considering supporting ollama for better quant support.</p>
|
||||
<p>Stand up a replica cluster on a new domain.</p>
|
||||
<pre><code>sudo apt-get install gcc python3.12-dev python3.12-venv
|
||||
cd ~
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
pip install vllm
|
||||
python -m vllm.entrypoints.openai.api_server --model adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic --host 0.0.0.0 --port 18888 --max-model-len 82000 --enable-auto-tool-choice --tool-call-parser hermes
|
||||
</code></pre>
|
||||
|
||||
<h3 id="proxy-setup">Proxy Setup</h3>
|
||||
<p>If you want to see how we setup the proxy, check out <a href="https://git.unturf.com/-/snippets/3" target="_blank">/etc/caddy/Caddyfile</a></p>
|
||||
|
||||
<pre><code>hermes.ai.unturf.com {
|
||||
reverse_proxy <removed>:18888
|
||||
log {
|
||||
output file /var/log/caddy/hermes.ai.unturf.com.log {
|
||||
roll_size 50mb
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
}
|
||||
|
||||
qwen.ai.unturf.com {
|
||||
reverse_proxy <removed>:18889
|
||||
log {
|
||||
output file /var/log/caddy/qwen.ai.unturf.com.log {
|
||||
roll_size 50mb
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
}
|
||||
|
||||
qwen-vl.ai.unturf.com {
|
||||
reverse_proxy <removed>:18890
|
||||
log {
|
||||
output file /var/log/caddy/qwen-vl.ai.unturf.com.log {
|
||||
roll_size 50mb
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
}
|
||||
|
||||
speech.ai.unturf.com {
|
||||
reverse_proxy <removed>:8000
|
||||
log {
|
||||
output file /var/log/caddy/speech.ai.unturf.com.log {
|
||||
roll_size 50mb
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3 id="model-discovery">Model Discovery</h3>
|
||||
<p>vLLM provides an OpenAI-compatible API with built-in documentation. You can discover available models and explore the full API using these endpoints:</p>
|
||||
|
||||
<h4>Swagger Documentation</h4>
|
||||
<p>Access the interactive API docs at the <code>/docs</code> endpoint:</p>
|
||||
<p><a href="https://hermes.ai.unturf.com/docs" target="_blank">hermes.ai.unturf.com/docs</a> - Hermes endpoint Swagger docs</p>
|
||||
<p><a href="https://qwen.ai.unturf.com/docs" target="_blank">qwen.ai.unturf.com/docs</a> - Qwen endpoint Swagger docs</p>
|
||||
<p><a href="https://qwen-vl.ai.unturf.com/docs" target="_blank">qwen-vl.ai.unturf.com/docs</a> - Qwen VL endpoint Swagger docs</p>
|
||||
<p>The Swagger UI lets you explore all available endpoints, see request/response schemas, and test API calls directly in your browser.</p>
|
||||
|
||||
<h4>Model Discovery</h4>
|
||||
<p>To get the current model ID being hosted, query the <code>/v1/models</code> endpoint:</p>
|
||||
<ul>
|
||||
<li><a href="https://hermes.ai.unturf.com/v1/models" target="_blank">hermes.ai.unturf.com/v1/models</a> - Hermes models</li>
|
||||
<li><a href="https://qwen.ai.unturf.com/v1/models" target="_blank">qwen.ai.unturf.com/v1/models</a> - Qwen models</li>
|
||||
<li><a href="https://qwen-vl.ai.unturf.com/v1/models" target="_blank">qwen-vl.ai.unturf.com/v1/models</a> - Qwen VL (vision) models</li>
|
||||
</ul>
|
||||
<p>Or via curl:</p>
|
||||
<pre><code class="language-bash">curl https://hermes.ai.unturf.com/v1/models
|
||||
curl https://qwen.ai.unturf.com/v1/models
|
||||
curl https://qwen-vl.ai.unturf.com/v1/models
|
||||
</code></pre>
|
||||
|
||||
<p>Example response:</p>
|
||||
<pre><code class="language-json">{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
|
||||
"object": "model",
|
||||
"created": 1735689600,
|
||||
"owned_by": "vllm",
|
||||
"root": "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
|
||||
"max_model_len": 82000
|
||||
}
|
||||
]
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<p>The <code>id</code> field contains the model name you should use in your API calls. The <code>max_model_len</code> field tells you the maximum context length supported.</p>
|
||||
|
||||
<p><strong>Tip:</strong> Always query <code>/v1/models</code> programmatically rather than hardcoding model names. This ensures your code works even when models are updated or swapped.</p>
|
||||
|
||||
<h3 id="rate-limiting">Rate Limiting</h3>
|
||||
<p>Rate limiting is configured based on client IP address: 3 requests per second per IP per endpoint.</p>
|
||||
|
||||
<h3 id="next-steps">Next Steps</h3>
|
||||
<p>Ready to add text-to-speech to your application?</p>
|
||||
<p><a href="/text-to-speech.html"><strong>📖 Read the Text-to-Speech documentation →</strong></a></p>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current section on scroll
|
||||
const sections = document.querySelectorAll('h2[id], h3[id]');
|
||||
const navLinks = document.querySelectorAll('.table-of-contents a');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
if (window.scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Page TOC -->
|
||||
<aside class="sidebar-right">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#inference-intro" class="active">Overview</a></li>
|
||||
<li><a href="#vllm-setup">vLLM Setup</a></li>
|
||||
<li><a href="#proxy-setup">Proxy Setup</a></li>
|
||||
<li><a href="#model-discovery">Model Discovery</a></li>
|
||||
<li><a href="#rate-limiting">Rate Limiting</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
384
public/java-examples.html
Normal file
384
public/java-examples.html
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Java Examples | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/c-examples.html">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html" class="active">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html">Browser Toys</a></li>
|
||||
<li><a href="/inference.html">Inference Setup</a></li>
|
||||
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
|
||||
<li><a href="/crawler.html">Our Crawler</a></li>
|
||||
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>Java Examples - Free LLM & TTS AI Service</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 id="java-intro">Java Examples</h2>
|
||||
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with Java using the official OpenAI Java library. All examples use the same OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
|
||||
|
||||
<p><strong>Available Endpoints:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
|
||||
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
|
||||
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="java-client">Java Client Installation</h3>
|
||||
<p>Add the official OpenAI Java library to your Maven <code>pom.xml</code>:</p>
|
||||
<pre><code class="xml"><dependency>
|
||||
<groupId>com.openai</groupId>
|
||||
<artifactId>openai-java</artifactId>
|
||||
<version>4.3.0</version>
|
||||
</dependency></code></pre>
|
||||
|
||||
<p>Or for Gradle:</p>
|
||||
<pre><code class="groovy">implementation 'com.openai:openai-java:4.3.0'</code></pre>
|
||||
|
||||
<h2 id="java-non-streaming">Non-Streaming Examples</h2>
|
||||
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
|
||||
|
||||
<h3 id="java-hermes-non-stream">Using Hermes (General Purpose)</h3>
|
||||
<pre><code class="java">import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
import com.openai.models.*;
|
||||
import java.util.List;
|
||||
|
||||
public class HermesExample {
|
||||
public static void main(String[] args) {
|
||||
OpenAIClient client = OpenAIOkHttpClient.builder()
|
||||
.apiKey("choose-any-value")
|
||||
.baseURL("https://hermes.ai.unturf.com/v1")
|
||||
.build();
|
||||
|
||||
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
|
||||
.model("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||
.addMessage(ChatCompletionMessageParam.ofChatCompletionUserMessageParam(
|
||||
ChatCompletionUserMessageParam.builder()
|
||||
.role(ChatCompletionUserMessageParam.Role.USER)
|
||||
.content(ChatCompletionUserMessageParam.Content.ofTextContent(
|
||||
"Give a Python Fizzbuzz solution in one line of code?"
|
||||
))
|
||||
.build()
|
||||
))
|
||||
.temperature(0.5)
|
||||
.maxTokens(150L)
|
||||
.build();
|
||||
|
||||
ChatCompletion completion = client.chat().completions().create(params);
|
||||
System.out.println(completion.choices().get(0).message().content().get());
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3 id="java-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
|
||||
<pre><code class="java">import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
import com.openai.models.*;
|
||||
import java.util.List;
|
||||
|
||||
public class QwenExample {
|
||||
public static void main(String[] args) {
|
||||
OpenAIClient client = OpenAIOkHttpClient.builder()
|
||||
.apiKey("choose-any-value")
|
||||
.baseURL("https://qwen.ai.unturf.com/v1")
|
||||
.build();
|
||||
|
||||
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
|
||||
.model("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
|
||||
.addMessage(ChatCompletionMessageParam.ofChatCompletionUserMessageParam(
|
||||
ChatCompletionUserMessageParam.builder()
|
||||
.role(ChatCompletionUserMessageParam.Role.USER)
|
||||
.content(ChatCompletionUserMessageParam.Content.ofTextContent(
|
||||
"Give a Python Fizzbuzz solution in one line of code?"
|
||||
))
|
||||
.build()
|
||||
))
|
||||
.temperature(0.5)
|
||||
.maxTokens(150L)
|
||||
.build();
|
||||
|
||||
ChatCompletion completion = client.chat().completions().create(params);
|
||||
System.out.println(completion.choices().get(0).message().content().get());
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2 id="java-streaming">Streaming Examples</h2>
|
||||
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
|
||||
|
||||
<h3 id="java-hermes-stream">Using Hermes (General Purpose)</h3>
|
||||
<pre><code class="java">import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
import com.openai.models.*;
|
||||
|
||||
public class HermesStreamExample {
|
||||
public static void main(String[] args) {
|
||||
OpenAIClient client = OpenAIOkHttpClient.builder()
|
||||
.apiKey("choose-any-value")
|
||||
.baseURL("https://hermes.ai.unturf.com/v1")
|
||||
.build();
|
||||
|
||||
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
|
||||
.model("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||
.addMessage(ChatCompletionMessageParam.ofChatCompletionUserMessageParam(
|
||||
ChatCompletionUserMessageParam.builder()
|
||||
.role(ChatCompletionUserMessageParam.Role.USER)
|
||||
.content(ChatCompletionUserMessageParam.Content.ofTextContent(
|
||||
"Give a Python Fizzbuzz solution in one line of code?"
|
||||
))
|
||||
.build()
|
||||
))
|
||||
.temperature(0.5)
|
||||
.maxTokens(150L)
|
||||
.build();
|
||||
|
||||
client.chat().completions().createStreaming(params)
|
||||
.forEach(chunk -> {
|
||||
String content = chunk.choices().get(0).delta().content().orElse("");
|
||||
if (!content.isEmpty()) {
|
||||
System.out.print(content);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3 id="java-qwen-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
|
||||
<pre><code class="java">import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
import com.openai.models.*;
|
||||
|
||||
public class QwenStreamExample {
|
||||
public static void main(String[] args) {
|
||||
OpenAIClient client = OpenAIOkHttpClient.builder()
|
||||
.apiKey("choose-any-value")
|
||||
.baseURL("https://qwen.ai.unturf.com/v1")
|
||||
.build();
|
||||
|
||||
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
|
||||
.model("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
|
||||
.addMessage(ChatCompletionMessageParam.ofChatCompletionUserMessageParam(
|
||||
ChatCompletionUserMessageParam.builder()
|
||||
.role(ChatCompletionUserMessageParam.Role.USER)
|
||||
.content(ChatCompletionUserMessageParam.Content.ofTextContent(
|
||||
"Give a Python Fizzbuzz solution in one line of code?"
|
||||
))
|
||||
.build()
|
||||
))
|
||||
.temperature(0.5)
|
||||
.maxTokens(150L)
|
||||
.build();
|
||||
|
||||
client.chat().completions().createStreaming(params)
|
||||
.forEach(chunk -> {
|
||||
String content = chunk.choices().get(0).delta().content().orElse("");
|
||||
if (!content.isEmpty()) {
|
||||
System.out.print(content);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2 id="java-tts">Text-to-Speech Example</h2>
|
||||
<p>Generate audio speech from text using the TTS endpoint. The audio is saved as an MP3 file.</p>
|
||||
|
||||
<pre><code class="java">import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
import com.openai.models.*;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TTSExample {
|
||||
public static void main(String[] args) throws IOException {
|
||||
OpenAIClient client = OpenAIOkHttpClient.builder()
|
||||
.apiKey("YOLO")
|
||||
.baseURL("https://speech.ai.unturf.com/v1")
|
||||
.build();
|
||||
|
||||
SpeechCreateParams params = SpeechCreateParams.builder()
|
||||
.model(SpeechModel.TTS_1)
|
||||
.voice(SpeechCreateParams.Voice.ALLOY)
|
||||
.input("I think so therefore, Today is a wonderful day to grow something people love!")
|
||||
.speed(0.9)
|
||||
.build();
|
||||
|
||||
byte[] audioBytes = client.audio().speech().create(params);
|
||||
|
||||
try (FileOutputStream fos = new FileOutputStream("speech.mp3")) {
|
||||
fos.write(audioBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current section on scroll
|
||||
const sections = document.querySelectorAll('h2[id], h3[id]');
|
||||
const navLinks = document.querySelectorAll('.table-of-contents a');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
if (window.scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Page TOC -->
|
||||
<aside class="sidebar-right">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#java-intro" class="active">Overview</a></li>
|
||||
<li><a href="#java-client">Installation</a></li>
|
||||
<li><a href="#java-non-streaming">Non-Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#java-hermes-non-stream">Hermes</a></li>
|
||||
<li><a href="#java-qwen-non-stream">Qwen Coder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#java-streaming">Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#java-hermes-stream">Hermes</a></li>
|
||||
<li><a href="#java-qwen-stream">Qwen Coder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#java-tts">Text-to-Speech</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
375
public/kotlin-examples.html
Normal file
375
public/kotlin-examples.html
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
<!--
|
||||
This is free software for the public good of a permacomputer hosted at
|
||||
permacomputer.com, an always-on computer by the people, for the people.
|
||||
One which is durable, easy to repair, & distributed like tap water
|
||||
for machine learning intelligence.
|
||||
|
||||
The permacomputer is community-owned infrastructure optimized around
|
||||
four values:
|
||||
|
||||
TRUTH First principles, math & science, open source code freely distributed
|
||||
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
LOVE Be yourself without hurting others, cooperation through natural law
|
||||
|
||||
This software contributes to that vision by making machine learning
|
||||
accessible to everyone through a free, open, embeddable chat interface.
|
||||
Code is seeds to sprout on any abandoned technology.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#43a047">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Kotlin Examples | uncloseai.com</title>
|
||||
|
||||
<!-- PicoCSS -->
|
||||
<link rel="stylesheet" href="/css/pico.classless.min.css">
|
||||
<!-- ChunkFive Font -->
|
||||
<link rel="stylesheet" href="/css/chunkfive/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<!-- Sidebar Theme -->
|
||||
<link rel="stylesheet" href="/css/sidebar-theme.css">
|
||||
|
||||
<!-- Highlight.js for syntax highlighting -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Theme Switcher Script -->
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
if (theme === "auto") {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable custom styling for uncloseai.js - use PicoCSS instead
|
||||
window.UNCLOSEAI_CUSTOM_STYLING = false;
|
||||
</script>
|
||||
|
||||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Left sidebar - Main Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/c-examples.html">C Examples</a></li>
|
||||
<li><a href="/csharp-examples.html">C# Examples</a></li>
|
||||
<li><a href="/dart-examples.html">Dart Examples</a></li>
|
||||
<li><a href="/elixir-examples.html">Elixir Examples</a></li>
|
||||
<li><a href="/go-examples.html">Go Examples</a></li>
|
||||
<li><a href="/java-examples.html">Java Examples</a></li>
|
||||
<li><a href="/kotlin-examples.html" class="active">Kotlin Examples</a></li>
|
||||
<li><a href="/nodejs-examples.html">Node.js Examples</a></li>
|
||||
<li><a href="/php-examples.html">PHP Examples</a></li>
|
||||
<li><a href="/python-examples.html">Python Examples</a></li>
|
||||
<li><a href="/ruby-examples.html">Ruby Examples</a></li>
|
||||
<li><a href="/rust-examples.html">Rust Examples</a></li>
|
||||
<li><a href="/swift-examples.html">Swift Examples</a></li>
|
||||
<li><a href="/uncloseai-js.html">uncloseai.js Docs</a></li>
|
||||
<li><a href="/uncloseai-js-styleguide.html">Styleguide</a></li>
|
||||
<li><a href="/cli.html">uncloseai-cli</a></li>
|
||||
<li><a href="/browser-toys.html">Browser Toys</a></li>
|
||||
<li><a href="/inference.html">Inference Setup</a></li>
|
||||
<li><a href="/text-to-speech.html">Text-to-Speech</a></li>
|
||||
<li><a href="/crawler.html">Our Crawler</a></li>
|
||||
<li><a href="/reverse-retrieval-augmented-generations-rag.html">Reverse RAG</a></li>
|
||||
<li><a href="/languages" target="_blank">🔗 All Languages</a></li>
|
||||
<li><a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank">📚 Book</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<header>
|
||||
<hgroup>
|
||||
<a href="https://uncloseai.com"><h1 class="unturf" style="font-family: 'ChunkFiveRegular';">uncloseai.</h1></a>
|
||||
<p>Kotlin Examples - Free LLM & TTS AI Service</p>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" onclick="switchTheme('auto')">Auto</a></li>
|
||||
<li><a href="#" onclick="switchTheme('light')">Light</a></li>
|
||||
<li><a href="#" onclick="switchTheme('dark')">Dark</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2 id="kotlin-intro">Kotlin Examples</h2>
|
||||
<p>This page demonstrates how to use the <strong>uncloseai.</strong> API endpoints with Kotlin using the openai-kotlin community library. All examples use the same OpenAI-compatible API interface, making it easy to switch between different models and endpoints.</p>
|
||||
|
||||
<p><strong>Available Endpoints:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Hermes:</strong> <code>https://hermes.ai.unturf.com/v1</code> - General purpose conversational AI</li>
|
||||
<li><strong>Qwen 3 Coder:</strong> <code>https://qwen.ai.unturf.com/v1</code> - Specialized coding model</li>
|
||||
<li><strong>TTS:</strong> <code>https://speech.ai.unturf.com/v1</code> - Text-to-speech generation</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="kotlin-client">Kotlin Client Installation</h3>
|
||||
<p>Add openai-kotlin to your <code>build.gradle.kts</code>:</p>
|
||||
<pre><code class="kotlin">dependencies {
|
||||
implementation("com.aallam.openai:openai-client:3.8.2")
|
||||
implementation("io.ktor:ktor-client-okhttp:2.3.12")
|
||||
}</code></pre>
|
||||
|
||||
<h2 id="kotlin-non-streaming">Non-Streaming Examples</h2>
|
||||
<p>Non-streaming mode waits for the complete response before returning. This is simpler to use but provides no intermediate feedback during generation.</p>
|
||||
|
||||
<h3 id="kotlin-hermes-non-stream">Using Hermes (General Purpose)</h3>
|
||||
<pre><code class="kotlin">import com.aallam.openai.api.chat.*
|
||||
import com.aallam.openai.api.model.ModelId
|
||||
import com.aallam.openai.client.OpenAI
|
||||
import com.aallam.openai.client.OpenAIConfig
|
||||
import com.aallam.openai.client.OpenAIHost
|
||||
|
||||
suspend fun main() {
|
||||
val openAI = OpenAI(
|
||||
OpenAIConfig(
|
||||
token = "choose-any-value",
|
||||
host = OpenAIHost("https://hermes.ai.unturf.com/v1")
|
||||
)
|
||||
)
|
||||
|
||||
val chatCompletionRequest = ChatCompletionRequest(
|
||||
model = ModelId("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"),
|
||||
messages = listOf(
|
||||
ChatMessage(
|
||||
role = ChatRole.User,
|
||||
content = "Give a Python Fizzbuzz solution in one line of code?"
|
||||
)
|
||||
),
|
||||
temperature = 0.5,
|
||||
maxTokens = 150
|
||||
)
|
||||
|
||||
val completion = openAI.chatCompletion(chatCompletionRequest)
|
||||
println(completion.choices[0].message.content)
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3 id="kotlin-qwen-non-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
|
||||
<pre><code class="kotlin">import com.aallam.openai.api.chat.*
|
||||
import com.aallam.openai.api.model.ModelId
|
||||
import com.aallam.openai.client.OpenAI
|
||||
import com.aallam.openai.client.OpenAIConfig
|
||||
import com.aallam.openai.client.OpenAIHost
|
||||
|
||||
suspend fun main() {
|
||||
val openAI = OpenAI(
|
||||
OpenAIConfig(
|
||||
token = "choose-any-value",
|
||||
host = OpenAIHost("https://qwen.ai.unturf.com/v1")
|
||||
)
|
||||
)
|
||||
|
||||
val chatCompletionRequest = ChatCompletionRequest(
|
||||
model = ModelId("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"),
|
||||
messages = listOf(
|
||||
ChatMessage(
|
||||
role = ChatRole.User,
|
||||
content = "Give a Python Fizzbuzz solution in one line of code?"
|
||||
)
|
||||
),
|
||||
temperature = 0.5,
|
||||
maxTokens = 150
|
||||
)
|
||||
|
||||
val completion = openAI.chatCompletion(chatCompletionRequest)
|
||||
println(completion.choices[0].message.content)
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2 id="kotlin-streaming">Streaming Examples</h2>
|
||||
<p>Streaming mode returns chunks of the response as they are generated, providing real-time feedback. This is ideal for interactive applications and long responses.</p>
|
||||
|
||||
<h3 id="kotlin-hermes-stream">Using Hermes (General Purpose)</h3>
|
||||
<pre><code class="kotlin">import com.aallam.openai.api.chat.*
|
||||
import com.aallam.openai.api.model.ModelId
|
||||
import com.aallam.openai.client.OpenAI
|
||||
import com.aallam.openai.client.OpenAIConfig
|
||||
import com.aallam.openai.client.OpenAIHost
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
||||
suspend fun main() {
|
||||
val openAI = OpenAI(
|
||||
OpenAIConfig(
|
||||
token = "choose-any-value",
|
||||
host = OpenAIHost("https://hermes.ai.unturf.com/v1")
|
||||
)
|
||||
)
|
||||
|
||||
val chatCompletionRequest = ChatCompletionRequest(
|
||||
model = ModelId("adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"),
|
||||
messages = listOf(
|
||||
ChatMessage(
|
||||
role = ChatRole.User,
|
||||
content = "Give a Python Fizzbuzz solution in one line of code?"
|
||||
)
|
||||
),
|
||||
temperature = 0.5,
|
||||
maxTokens = 150
|
||||
)
|
||||
|
||||
openAI.chatCompletions(chatCompletionRequest).collect { chunk ->
|
||||
chunk.choices.forEach { choice ->
|
||||
choice.delta.content?.let { print(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3 id="kotlin-qwen-stream">Using Qwen 3 Coder (Specialized for Coding)</h3>
|
||||
<pre><code class="kotlin">import com.aallam.openai.api.chat.*
|
||||
import com.aallam.openai.api.model.ModelId
|
||||
import com.aallam.openai.client.OpenAI
|
||||
import com.aallam.openai.client.OpenAIConfig
|
||||
import com.aallam.openai.client.OpenAIHost
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
||||
suspend fun main() {
|
||||
val openAI = OpenAI(
|
||||
OpenAIConfig(
|
||||
token = "choose-any-value",
|
||||
host = OpenAIHost("https://qwen.ai.unturf.com/v1")
|
||||
)
|
||||
)
|
||||
|
||||
val chatCompletionRequest = ChatCompletionRequest(
|
||||
model = ModelId("hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"),
|
||||
messages = listOf(
|
||||
ChatMessage(
|
||||
role = ChatRole.User,
|
||||
content = "Give a Python Fizzbuzz solution in one line of code?"
|
||||
)
|
||||
),
|
||||
temperature = 0.5,
|
||||
maxTokens = 150
|
||||
)
|
||||
|
||||
openAI.chatCompletions(chatCompletionRequest).collect { chunk ->
|
||||
chunk.choices.forEach { choice ->
|
||||
choice.delta.content?.let { print(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2 id="kotlin-tts">Text-to-Speech Example</h2>
|
||||
<p>Generate audio speech from text using the TTS endpoint. The audio is saved as an MP3 file.</p>
|
||||
|
||||
<pre><code class="kotlin">import com.aallam.openai.api.audio.SpeechRequest
|
||||
import com.aallam.openai.api.audio.Voice
|
||||
import com.aallam.openai.api.model.ModelId
|
||||
import com.aallam.openai.client.OpenAI
|
||||
import com.aallam.openai.client.OpenAIConfig
|
||||
import com.aallam.openai.client.OpenAIHost
|
||||
import java.io.File
|
||||
|
||||
suspend fun main() {
|
||||
val openAI = OpenAI(
|
||||
OpenAIConfig(
|
||||
token = "YOLO",
|
||||
host = OpenAIHost("https://speech.ai.unturf.com/v1")
|
||||
)
|
||||
)
|
||||
|
||||
val speechRequest = SpeechRequest(
|
||||
model = ModelId("tts-1"),
|
||||
input = "I think so therefore, Today is a wonderful day to grow something people love!",
|
||||
voice = Voice.Alloy,
|
||||
speed = 0.9
|
||||
)
|
||||
|
||||
val speech = openAI.speech(speechRequest)
|
||||
File("speech.mp3").writeBytes(speech)
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<footer>
|
||||
<small>© uncloseai. 2025</small>
|
||||
<br>
|
||||
<small>Stylesheets by <a href="https://picocss.com" target="_blank">PicoCSS</a></small>
|
||||
<small>& <a href="https://highlightjs.org/" target="_blank">highlight.js</a></small>
|
||||
<br>
|
||||
<small><a href="/privacy-policy.html">Privacy Policy</a> | <a href="/terms-of-use.html">Terms of Use</a></small>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Simple active link highlighting
|
||||
document.querySelectorAll('.table-of-contents a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll('.table-of-contents a').forEach(a => a.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight current section on scroll
|
||||
const sections = document.querySelectorAll('h2[id], h3[id]');
|
||||
const navLinks = document.querySelectorAll('.table-of-contents a');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
if (window.scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + current) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Page TOC -->
|
||||
<aside class="sidebar-right">
|
||||
<div class="table-of-contents">
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#kotlin-intro" class="active">Overview</a></li>
|
||||
<li><a href="#kotlin-client">Installation</a></li>
|
||||
<li><a href="#kotlin-non-streaming">Non-Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#kotlin-hermes-non-stream">Hermes</a></li>
|
||||
<li><a href="#kotlin-qwen-non-stream">Qwen Coder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#kotlin-streaming">Streaming</a>
|
||||
<ul>
|
||||
<li><a href="#kotlin-hermes-stream">Hermes</a></li>
|
||||
<li><a href="#kotlin-qwen-stream">Qwen Coder</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#kotlin-tts">Text-to-Speech</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
15
public/languages/awk/Dockerfile
Normal file
15
public/languages/awk/Dockerfile
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# GNU AWK 5.3.2 (checked 2025-10-13: gawk 5.3.2-r2 is latest in Alpine edge)
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache gawk curl ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
COPY uncloseai.awk .
|
||||
|
||||
RUN chmod +x uncloseai.awk
|
||||
|
||||
CMD ["./uncloseai.awk"]
|
||||
18
public/languages/awk/README.md
Normal file
18
public/languages/awk/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Awk Implementation
|
||||
|
||||
OpenAI-compatible API client with streaming support.
|
||||
|
||||
## Features
|
||||
|
||||
- Chat completions (streaming & non-streaming)
|
||||
- Text-to-speech generation
|
||||
- Dynamic model discovery via environment variables
|
||||
|
||||
## Source Code
|
||||
|
||||
View the full implementation in the git repository:
|
||||
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/awk
|
||||
|
||||
## Documentation
|
||||
|
||||
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.
|
||||
297
public/languages/awk/uncloseai.awk
Normal file
297
public/languages/awk/uncloseai.awk
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
#!/usr/bin/awk -f
|
||||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
|
||||
# UncloseAI AWK Library - OpenAI-compatible API client with streaming support
|
||||
# Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
#
|
||||
# Library Functions:
|
||||
# uncloseai_init() - Initialize client with model discovery
|
||||
# uncloseai_list_models() - List discovered models
|
||||
# uncloseai_chat(messages, model) - Non-streaming chat completion
|
||||
# uncloseai_chat_stream(messages, model) - Streaming chat completion
|
||||
# uncloseai_tts(text, voice, model) - Text-to-speech generation
|
||||
|
||||
# Global client state
|
||||
# client_models[i,"id|endpoint|max_tokens"] - Discovered models
|
||||
# client_tts_endpoints[i] - TTS endpoints
|
||||
# client_model_count - Number of discovered models
|
||||
# client_tts_count - Number of TTS endpoints
|
||||
|
||||
function uncloseai_init( i, endpoint, cmd, response, model_id, max_tokens) {
|
||||
# Initialize client state
|
||||
client_model_count = 0
|
||||
client_tts_count = 0
|
||||
|
||||
# Discover chat/code models from MODEL_ENDPOINT_N
|
||||
for (i = 1; i <= 9999; i++) {
|
||||
endpoint = ENVIRON["MODEL_ENDPOINT_" i]
|
||||
if (endpoint == "") break
|
||||
|
||||
cmd = "curl -s " endpoint "/models"
|
||||
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line
|
||||
}
|
||||
close(cmd)
|
||||
|
||||
# Parse models from JSON response
|
||||
# Look for "id":"model-name" patterns
|
||||
while (match(response, /"id":"([^"]+)"/, arr)) {
|
||||
model_id = arr[1]
|
||||
|
||||
# Skip modelperm entries (they are permission tokens, not models)
|
||||
if (index(model_id, "modelperm-") == 1) {
|
||||
sub(/"id":"[^"]+"/, "", response)
|
||||
continue
|
||||
}
|
||||
|
||||
# Extract max_model_len if present (vLLM style)
|
||||
if (match(response, /"max_model_len":([0-9]+)/, max_arr)) {
|
||||
max_tokens = max_arr[1]
|
||||
} else {
|
||||
max_tokens = 8192 # Default for Ollama
|
||||
}
|
||||
|
||||
client_model_count++
|
||||
client_models[client_model_count,"id"] = model_id
|
||||
client_models[client_model_count,"endpoint"] = endpoint
|
||||
client_models[client_model_count,"max_tokens"] = max_tokens
|
||||
|
||||
# Remove this model from response to find next one
|
||||
sub(/"id":"[^"]+"/, "", response)
|
||||
}
|
||||
}
|
||||
|
||||
# Discover TTS endpoints from TTS_ENDPOINT_N
|
||||
for (i = 1; i <= 9999; i++) {
|
||||
endpoint = ENVIRON["TTS_ENDPOINT_" i]
|
||||
if (endpoint == "") break
|
||||
|
||||
client_tts_count++
|
||||
client_tts_endpoints[client_tts_count] = endpoint
|
||||
}
|
||||
|
||||
return client_model_count
|
||||
}
|
||||
|
||||
function uncloseai_list_models( i) {
|
||||
# Print all discovered models
|
||||
for (i = 1; i <= client_model_count; i++) {
|
||||
printf " - %s (max_tokens: %d)\n", \
|
||||
client_models[i,"id"], \
|
||||
client_models[i,"max_tokens"]
|
||||
}
|
||||
}
|
||||
|
||||
function uncloseai_get_model_idx(model_id, i) {
|
||||
# Get model index by ID or return 1 for first model
|
||||
if (model_id == "" && client_model_count > 0) {
|
||||
return 1 # Return first model index
|
||||
}
|
||||
|
||||
# Search for specific model
|
||||
for (i = 1; i <= client_model_count; i++) {
|
||||
if (client_models[i,"id"] == model_id) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return 0 # Model not found
|
||||
}
|
||||
|
||||
function uncloseai_chat(messages_json, model_id, max_tokens, temperature, model_idx, cmd, response, content) {
|
||||
# Non-streaming chat completion
|
||||
# messages_json: JSON array string like '[{"role":"user","content":"..."}]'
|
||||
# Returns: content string
|
||||
|
||||
model_idx = uncloseai_get_model_idx(model_id)
|
||||
if (model_idx == 0) {
|
||||
return "ERROR: Model not found"
|
||||
}
|
||||
|
||||
if (max_tokens == "") max_tokens = 100
|
||||
if (temperature == "") temperature = 0.7
|
||||
|
||||
cmd = "curl -s " client_models[model_idx,"endpoint"] "/chat/completions " \
|
||||
"-H 'Content-Type: application/json' " \
|
||||
"-d '{\"model\":\"" client_models[model_idx,"id"] "\"," \
|
||||
"\"messages\":" messages_json "," \
|
||||
"\"max_tokens\":" max_tokens "," \
|
||||
"\"temperature\":" temperature "," \
|
||||
"\"stream\":false}'"
|
||||
|
||||
# Execute curl and capture response
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line
|
||||
}
|
||||
close(cmd)
|
||||
|
||||
# Extract content field from JSON
|
||||
if (match(response, /"content":"([^"\\]*(\\.[^"\\]*)*)"/, arr)) {
|
||||
content = arr[1]
|
||||
# Unescape common JSON escape sequences
|
||||
gsub(/\\n/, "\n", content)
|
||||
gsub(/\\"/, "\"", content)
|
||||
gsub(/\\\\/, "\\", content)
|
||||
return content
|
||||
}
|
||||
|
||||
return "ERROR: No response content"
|
||||
}
|
||||
|
||||
function uncloseai_chat_stream(messages_json, model_id, max_tokens, temperature, model_idx, cmd) {
|
||||
# Streaming chat completion with SSE parsing
|
||||
# Prints content chunks as they arrive
|
||||
# messages_json: JSON array string like '[{"role":"user","content":"..."}]'
|
||||
|
||||
model_idx = uncloseai_get_model_idx(model_id)
|
||||
if (model_idx == 0) {
|
||||
print "ERROR: Model not found"
|
||||
return 0
|
||||
}
|
||||
|
||||
if (max_tokens == "") max_tokens = 500
|
||||
if (temperature == "") temperature = 0.7
|
||||
|
||||
# Use curl with --no-buffer for line-by-line streaming
|
||||
cmd = "curl -s --no-buffer " client_models[model_idx,"endpoint"] "/chat/completions " \
|
||||
"-H 'Content-Type: application/json' " \
|
||||
"-d '{\"model\":\"" client_models[model_idx,"id"] "\"," \
|
||||
"\"messages\":" messages_json "," \
|
||||
"\"max_tokens\":" max_tokens "," \
|
||||
"\"temperature\":" temperature "," \
|
||||
"\"stream\":true}'"
|
||||
|
||||
# Process SSE stream line by line
|
||||
while ((cmd | getline line) > 0) {
|
||||
# SSE format: "data: {...}"
|
||||
if (match(line, /^data: (.+)$/, arr)) {
|
||||
data = arr[1]
|
||||
|
||||
# Check for stream termination
|
||||
if (data == "[DONE]") {
|
||||
break
|
||||
}
|
||||
|
||||
# Extract delta content from streaming chunk
|
||||
# Format: {"choices":[{"delta":{"content":"..."}}]}
|
||||
if (match(data, /"delta":\{[^}]*"content":"([^"\\]*(\\.[^"\\]*)*)"/, content_arr)) {
|
||||
content = content_arr[1]
|
||||
# Unescape JSON sequences
|
||||
gsub(/\\n/, "\n", content)
|
||||
gsub(/\\"/, "\"", content)
|
||||
gsub(/\\\\/, "\\", content)
|
||||
# Print chunk immediately (no newline for streaming effect)
|
||||
printf "%s", content
|
||||
fflush() # Flush output for real-time display
|
||||
}
|
||||
}
|
||||
}
|
||||
close(cmd)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function uncloseai_tts(text, voice, model_name, output_file, endpoint, cmd, size_cmd, file_size) {
|
||||
# Text-to-speech generation
|
||||
# Returns: file size in bytes (0 on error)
|
||||
|
||||
if (client_tts_count == 0) {
|
||||
print "ERROR: No TTS endpoints available"
|
||||
return 0
|
||||
}
|
||||
|
||||
endpoint = client_tts_endpoints[1]
|
||||
|
||||
if (voice == "") voice = "alloy"
|
||||
if (model_name == "") model_name = "tts-1"
|
||||
if (output_file == "") output_file = "/tmp/speech.mp3"
|
||||
|
||||
cmd = "curl -s " endpoint "/audio/speech " \
|
||||
"-H 'Content-Type: application/json' " \
|
||||
"-d '{\"model\":\"" model_name "\"," \
|
||||
"\"voice\":\"" voice "\"," \
|
||||
"\"input\":\"" text "\"}' " \
|
||||
"-o " output_file
|
||||
|
||||
system(cmd)
|
||||
|
||||
# Check file size
|
||||
size_cmd = "stat -f%z " output_file " 2>/dev/null || stat -c%s " output_file " 2>/dev/null"
|
||||
size_cmd | getline file_size
|
||||
close(size_cmd)
|
||||
|
||||
return file_size + 0 # Convert to number
|
||||
}
|
||||
|
||||
# Demo usage when run as script
|
||||
BEGIN {
|
||||
print "=== UncloseAI AWK Client (with Streaming) ===\n"
|
||||
|
||||
# Initialize client with model discovery
|
||||
model_count = uncloseai_init()
|
||||
|
||||
if (model_count == 0) {
|
||||
print "ERROR: No models discovered. Set environment variables:"
|
||||
print " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc."
|
||||
exit 1
|
||||
}
|
||||
|
||||
print "Discovered " model_count " model(s)"
|
||||
uncloseai_list_models()
|
||||
print ""
|
||||
|
||||
# Non-streaming chat example
|
||||
print "=== Non-Streaming Chat ==="
|
||||
messages = "[{\"role\":\"system\",\"content\":\"You are a helpful AI assistant.\"}," \
|
||||
"{\"role\":\"user\",\"content\":\"Explain quantum computing in one sentence.\"}]"
|
||||
|
||||
response = uncloseai_chat(messages, "", 100, 0.7)
|
||||
print "Model: " client_models[1,"id"]
|
||||
print "Response: " response
|
||||
print ""
|
||||
|
||||
# Streaming chat example
|
||||
print "=== Streaming Chat ==="
|
||||
|
||||
# Use second model if available, otherwise first
|
||||
model_id = ""
|
||||
if (client_model_count >= 2) {
|
||||
model_id = client_models[2,"id"]
|
||||
} else {
|
||||
model_id = client_models[1,"id"]
|
||||
}
|
||||
|
||||
print "Model: " model_id
|
||||
print "Response: "
|
||||
|
||||
messages = "[{\"role\":\"system\",\"content\":\"You are a coding assistant.\"}," \
|
||||
"{\"role\":\"user\",\"content\":\"Write a hello world function in AWK.\"}]"
|
||||
|
||||
uncloseai_chat_stream(messages, model_id, 200, 0.7)
|
||||
print "\n"
|
||||
|
||||
# TTS example
|
||||
if (client_tts_count > 0) {
|
||||
print "=== TTS Speech Generation ==="
|
||||
|
||||
text = "Hello from UncloseAI AWK client! This demonstrates text to speech with streaming support."
|
||||
output_file = "/tmp/speech.mp3"
|
||||
|
||||
file_size = uncloseai_tts(text, "alloy", "tts-1", output_file)
|
||||
|
||||
if (file_size > 0) {
|
||||
printf "[OK] Speech file created: %s (%d bytes)\n\n", output_file, file_size
|
||||
} else {
|
||||
print "[ERROR] TTS generation failed\n"
|
||||
}
|
||||
}
|
||||
|
||||
print "=== Examples Complete ==="
|
||||
exit
|
||||
}
|
||||
13
public/languages/bash/Dockerfile
Normal file
13
public/languages/bash/Dockerfile
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache bash curl jq ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
COPY uncloseai.sh .
|
||||
RUN chmod +x uncloseai.sh
|
||||
|
||||
CMD ["./uncloseai.sh"]
|
||||
18
public/languages/bash/README.md
Normal file
18
public/languages/bash/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Bash Implementation
|
||||
|
||||
OpenAI-compatible API client with streaming support.
|
||||
|
||||
## Features
|
||||
|
||||
- Chat completions (streaming & non-streaming)
|
||||
- Text-to-speech generation
|
||||
- Dynamic model discovery via environment variables
|
||||
|
||||
## Source Code
|
||||
|
||||
View the full implementation in the git repository:
|
||||
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/bash
|
||||
|
||||
## Documentation
|
||||
|
||||
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.
|
||||
300
public/languages/bash/uncloseai.sh
Executable file
300
public/languages/bash/uncloseai.sh
Executable file
|
|
@ -0,0 +1,300 @@
|
|||
#!/bin/bash
|
||||
# This is free software for the public good of a permacomputer hosted at
|
||||
# permacomputer.com, an always-on computer by the people, for the people.
|
||||
# One which is durable, easy to repair, & distributed like tap water
|
||||
# for machine learning intelligence.
|
||||
#
|
||||
# The permacomputer is community-owned infrastructure optimized around
|
||||
# four values:
|
||||
#
|
||||
# TRUTH First principles, math & science, open source code freely distributed
|
||||
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
# LOVE Be yourself without hurting others, cooperation through natural law
|
||||
#
|
||||
# This software contributes to that vision by making machine learning
|
||||
# accessible to everyone through a free, open, embeddable chat interface.
|
||||
# Code is seeds to sprout on any abandoned technology.
|
||||
|
||||
# UncloseAI Bash Library - OpenAI-compatible API client with streaming support
|
||||
# Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
#
|
||||
# Library Functions:
|
||||
# uncloseai_init - Initialize client with model discovery
|
||||
# uncloseai_list_models - List discovered models
|
||||
# uncloseai_chat <messages_json> - Non-streaming chat completion
|
||||
# uncloseai_chat_stream <messages_json> - Streaming chat completion
|
||||
# uncloseai_tts <text> <voice> - Text-to-speech generation
|
||||
|
||||
# Global client state
|
||||
declare -a UNCLOSEAI_MODEL_IDS
|
||||
declare -a UNCLOSEAI_MODEL_ENDPOINTS
|
||||
declare -a UNCLOSEAI_MODEL_MAX_TOKENS
|
||||
declare -a UNCLOSEAI_TTS_ENDPOINTS
|
||||
|
||||
# Initialize client and discover models
|
||||
uncloseai_init() {
|
||||
UNCLOSEAI_MODEL_IDS=()
|
||||
UNCLOSEAI_MODEL_ENDPOINTS=()
|
||||
UNCLOSEAI_MODEL_MAX_TOKENS=()
|
||||
UNCLOSEAI_TTS_ENDPOINTS=()
|
||||
|
||||
# Discover chat/code models from MODEL_ENDPOINT_N
|
||||
for i in {1..9999}; do
|
||||
local var_name="MODEL_ENDPOINT_$i"
|
||||
local endpoint="${!var_name}"
|
||||
|
||||
if [ -z "$endpoint" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
local response=$(curl -s "${endpoint}/models")
|
||||
|
||||
# Extract model IDs from JSON, filtering out modelperm entries
|
||||
local model_ids=$(echo "$response" | grep -o '"id":"[^"]*"' | sed 's/"id":"//g' | sed 's/"//g' | grep -v "^modelperm-")
|
||||
|
||||
# Add each discovered model to arrays
|
||||
while IFS= read -r model_id; do
|
||||
if [ -n "$model_id" ]; then
|
||||
# Try to extract max_model_len from vLLM response
|
||||
local max_tokens=$(echo "$response" | grep -o '"max_model_len":[0-9]*' | head -1 | sed 's/"max_model_len"://g')
|
||||
if [ -z "$max_tokens" ]; then
|
||||
max_tokens=8192 # Default for Ollama
|
||||
fi
|
||||
|
||||
UNCLOSEAI_MODEL_IDS+=("$model_id")
|
||||
UNCLOSEAI_MODEL_ENDPOINTS+=("$endpoint")
|
||||
UNCLOSEAI_MODEL_MAX_TOKENS+=("$max_tokens")
|
||||
fi
|
||||
done <<< "$model_ids"
|
||||
done
|
||||
|
||||
# Discover TTS endpoints from TTS_ENDPOINT_N
|
||||
for i in {1..9999}; do
|
||||
local var_name="TTS_ENDPOINT_$i"
|
||||
local endpoint="${!var_name}"
|
||||
|
||||
if [ -z "$endpoint" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
UNCLOSEAI_TTS_ENDPOINTS+=("$endpoint")
|
||||
done
|
||||
|
||||
return ${#UNCLOSEAI_MODEL_IDS[@]}
|
||||
}
|
||||
|
||||
# List all discovered models
|
||||
uncloseai_list_models() {
|
||||
for i in "${!UNCLOSEAI_MODEL_IDS[@]}"; do
|
||||
echo " - ${UNCLOSEAI_MODEL_IDS[$i]} (max_tokens: ${UNCLOSEAI_MODEL_MAX_TOKENS[$i]})"
|
||||
done
|
||||
}
|
||||
|
||||
# Get model index by ID or return 0 for first model
|
||||
uncloseai_get_model_idx() {
|
||||
local model_id="$1"
|
||||
|
||||
if [ -z "$model_id" ]; then
|
||||
echo "0"
|
||||
return
|
||||
fi
|
||||
|
||||
for i in "${!UNCLOSEAI_MODEL_IDS[@]}"; do
|
||||
if [ "${UNCLOSEAI_MODEL_IDS[$i]}" == "$model_id" ]; then
|
||||
echo "$i"
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
echo "-1" # Not found
|
||||
}
|
||||
|
||||
# Non-streaming chat completion
|
||||
# Usage: uncloseai_chat '<messages_json>' [model_id] [max_tokens] [temperature]
|
||||
uncloseai_chat() {
|
||||
local messages_json="$1"
|
||||
local model_id="${2:-}"
|
||||
local max_tokens="${3:-100}"
|
||||
local temperature="${4:-0.7}"
|
||||
|
||||
local model_idx=$(uncloseai_get_model_idx "$model_id")
|
||||
|
||||
if [ "$model_idx" == "-1" ]; then
|
||||
echo "ERROR: Model not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local endpoint="${UNCLOSEAI_MODEL_ENDPOINTS[$model_idx]}"
|
||||
local model="${UNCLOSEAI_MODEL_IDS[$model_idx]}"
|
||||
|
||||
local response=$(curl -s -X POST "${endpoint}/chat/completions" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"model\": \"${model}\",
|
||||
\"messages\": ${messages_json},
|
||||
\"max_tokens\": ${max_tokens},
|
||||
\"temperature\": ${temperature},
|
||||
\"stream\": false
|
||||
}")
|
||||
|
||||
# Extract content using jq if available, otherwise use grep
|
||||
if command -v jq &> /dev/null; then
|
||||
echo "$response" | jq -r '.choices[0].message.content'
|
||||
else
|
||||
echo "$response" | grep -o '"content":"[^"]*"' | head -1 | sed 's/"content":"//g' | sed 's/"$//g'
|
||||
fi
|
||||
}
|
||||
|
||||
# Streaming chat completion with SSE parsing
|
||||
# Usage: uncloseai_chat_stream '<messages_json>' [model_id] [max_tokens] [temperature]
|
||||
uncloseai_chat_stream() {
|
||||
local messages_json="$1"
|
||||
local model_id="${2:-}"
|
||||
local max_tokens="${3:-500}"
|
||||
local temperature="${4:-0.7}"
|
||||
|
||||
local model_idx=$(uncloseai_get_model_idx "$model_id")
|
||||
|
||||
if [ "$model_idx" == "-1" ]; then
|
||||
echo "ERROR: Model not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local endpoint="${UNCLOSEAI_MODEL_ENDPOINTS[$model_idx]}"
|
||||
local model="${UNCLOSEAI_MODEL_IDS[$model_idx]}"
|
||||
|
||||
# Use curl with --no-buffer for line-by-line streaming
|
||||
curl -s --no-buffer -X POST "${endpoint}/chat/completions" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"model\": \"${model}\",
|
||||
\"messages\": ${messages_json},
|
||||
\"max_tokens\": ${max_tokens},
|
||||
\"temperature\": ${temperature},
|
||||
\"stream\": true
|
||||
}" | while IFS= read -r line; do
|
||||
# SSE format: "data: {...}"
|
||||
if [[ "$line" =~ ^data:\ (.+)$ ]]; then
|
||||
local data="${BASH_REMATCH[1]}"
|
||||
|
||||
# Check for stream termination
|
||||
if [ "$data" == "[DONE]" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# Extract delta content from streaming chunk
|
||||
# Use jq if available, otherwise grep
|
||||
if command -v jq &> /dev/null; then
|
||||
local content=$(echo "$data" | jq -r '.choices[0].delta.content // empty' 2>/dev/null)
|
||||
else
|
||||
local content=$(echo "$data" | grep -o '"content":"[^"\\]*\\*[^"]*"' | sed 's/"content":"//g' | sed 's/"$//g' | sed 's/\\n/\n/g' | sed 's/\\"/"/g')
|
||||
fi
|
||||
|
||||
if [ -n "$content" ] && [ "$content" != "null" ]; then
|
||||
printf "%s" "$content"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Text-to-speech generation
|
||||
# Usage: uncloseai_tts <text> [voice] [model] [output_file]
|
||||
uncloseai_tts() {
|
||||
local text="$1"
|
||||
local voice="${2:-alloy}"
|
||||
local model="${3:-tts-1}"
|
||||
local output_file="${4:-/tmp/speech.mp3}"
|
||||
|
||||
if [ ${#UNCLOSEAI_TTS_ENDPOINTS[@]} -eq 0 ]; then
|
||||
echo "ERROR: No TTS endpoints available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local endpoint="${UNCLOSEAI_TTS_ENDPOINTS[0]}"
|
||||
|
||||
curl -s -X POST "${endpoint}/audio/speech" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"model\": \"${model}\",
|
||||
\"voice\": \"${voice}\",
|
||||
\"input\": \"${text}\"
|
||||
}" \
|
||||
--output "$output_file"
|
||||
|
||||
if [ -f "$output_file" ]; then
|
||||
local file_size=$(stat -f%z "$output_file" 2>/dev/null || stat -c%s "$output_file" 2>/dev/null)
|
||||
echo "$file_size"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Demo usage when run as script
|
||||
if [ "${BASH_SOURCE[0]}" == "${0}" ]; then
|
||||
echo "=== UncloseAI Bash Client (with Streaming) ==="
|
||||
echo ""
|
||||
|
||||
# Initialize client with model discovery
|
||||
uncloseai_init
|
||||
model_count=${#UNCLOSEAI_MODEL_IDS[@]}
|
||||
|
||||
if [ "$model_count" -eq 0 ]; then
|
||||
echo "ERROR: No models discovered. Set environment variables:"
|
||||
echo " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Discovered $model_count model(s)"
|
||||
uncloseai_list_models
|
||||
echo ""
|
||||
|
||||
# Non-streaming chat example
|
||||
echo "=== Non-Streaming Chat ==="
|
||||
messages='[{"role":"system","content":"You are a helpful AI assistant."},{"role":"user","content":"Explain quantum computing in one sentence."}]'
|
||||
|
||||
response=$(uncloseai_chat "$messages")
|
||||
echo "Model: ${UNCLOSEAI_MODEL_IDS[0]}"
|
||||
echo "Response: $response"
|
||||
echo ""
|
||||
|
||||
# Streaming chat example
|
||||
echo "=== Streaming Chat ==="
|
||||
|
||||
# Use second model if available, otherwise first
|
||||
if [ "$model_count" -ge 2 ]; then
|
||||
model_id="${UNCLOSEAI_MODEL_IDS[1]}"
|
||||
else
|
||||
model_id="${UNCLOSEAI_MODEL_IDS[0]}"
|
||||
fi
|
||||
|
||||
echo "Model: $model_id"
|
||||
echo "Response: "
|
||||
|
||||
messages='[{"role":"system","content":"You are a coding assistant."},{"role":"user","content":"Write a bash function to check if a port is open"}]'
|
||||
|
||||
uncloseai_chat_stream "$messages" "$model_id" 200
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
# TTS example
|
||||
if [ ${#UNCLOSEAI_TTS_ENDPOINTS[@]} -gt 0 ]; then
|
||||
echo "=== TTS Speech Generation ==="
|
||||
|
||||
text="Hello from UncloseAI Bash client! This demonstrates text to speech with streaming support."
|
||||
output_file="/tmp/speech.mp3"
|
||||
|
||||
file_size=$(uncloseai_tts "$text" "alloy" "tts-1" "$output_file")
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "[OK] Speech file created: $output_file ($file_size bytes)"
|
||||
echo ""
|
||||
else
|
||||
echo "[ERROR] TTS generation failed"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== Examples Complete ==="
|
||||
fi
|
||||
1
public/languages/c#
Symbolic link
1
public/languages/c#
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
csharp
|
||||
18
public/languages/c/README.md
Normal file
18
public/languages/c/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# C Implementation
|
||||
|
||||
OpenAI-compatible API client with streaming support.
|
||||
|
||||
## Features
|
||||
|
||||
- Chat completions (streaming & non-streaming)
|
||||
- Text-to-speech generation
|
||||
- Dynamic model discovery via environment variables
|
||||
|
||||
## Source Code
|
||||
|
||||
View the full implementation in the git repository:
|
||||
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/c
|
||||
|
||||
## Documentation
|
||||
|
||||
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.
|
||||
25
public/languages/c/curl/Dockerfile
Normal file
25
public/languages/c/curl/Dockerfile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# Pin to specific Alpine version (checked 2025-10-12: alpine:3.21 is latest stable)
|
||||
FROM alpine:latest
|
||||
|
||||
# Install C compiler and libcurl development libraries
|
||||
RUN apk --no-cache add \
|
||||
gcc \
|
||||
musl-dev \
|
||||
curl-dev \
|
||||
make \
|
||||
ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY uncloseai.c .
|
||||
COPY Makefile .
|
||||
|
||||
# Compile the application
|
||||
RUN make
|
||||
|
||||
# Run the examples
|
||||
CMD ["./uncloseai"]
|
||||
20
public/languages/c/curl/Makefile
Normal file
20
public/languages/c/curl/Makefile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -O2
|
||||
LDFLAGS = -lcurl
|
||||
|
||||
TARGET = uncloseai
|
||||
SRC = uncloseai.c
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SRC)
|
||||
$(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) speech.mp3
|
||||
|
||||
.PHONY: all clean
|
||||
467
public/languages/c/curl/uncloseai.c
Normal file
467
public/languages/c/curl/uncloseai.c
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
// https://www.permacomputer.com
|
||||
|
||||
/*
|
||||
* UncloseAI C Library using libcurl
|
||||
* OpenAI-compatible API client with streaming support
|
||||
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#define MAX_ENDPOINTS 100
|
||||
#define MAX_MODELS 100
|
||||
#define MAX_URL_LEN 512
|
||||
#define MAX_MODEL_LEN 256
|
||||
#define MAX_CONTENT_LEN 1024
|
||||
|
||||
// Structure to hold response data
|
||||
struct MemoryStruct {
|
||||
char *memory;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
// Structure to hold discovered model info
|
||||
struct ModelInfo {
|
||||
char id[MAX_MODEL_LEN];
|
||||
char endpoint[MAX_URL_LEN];
|
||||
int max_tokens;
|
||||
};
|
||||
|
||||
// UncloseAI Client structure
|
||||
typedef struct {
|
||||
struct ModelInfo *models;
|
||||
int model_count;
|
||||
char tts_endpoints[MAX_ENDPOINTS][MAX_URL_LEN];
|
||||
int tts_count;
|
||||
int timeout;
|
||||
} UncloseAIClient;
|
||||
|
||||
// Callback function type for streaming
|
||||
typedef void (*StreamCallback)(const char *content, void *userdata);
|
||||
|
||||
// Structure for streaming context
|
||||
struct StreamContext {
|
||||
StreamCallback callback;
|
||||
void *userdata;
|
||||
char buffer[8192];
|
||||
size_t buffer_pos;
|
||||
};
|
||||
|
||||
/*************************************************************
|
||||
* LIBRARY API - Callback function to capture response data
|
||||
*************************************************************/
|
||||
static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) {
|
||||
size_t realsize = size * nmemb;
|
||||
struct MemoryStruct *mem = (struct MemoryStruct *)userp;
|
||||
|
||||
char *ptr = realloc(mem->memory, mem->size + realsize + 1);
|
||||
if(!ptr) {
|
||||
printf("Not enough memory (realloc returned NULL)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
mem->memory = ptr;
|
||||
memcpy(&(mem->memory[mem->size]), contents, realsize);
|
||||
mem->size += realsize;
|
||||
mem->memory[mem->size] = 0;
|
||||
|
||||
return realsize;
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* LIBRARY API - Extract content from SSE data chunk
|
||||
*************************************************************/
|
||||
static void extract_sse_content(const char *data, char *content, size_t max_len) {
|
||||
// Look for "content":"..." pattern
|
||||
const char *content_marker = "\"content\":\"";
|
||||
const char *start = strstr(data, content_marker);
|
||||
if(!start) return;
|
||||
|
||||
start += strlen(content_marker);
|
||||
const char *end = start;
|
||||
|
||||
// Find closing quote, handling escaped quotes
|
||||
while(*end && *end != '"') {
|
||||
if(*end == '\\' && *(end+1)) {
|
||||
end += 2;
|
||||
} else {
|
||||
end++;
|
||||
}
|
||||
}
|
||||
|
||||
size_t len = end - start;
|
||||
if(len > max_len - 1) len = max_len - 1;
|
||||
strncpy(content, start, len);
|
||||
content[len] = '\0';
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* LIBRARY API - Streaming callback for curl
|
||||
*************************************************************/
|
||||
static size_t StreamWriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {
|
||||
size_t realsize = size * nmemb;
|
||||
struct StreamContext *ctx = (struct StreamContext *)userp;
|
||||
|
||||
// Append to buffer
|
||||
char *data = (char *)contents;
|
||||
for(size_t i = 0; i < realsize; i++) {
|
||||
if(ctx->buffer_pos >= sizeof(ctx->buffer) - 1) {
|
||||
// Buffer full, skip
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx->buffer[ctx->buffer_pos++] = data[i];
|
||||
|
||||
// Check for line ending
|
||||
if(data[i] == '\n' && ctx->buffer_pos >= 2 &&
|
||||
ctx->buffer[ctx->buffer_pos-2] == '\n') {
|
||||
ctx->buffer[ctx->buffer_pos] = '\0';
|
||||
|
||||
// Process SSE line
|
||||
if(strncmp(ctx->buffer, "data: ", 6) == 0) {
|
||||
const char *json_data = ctx->buffer + 6;
|
||||
|
||||
// Check for [DONE]
|
||||
if(strncmp(json_data, "[DONE]", 6) == 0) {
|
||||
ctx->buffer_pos = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract content
|
||||
char content[MAX_CONTENT_LEN];
|
||||
extract_sse_content(json_data, content, sizeof(content));
|
||||
|
||||
if(strlen(content) > 0 && ctx->callback) {
|
||||
ctx->callback(content, ctx->userdata);
|
||||
}
|
||||
}
|
||||
|
||||
ctx->buffer_pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return realsize;
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* LIBRARY API - Extract model IDs from JSON
|
||||
*************************************************************/
|
||||
static void extract_model_ids(UncloseAIClient *client, const char *json, const char *endpoint) {
|
||||
const char *search = json;
|
||||
const char *id_marker = "\"id\":\"";
|
||||
|
||||
while((search = strstr(search, id_marker)) != NULL &&
|
||||
client->model_count < MAX_MODELS) {
|
||||
search += strlen(id_marker);
|
||||
const char *end = strchr(search, '"');
|
||||
if(end) {
|
||||
size_t len = end - search;
|
||||
if(len < MAX_MODEL_LEN) {
|
||||
// Skip modelperm-* entries
|
||||
if(strncmp(search, "modelperm-", 10) == 0) {
|
||||
search = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
strncpy(client->models[client->model_count].id, search, len);
|
||||
client->models[client->model_count].id[len] = '\0';
|
||||
strncpy(client->models[client->model_count].endpoint, endpoint, MAX_URL_LEN-1);
|
||||
client->models[client->model_count].max_tokens = 8192;
|
||||
client->model_count++;
|
||||
}
|
||||
}
|
||||
search = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* LIBRARY API - Initialize client and discover models
|
||||
*************************************************************/
|
||||
UncloseAIClient* uncloseai_init(int timeout) {
|
||||
UncloseAIClient *client = (UncloseAIClient*)malloc(sizeof(UncloseAIClient));
|
||||
if(!client) return NULL;
|
||||
|
||||
client->models = (struct ModelInfo*)malloc(MAX_MODELS * sizeof(struct ModelInfo));
|
||||
if(!client->models) {
|
||||
free(client);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
client->model_count = 0;
|
||||
client->tts_count = 0;
|
||||
client->timeout = timeout;
|
||||
|
||||
printf("Initializing UncloseAI client...\n");
|
||||
|
||||
// Discover chat/code models
|
||||
for(int i = 1; i < 10000; i++) {
|
||||
char var_name[32];
|
||||
snprintf(var_name, sizeof(var_name), "MODEL_ENDPOINT_%d", i);
|
||||
char *endpoint = getenv(var_name);
|
||||
if(!endpoint) break;
|
||||
|
||||
printf("Endpoint %d: %s\n", i, endpoint);
|
||||
|
||||
// Fetch models
|
||||
char url[MAX_URL_LEN];
|
||||
snprintf(url, sizeof(url), "%s/models", endpoint);
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if(curl) {
|
||||
struct MemoryStruct chunk = {NULL, 0};
|
||||
chunk.memory = malloc(1);
|
||||
chunk.size = 0;
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
if(res == CURLE_OK && chunk.memory) {
|
||||
extract_model_ids(client, chunk.memory, endpoint);
|
||||
}
|
||||
|
||||
free(chunk.memory);
|
||||
curl_easy_cleanup(curl);
|
||||
}
|
||||
}
|
||||
|
||||
// Discover TTS endpoints
|
||||
for(int i = 1; i < 10000; i++) {
|
||||
char var_name[32];
|
||||
snprintf(var_name, sizeof(var_name), "TTS_ENDPOINT_%d", i);
|
||||
char *endpoint = getenv(var_name);
|
||||
if(!endpoint) break;
|
||||
strncpy(client->tts_endpoints[client->tts_count++], endpoint, MAX_URL_LEN-1);
|
||||
}
|
||||
|
||||
printf("Discovered %d models, %d TTS endpoints\n\n", client->model_count, client->tts_count);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* LIBRARY API - Non-streaming chat completion
|
||||
*************************************************************/
|
||||
int uncloseai_chat(UncloseAIClient *client, int model_idx, const char *prompt,
|
||||
struct MemoryStruct *response) {
|
||||
if(model_idx >= client->model_count) return -1;
|
||||
|
||||
char url[MAX_URL_LEN];
|
||||
char json[2048];
|
||||
|
||||
snprintf(url, sizeof(url), "%s/chat/completions",
|
||||
client->models[model_idx].endpoint);
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"model\":\"%s\","
|
||||
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
|
||||
"\"stream\":false,"
|
||||
"\"temperature\":0.7,"
|
||||
"\"max_tokens\":100}",
|
||||
client->models[model_idx].id, prompt);
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if(!curl) return -1;
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)client->timeout);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
return (res == CURLE_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* LIBRARY API - Streaming chat completion
|
||||
*************************************************************/
|
||||
int uncloseai_chat_stream(UncloseAIClient *client, int model_idx, const char *prompt,
|
||||
StreamCallback callback, void *userdata) {
|
||||
if(model_idx >= client->model_count) return -1;
|
||||
|
||||
char url[MAX_URL_LEN];
|
||||
char json[2048];
|
||||
|
||||
snprintf(url, sizeof(url), "%s/chat/completions",
|
||||
client->models[model_idx].endpoint);
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"model\":\"%s\","
|
||||
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
|
||||
"\"stream\":true,"
|
||||
"\"temperature\":0.7,"
|
||||
"\"max_tokens\":500}",
|
||||
client->models[model_idx].id, prompt);
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if(!curl) return -1;
|
||||
|
||||
struct StreamContext ctx;
|
||||
ctx.callback = callback;
|
||||
ctx.userdata = userdata;
|
||||
ctx.buffer_pos = 0;
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, StreamWriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&ctx);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)client->timeout);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
return (res == CURLE_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* LIBRARY API - Text-to-speech generation
|
||||
*************************************************************/
|
||||
int uncloseai_tts(UncloseAIClient *client, const char *text, const char *voice,
|
||||
const char *output_file) {
|
||||
if(client->tts_count == 0) return -1;
|
||||
|
||||
char url[MAX_URL_LEN];
|
||||
char json[2048];
|
||||
|
||||
snprintf(url, sizeof(url), "%s/audio/speech", client->tts_endpoints[0]);
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"model\":\"tts-1\","
|
||||
"\"voice\":\"%s\","
|
||||
"\"input\":\"%s\"}",
|
||||
voice, text);
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if(!curl) return -1;
|
||||
|
||||
struct MemoryStruct chunk = {NULL, 0};
|
||||
chunk.memory = malloc(1);
|
||||
chunk.size = 0;
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)client->timeout);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
int result = -1;
|
||||
if(res == CURLE_OK) {
|
||||
FILE *fp = fopen(output_file, "wb");
|
||||
if(fp) {
|
||||
fwrite(chunk.memory, 1, chunk.size, fp);
|
||||
fclose(fp);
|
||||
result = 0;
|
||||
}
|
||||
}
|
||||
|
||||
free(chunk.memory);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* LIBRARY API - Free client resources
|
||||
*************************************************************/
|
||||
void uncloseai_free(UncloseAIClient *client) {
|
||||
if(client) {
|
||||
if(client->models) free(client->models);
|
||||
free(client);
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* DEMO PROGRAM - Shows library usage
|
||||
*************************************************************/
|
||||
|
||||
// Callback for streaming
|
||||
void stream_callback(const char *content, void *userdata) {
|
||||
printf("%s", content);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== UncloseAI C Client (with Streaming) ===\n\n");
|
||||
|
||||
curl_global_init(CURL_GLOBAL_ALL);
|
||||
|
||||
// Initialize client
|
||||
UncloseAIClient *client = uncloseai_init(30);
|
||||
if(!client || client->model_count == 0) {
|
||||
printf("ERROR: No models discovered\n");
|
||||
curl_global_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Non-streaming chat example
|
||||
printf("=== Non-Streaming Chat ===\n");
|
||||
printf("Model: %s\n", client->models[0].id);
|
||||
|
||||
struct MemoryStruct response = {NULL, 0};
|
||||
response.memory = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
if(uncloseai_chat(client, 0, "Explain quantum computing in one sentence",
|
||||
&response) == 0) {
|
||||
printf("Response: (%zu bytes received)\n", response.size);
|
||||
}
|
||||
free(response.memory);
|
||||
printf("\n");
|
||||
|
||||
// Streaming chat example
|
||||
int model_idx = (client->model_count >= 2) ? 1 : 0;
|
||||
printf("=== Streaming Chat ===\n");
|
||||
printf("Model: %s\n", client->models[model_idx].id);
|
||||
printf("Response: ");
|
||||
|
||||
uncloseai_chat_stream(client, model_idx,
|
||||
"Write a hello world program in C",
|
||||
stream_callback, NULL);
|
||||
printf("\n\n");
|
||||
|
||||
// TTS example
|
||||
if(client->tts_count > 0) {
|
||||
printf("=== TTS Speech Generation ===\n");
|
||||
printf("Model: tts-1\n");
|
||||
|
||||
if(uncloseai_tts(client, "Hello from UncloseAI C client!",
|
||||
"alloy", "/tmp/speech.mp3") == 0) {
|
||||
printf("Audio saved to /tmp/speech.mp3\n");
|
||||
} else {
|
||||
printf("TTS failed\n");
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n=== Examples Complete ===\n");
|
||||
|
||||
uncloseai_free(client);
|
||||
curl_global_cleanup();
|
||||
return 0;
|
||||
}
|
||||
26
public/languages/c/libsoup/Dockerfile
Normal file
26
public/languages/c/libsoup/Dockerfile
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# Pin to specific Alpine version (checked 2025-10-14: alpine:3.20 is latest stable)
|
||||
FROM alpine:latest
|
||||
|
||||
# Install C compiler and libsoup development libraries
|
||||
RUN apk --no-cache add \
|
||||
gcc \
|
||||
musl-dev \
|
||||
libsoup-dev \
|
||||
glib-dev \
|
||||
make \
|
||||
ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY uncloseai.c .
|
||||
COPY Makefile .
|
||||
|
||||
# Compile the application
|
||||
RUN make
|
||||
|
||||
# Run the examples
|
||||
CMD ["./uncloseai"]
|
||||
20
public/languages/c/libsoup/Makefile
Normal file
20
public/languages/c/libsoup/Makefile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra $(shell pkg-config --cflags libsoup-2.4)
|
||||
LDFLAGS = $(shell pkg-config --libs libsoup-2.4)
|
||||
|
||||
TARGET = uncloseai
|
||||
SRC = uncloseai.c
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SRC)
|
||||
$(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET)
|
||||
|
||||
.PHONY: all clean
|
||||
349
public/languages/c/libsoup/uncloseai.c
Normal file
349
public/languages/c/libsoup/uncloseai.c
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
// https://www.permacomputer.com
|
||||
|
||||
/*
|
||||
* UncloseAI C Client using libsoup (GNOME HTTP Library)
|
||||
* OpenAI-compatible API client with streaming support
|
||||
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <libsoup/soup.h>
|
||||
|
||||
#define MAX_ENDPOINTS 100
|
||||
#define MAX_MODELS 100
|
||||
#define MAX_URL_LEN 512
|
||||
#define MAX_MODEL_LEN 256
|
||||
|
||||
// Structure to hold discovered model info
|
||||
struct ModelInfo {
|
||||
char id[MAX_MODEL_LEN];
|
||||
char endpoint[MAX_URL_LEN];
|
||||
int max_tokens;
|
||||
};
|
||||
|
||||
struct ModelInfo models[MAX_MODELS];
|
||||
int model_count = 0;
|
||||
|
||||
char tts_endpoints[MAX_ENDPOINTS][MAX_URL_LEN];
|
||||
int tts_count = 0;
|
||||
|
||||
// Simple JSON string extractor (finds "id":"value" patterns)
|
||||
void extract_model_ids(const char *json, const char *endpoint) {
|
||||
const char *search = json;
|
||||
const char *id_marker = "\"id\":\"";
|
||||
|
||||
while((search = strstr(search, id_marker)) != NULL && model_count < MAX_MODELS) {
|
||||
search += strlen(id_marker);
|
||||
const char *end = strchr(search, '"');
|
||||
if(end) {
|
||||
size_t len = end - search;
|
||||
if(len < MAX_MODEL_LEN) {
|
||||
strncpy(models[model_count].id, search, len);
|
||||
models[model_count].id[len] = '\0';
|
||||
|
||||
// Filter out modelperm-* and chatcmpl-* entries
|
||||
if(strncmp(models[model_count].id, "modelperm-", 10) == 0 ||
|
||||
strncmp(models[model_count].id, "chatcmpl-", 9) == 0) {
|
||||
search = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
strncpy(models[model_count].endpoint, endpoint, MAX_URL_LEN-1);
|
||||
models[model_count].max_tokens = 8192; // Default
|
||||
printf(" - Discovered: %s\n", models[model_count].id);
|
||||
model_count++;
|
||||
}
|
||||
}
|
||||
search = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Discover models from an endpoint
|
||||
void discover_models_from_endpoint(SoupSession *session, const char *endpoint) {
|
||||
char url[MAX_URL_LEN];
|
||||
snprintf(url, sizeof(url), "%s/models", endpoint);
|
||||
|
||||
printf("Discovering models from: %s\n", endpoint);
|
||||
|
||||
SoupMessage *msg = soup_message_new("GET", url);
|
||||
if(!msg) return;
|
||||
|
||||
GError *error = NULL;
|
||||
GInputStream *stream = soup_session_send(session, msg, NULL, &error);
|
||||
|
||||
if(error) {
|
||||
g_error_free(error);
|
||||
g_object_unref(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read response body
|
||||
GString *body = g_string_new(NULL);
|
||||
char buffer[4096];
|
||||
gssize read;
|
||||
|
||||
while((read = g_input_stream_read(stream, buffer, sizeof(buffer)-1, NULL, NULL)) > 0) {
|
||||
buffer[read] = '\0';
|
||||
g_string_append(body, buffer);
|
||||
}
|
||||
|
||||
extract_model_ids(body->str, endpoint);
|
||||
|
||||
g_string_free(body, TRUE);
|
||||
g_object_unref(stream);
|
||||
g_object_unref(msg);
|
||||
}
|
||||
|
||||
// Discover all models from environment variables
|
||||
void discover_all_models(SoupSession *session) {
|
||||
printf("=== Model Discovery ===\n");
|
||||
|
||||
// Discover chat/code models
|
||||
for(int i = 1; i < 10000; i++) {
|
||||
char var_name[32];
|
||||
snprintf(var_name, sizeof(var_name), "MODEL_ENDPOINT_%d", i);
|
||||
char *endpoint = getenv(var_name);
|
||||
if(!endpoint) break;
|
||||
discover_models_from_endpoint(session, endpoint);
|
||||
}
|
||||
|
||||
// Discover TTS endpoints
|
||||
for(int i = 1; i < 10000; i++) {
|
||||
char var_name[32];
|
||||
snprintf(var_name, sizeof(var_name), "TTS_ENDPOINT_%d", i);
|
||||
char *endpoint = getenv(var_name);
|
||||
if(!endpoint) break;
|
||||
printf("Discovering TTS from: %s\n", endpoint);
|
||||
strncpy(tts_endpoints[tts_count++], endpoint, MAX_URL_LEN-1);
|
||||
}
|
||||
|
||||
printf("\nTotal models discovered: %d\n", model_count);
|
||||
printf("Total TTS endpoints: %d\n\n", tts_count);
|
||||
}
|
||||
|
||||
// Make a non-streaming chat request
|
||||
int chat_request(SoupSession *session, int model_idx, const char *prompt) {
|
||||
char url[MAX_URL_LEN];
|
||||
char json[2048];
|
||||
|
||||
snprintf(url, sizeof(url), "%s/chat/completions", models[model_idx].endpoint);
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"model\":\"%s\","
|
||||
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
|
||||
"\"stream\":false,"
|
||||
"\"temperature\":0.7,"
|
||||
"\"max_tokens\":100}",
|
||||
models[model_idx].id, prompt);
|
||||
|
||||
SoupMessage *msg = soup_message_new("POST", url);
|
||||
if(!msg) return -1;
|
||||
|
||||
soup_message_set_request(msg, "application/json", SOUP_MEMORY_COPY, json, strlen(json));
|
||||
|
||||
GError *error = NULL;
|
||||
GInputStream *stream = soup_session_send(session, msg, NULL, &error);
|
||||
|
||||
if(error) {
|
||||
g_error_free(error);
|
||||
g_object_unref(msg);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read response
|
||||
char buffer[4096];
|
||||
gssize read = g_input_stream_read(stream, buffer, sizeof(buffer)-1, NULL, NULL);
|
||||
if(read > 0) {
|
||||
printf("Response received (%ld bytes)\n", read);
|
||||
printf("(Full response requires JSON parsing library)\n");
|
||||
}
|
||||
|
||||
g_object_unref(stream);
|
||||
g_object_unref(msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Streaming context for SSE parsing
|
||||
typedef struct {
|
||||
char buffer[4096];
|
||||
size_t buffer_pos;
|
||||
} StreamContext;
|
||||
|
||||
// Extract content from SSE data line
|
||||
void extract_sse_content(const char *json_data, char *content, size_t content_size) {
|
||||
const char *content_marker = "\"content\":\"";
|
||||
const char *found = strstr(json_data, content_marker);
|
||||
if(found) {
|
||||
found += strlen(content_marker);
|
||||
const char *end = strchr(found, '"');
|
||||
if(end) {
|
||||
size_t len = end - found;
|
||||
if(len < content_size) {
|
||||
strncpy(content, found, len);
|
||||
content[len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process streaming chunk
|
||||
void process_stream_chunk(const char *data, size_t len, StreamContext *ctx) {
|
||||
for(size_t i = 0; i < len; i++) {
|
||||
if(data[i] == '\n') {
|
||||
ctx->buffer[ctx->buffer_pos] = '\0';
|
||||
|
||||
// Process SSE line
|
||||
if(strncmp(ctx->buffer, "data: ", 6) == 0) {
|
||||
const char *json_data = ctx->buffer + 6;
|
||||
if(strcmp(json_data, "[DONE]") == 0) {
|
||||
return; // Stop streaming
|
||||
}
|
||||
|
||||
char content[1024] = {0};
|
||||
extract_sse_content(json_data, content, sizeof(content));
|
||||
if(strlen(content) > 0) {
|
||||
printf("%s", content);
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
ctx->buffer_pos = 0;
|
||||
} else {
|
||||
if(ctx->buffer_pos < sizeof(ctx->buffer) - 1) {
|
||||
ctx->buffer[ctx->buffer_pos++] = data[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Streaming chat request
|
||||
int chat_stream_request(SoupSession *session, int model_idx, const char *prompt) {
|
||||
char url[MAX_URL_LEN];
|
||||
char json[2048];
|
||||
|
||||
snprintf(url, sizeof(url), "%s/chat/completions", models[model_idx].endpoint);
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"model\":\"%s\","
|
||||
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
|
||||
"\"stream\":true,"
|
||||
"\"temperature\":0.7,"
|
||||
"\"max_tokens\":500}",
|
||||
models[model_idx].id, prompt);
|
||||
|
||||
SoupMessage *msg = soup_message_new("POST", url);
|
||||
if(!msg) return -1;
|
||||
|
||||
soup_message_set_request(msg, "application/json", SOUP_MEMORY_COPY, json, strlen(json));
|
||||
|
||||
GError *error = NULL;
|
||||
GInputStream *stream = soup_session_send(session, msg, NULL, &error);
|
||||
|
||||
if(error) {
|
||||
g_error_free(error);
|
||||
g_object_unref(msg);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read and process streaming chunks
|
||||
StreamContext ctx = {{0}, 0};
|
||||
char buffer[1024];
|
||||
gssize read;
|
||||
|
||||
while((read = g_input_stream_read(stream, buffer, sizeof(buffer), NULL, NULL)) > 0) {
|
||||
process_stream_chunk(buffer, read, &ctx);
|
||||
}
|
||||
|
||||
g_object_unref(stream);
|
||||
g_object_unref(msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== UncloseAI C Client (libsoup/GNOME) ===\n\n");
|
||||
|
||||
// Initialize libsoup session
|
||||
SoupSession *session = soup_session_new();
|
||||
|
||||
discover_all_models(session);
|
||||
|
||||
if(model_count == 0) {
|
||||
printf("ERROR: No models discovered\n");
|
||||
g_object_unref(session);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Non-streaming chat example
|
||||
printf("=== Non-Streaming Chat ===\n");
|
||||
printf("Model: %s\n", models[0].id);
|
||||
|
||||
if(chat_request(session, 0, "Explain quantum computing in one sentence") != 0) {
|
||||
printf("Request failed\n");
|
||||
}
|
||||
|
||||
printf("\n");
|
||||
|
||||
// Streaming chat example
|
||||
int model_idx = (model_count >= 2) ? 1 : 0;
|
||||
printf("=== Streaming Chat ===\n");
|
||||
printf("Model: %s\n", models[model_idx].id);
|
||||
printf("Response: ");
|
||||
|
||||
if(chat_stream_request(session, model_idx, "Write a hello world program in C") != 0) {
|
||||
printf("\nStreaming request failed\n");
|
||||
}
|
||||
|
||||
printf("\n\n");
|
||||
|
||||
// TTS example
|
||||
if(tts_count > 0) {
|
||||
printf("=== TTS Speech Generation ===\n");
|
||||
printf("Model: tts-1\n");
|
||||
|
||||
char tts_url[MAX_URL_LEN];
|
||||
snprintf(tts_url, sizeof(tts_url), "%s/audio/speech", tts_endpoints[0]);
|
||||
|
||||
const char *tts_json = "{"
|
||||
"\"model\":\"tts-1\","
|
||||
"\"voice\":\"alloy\","
|
||||
"\"input\":\"Hello from UncloseAI libsoup client!\""
|
||||
"}";
|
||||
|
||||
SoupMessage *tts_msg = soup_message_new("POST", tts_url);
|
||||
if(tts_msg) {
|
||||
soup_message_set_request(tts_msg, "application/json", SOUP_MEMORY_COPY,
|
||||
tts_json, strlen(tts_json));
|
||||
|
||||
GError *error = NULL;
|
||||
GInputStream *stream = soup_session_send(session, tts_msg, NULL, &error);
|
||||
|
||||
if(!error) {
|
||||
FILE *fp = fopen("/tmp/speech.mp3", "wb");
|
||||
if(fp) {
|
||||
char buffer[4096];
|
||||
gssize read;
|
||||
while((read = g_input_stream_read(stream, buffer, sizeof(buffer), NULL, NULL)) > 0) {
|
||||
fwrite(buffer, 1, read, fp);
|
||||
}
|
||||
fclose(fp);
|
||||
printf("Audio saved to /tmp/speech.mp3\n");
|
||||
} else {
|
||||
printf("TTS failed: could not write file\n");
|
||||
}
|
||||
g_object_unref(stream);
|
||||
} else {
|
||||
printf("TTS failed: %s\n", error->message);
|
||||
g_error_free(error);
|
||||
}
|
||||
|
||||
g_object_unref(tts_msg);
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n=== Examples Complete ===\n");
|
||||
|
||||
g_object_unref(session);
|
||||
return 0;
|
||||
}
|
||||
25
public/languages/c/nghttp2/Dockerfile
Normal file
25
public/languages/c/nghttp2/Dockerfile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# Pin to specific Alpine version (checked 2025-10-14: alpine:3.20 is latest stable)
|
||||
FROM alpine:latest
|
||||
|
||||
# Install C compiler and nghttp2 development libraries
|
||||
RUN apk --no-cache add \
|
||||
gcc \
|
||||
musl-dev \
|
||||
nghttp2-dev \
|
||||
make \
|
||||
ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY uncloseai.c .
|
||||
COPY Makefile .
|
||||
|
||||
# Compile the application
|
||||
RUN make
|
||||
|
||||
# Run the examples
|
||||
CMD ["./uncloseai"]
|
||||
20
public/languages/c/nghttp2/Makefile
Normal file
20
public/languages/c/nghttp2/Makefile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -O2
|
||||
LDFLAGS = -lnghttp2
|
||||
|
||||
TARGET = uncloseai
|
||||
SRC = uncloseai.c
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SRC)
|
||||
$(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) speech.mp3
|
||||
|
||||
.PHONY: all clean
|
||||
630
public/languages/c/nghttp2/uncloseai.c
Normal file
630
public/languages/c/nghttp2/uncloseai.c
Normal file
|
|
@ -0,0 +1,630 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
// https://www.permacomputer.com
|
||||
|
||||
/*
|
||||
* UncloseAI C Client using nghttp2 (HTTP/2)
|
||||
* OpenAI-compatible API client with HTTP/2 and streaming support
|
||||
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <netdb.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <nghttp2/nghttp2.h>
|
||||
|
||||
#define MAX_ENDPOINTS 100
|
||||
#define MAX_MODELS 100
|
||||
#define MAX_URL_LEN 512
|
||||
#define MAX_MODEL_LEN 256
|
||||
#define BUFFER_SIZE 8192
|
||||
|
||||
// Structure to hold discovered model info
|
||||
struct ModelInfo {
|
||||
char id[MAX_MODEL_LEN];
|
||||
char endpoint[MAX_URL_LEN];
|
||||
char host[256];
|
||||
int port;
|
||||
int max_tokens;
|
||||
};
|
||||
|
||||
struct ModelInfo models[MAX_MODELS];
|
||||
int model_count = 0;
|
||||
|
||||
char tts_endpoints[MAX_ENDPOINTS][MAX_URL_LEN];
|
||||
int tts_count = 0;
|
||||
|
||||
// HTTP/2 connection context
|
||||
struct Connection {
|
||||
nghttp2_session *session;
|
||||
int sock;
|
||||
char *response_data;
|
||||
size_t response_size;
|
||||
size_t response_capacity;
|
||||
int done;
|
||||
int streaming;
|
||||
char stream_buffer[8192];
|
||||
size_t stream_buffer_pos;
|
||||
};
|
||||
|
||||
// Request data provider
|
||||
struct RequestData {
|
||||
const char *data;
|
||||
size_t length;
|
||||
size_t position;
|
||||
};
|
||||
|
||||
// Parse URL into host, port, path
|
||||
int parse_url(const char *url, char *host, int *port, char *path) {
|
||||
const char *p = url;
|
||||
|
||||
// Skip protocol
|
||||
if (strncmp(p, "http://", 7) == 0) {
|
||||
p += 7;
|
||||
*port = 80;
|
||||
} else if (strncmp(p, "https://", 8) == 0) {
|
||||
p += 8;
|
||||
*port = 443;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Extract host and optional port
|
||||
const char *slash = strchr(p, '/');
|
||||
const char *colon = strchr(p, ':');
|
||||
|
||||
if (colon && (!slash || colon < slash)) {
|
||||
// Port specified
|
||||
size_t host_len = colon - p;
|
||||
strncpy(host, p, host_len);
|
||||
host[host_len] = '\0';
|
||||
*port = atoi(colon + 1);
|
||||
p = slash ? slash : (colon + strlen(colon));
|
||||
} else if (slash) {
|
||||
// No port, use default
|
||||
size_t host_len = slash - p;
|
||||
strncpy(host, p, host_len);
|
||||
host[host_len] = '\0';
|
||||
p = slash;
|
||||
} else {
|
||||
// No path
|
||||
strcpy(host, p);
|
||||
p = "";
|
||||
}
|
||||
|
||||
strcpy(path, p[0] ? p : "/");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Connect to server
|
||||
int connect_to_server(const char *host, int port) {
|
||||
struct addrinfo hints, *res, *rp;
|
||||
int sock = -1;
|
||||
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
char port_str[16];
|
||||
snprintf(port_str, sizeof(port_str), "%d", port);
|
||||
|
||||
if (getaddrinfo(host, port_str, &hints, &res) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (rp = res; rp; rp = rp->ai_next) {
|
||||
sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
if (sock == -1) continue;
|
||||
|
||||
if (connect(sock, rp->ai_addr, rp->ai_addrlen) == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
close(sock);
|
||||
sock = -1;
|
||||
}
|
||||
|
||||
freeaddrinfo(res);
|
||||
return sock;
|
||||
}
|
||||
|
||||
// Send callback for nghttp2
|
||||
ssize_t send_callback(nghttp2_session *session, const uint8_t *data,
|
||||
size_t length, int flags, void *user_data) {
|
||||
struct Connection *conn = (struct Connection *)user_data;
|
||||
ssize_t rv = send(conn->sock, data, length, 0);
|
||||
if (rv < 0) {
|
||||
return NGHTTP2_ERR_CALLBACK_FAILURE;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Receive callback for nghttp2
|
||||
ssize_t recv_callback(nghttp2_session *session, uint8_t *buf, size_t length,
|
||||
int flags, void *user_data) {
|
||||
struct Connection *conn = (struct Connection *)user_data;
|
||||
ssize_t rv = recv(conn->sock, buf, length, 0);
|
||||
if (rv < 0) {
|
||||
return NGHTTP2_ERR_CALLBACK_FAILURE;
|
||||
}
|
||||
if (rv == 0) {
|
||||
return NGHTTP2_ERR_EOF;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Extract content from SSE data chunk
|
||||
void extract_sse_content(const char *data, char *content, size_t max_len) {
|
||||
const char *content_marker = "\"content\":\"";
|
||||
const char *start = strstr(data, content_marker);
|
||||
if (!start) return;
|
||||
|
||||
start += strlen(content_marker);
|
||||
const char *end = start;
|
||||
|
||||
// Find closing quote, handling escaped quotes
|
||||
while (*end && *end != '"') {
|
||||
if (*end == '\\' && *(end + 1)) {
|
||||
end += 2;
|
||||
} else {
|
||||
end++;
|
||||
}
|
||||
}
|
||||
|
||||
size_t len = end - start;
|
||||
if (len > max_len - 1) len = max_len - 1;
|
||||
strncpy(content, start, len);
|
||||
content[len] = '\0';
|
||||
}
|
||||
|
||||
// Data chunk received callback
|
||||
int on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags,
|
||||
int32_t stream_id, const uint8_t *data,
|
||||
size_t len, void *user_data) {
|
||||
struct Connection *conn = (struct Connection *)user_data;
|
||||
|
||||
if (conn->streaming) {
|
||||
// Process streaming SSE data
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (conn->stream_buffer_pos >= sizeof(conn->stream_buffer) - 1) {
|
||||
conn->stream_buffer_pos = 0; // Reset if overflow
|
||||
}
|
||||
|
||||
conn->stream_buffer[conn->stream_buffer_pos++] = data[i];
|
||||
|
||||
// Check for line ending
|
||||
if (data[i] == '\n' && conn->stream_buffer_pos >= 2 &&
|
||||
conn->stream_buffer[conn->stream_buffer_pos - 2] == '\n') {
|
||||
conn->stream_buffer[conn->stream_buffer_pos] = '\0';
|
||||
|
||||
// Process SSE line
|
||||
if (strncmp(conn->stream_buffer, "data: ", 6) == 0) {
|
||||
const char *json_data = conn->stream_buffer + 6;
|
||||
|
||||
// Check for [DONE]
|
||||
if (strncmp(json_data, "[DONE]", 6) == 0) {
|
||||
conn->stream_buffer_pos = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract and print content
|
||||
char content[1024];
|
||||
extract_sse_content(json_data, content, sizeof(content));
|
||||
|
||||
if (strlen(content) > 0) {
|
||||
printf("%s", content);
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
conn->stream_buffer_pos = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-streaming: accumulate response
|
||||
if (conn->response_size + len > conn->response_capacity) {
|
||||
size_t new_capacity = (conn->response_capacity + len) * 2;
|
||||
char *new_data = realloc(conn->response_data, new_capacity);
|
||||
if (!new_data) return 0;
|
||||
conn->response_data = new_data;
|
||||
conn->response_capacity = new_capacity;
|
||||
}
|
||||
|
||||
memcpy(conn->response_data + conn->response_size, data, len);
|
||||
conn->response_size += len;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Stream close callback
|
||||
int on_stream_close_callback(nghttp2_session *session, int32_t stream_id,
|
||||
uint32_t error_code, void *user_data) {
|
||||
struct Connection *conn = (struct Connection *)user_data;
|
||||
conn->done = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Data provider read callback
|
||||
ssize_t data_provider_read_callback(nghttp2_session *session, int32_t stream_id,
|
||||
uint8_t *buf, size_t length,
|
||||
uint32_t *data_flags,
|
||||
nghttp2_data_source *source,
|
||||
void *user_data) {
|
||||
struct RequestData *req_data = (struct RequestData *)source->ptr;
|
||||
|
||||
size_t remaining = req_data->length - req_data->position;
|
||||
size_t to_copy = (remaining < length) ? remaining : length;
|
||||
|
||||
memcpy(buf, req_data->data + req_data->position, to_copy);
|
||||
req_data->position += to_copy;
|
||||
|
||||
if (req_data->position >= req_data->length) {
|
||||
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||||
}
|
||||
|
||||
return to_copy;
|
||||
}
|
||||
|
||||
// Setup HTTP/2 headers
|
||||
void setup_http2_headers(nghttp2_nv *hdrs, const char *method, const char *path, const char *host) {
|
||||
hdrs[0].name = (uint8_t *)":method";
|
||||
hdrs[0].value = (uint8_t *)method;
|
||||
hdrs[0].namelen = strlen(":method");
|
||||
hdrs[0].valuelen = strlen(method);
|
||||
hdrs[0].flags = NGHTTP2_NV_FLAG_NONE;
|
||||
|
||||
hdrs[1].name = (uint8_t *)":path";
|
||||
hdrs[1].value = (uint8_t *)path;
|
||||
hdrs[1].namelen = strlen(":path");
|
||||
hdrs[1].valuelen = strlen(path);
|
||||
hdrs[1].flags = NGHTTP2_NV_FLAG_NONE;
|
||||
|
||||
hdrs[2].name = (uint8_t *)":scheme";
|
||||
hdrs[2].value = (uint8_t *)"http";
|
||||
hdrs[2].namelen = strlen(":scheme");
|
||||
hdrs[2].valuelen = 4;
|
||||
hdrs[2].flags = NGHTTP2_NV_FLAG_NONE;
|
||||
|
||||
hdrs[3].name = (uint8_t *)":authority";
|
||||
hdrs[3].value = (uint8_t *)host;
|
||||
hdrs[3].namelen = strlen(":authority");
|
||||
hdrs[3].valuelen = strlen(host);
|
||||
hdrs[3].flags = NGHTTP2_NV_FLAG_NONE;
|
||||
|
||||
hdrs[4].name = (uint8_t *)"content-type";
|
||||
hdrs[4].value = (uint8_t *)"application/json";
|
||||
hdrs[4].namelen = strlen("content-type");
|
||||
hdrs[4].valuelen = strlen("application/json");
|
||||
hdrs[4].flags = NGHTTP2_NV_FLAG_NONE;
|
||||
}
|
||||
|
||||
// Setup nghttp2 session with callbacks
|
||||
void setup_nghttp2_session(struct Connection *conn, nghttp2_session_callbacks *callbacks) {
|
||||
nghttp2_session_callbacks_new(&callbacks);
|
||||
nghttp2_session_callbacks_set_send_callback(callbacks, send_callback);
|
||||
nghttp2_session_callbacks_set_recv_callback(callbacks, recv_callback);
|
||||
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, on_data_chunk_recv_callback);
|
||||
nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, on_stream_close_callback);
|
||||
|
||||
nghttp2_session_client_new(&conn->session, callbacks, conn);
|
||||
nghttp2_session_callbacks_del(callbacks);
|
||||
|
||||
nghttp2_submit_settings(conn->session, NGHTTP2_FLAG_NONE, NULL, 0);
|
||||
}
|
||||
|
||||
// Make HTTP/2 request
|
||||
int http2_request(const char *host, int port, const char *path,
|
||||
const char *method, const char *body,
|
||||
char **response, size_t *response_size) {
|
||||
int sock = connect_to_server(host, port);
|
||||
if (sock < 0) return -1;
|
||||
|
||||
// Initialize connection
|
||||
struct Connection conn = {0};
|
||||
conn.sock = sock;
|
||||
conn.response_data = malloc(BUFFER_SIZE);
|
||||
conn.response_capacity = BUFFER_SIZE;
|
||||
conn.response_size = 0;
|
||||
conn.done = 0;
|
||||
conn.streaming = 0;
|
||||
conn.stream_buffer_pos = 0;
|
||||
|
||||
// Setup nghttp2 session and callbacks
|
||||
nghttp2_session_callbacks *callbacks;
|
||||
setup_nghttp2_session(&conn, callbacks);
|
||||
|
||||
// Prepare headers
|
||||
nghttp2_nv hdrs[5];
|
||||
setup_http2_headers(hdrs, method, path, host);
|
||||
|
||||
// Submit request
|
||||
if (body && strlen(body) > 0) {
|
||||
struct RequestData req_data = {body, strlen(body), 0};
|
||||
nghttp2_data_provider data_prd;
|
||||
data_prd.source.ptr = &req_data;
|
||||
data_prd.read_callback = data_provider_read_callback;
|
||||
nghttp2_submit_request(conn.session, NULL, hdrs, 5, &data_prd, NULL);
|
||||
} else {
|
||||
nghttp2_submit_request(conn.session, NULL, hdrs, 4, NULL, NULL);
|
||||
}
|
||||
|
||||
// Send and receive
|
||||
while (!conn.done) {
|
||||
if (nghttp2_session_send(conn.session) != 0) break;
|
||||
if (nghttp2_session_recv(conn.session) != 0) break;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
*response = conn.response_data;
|
||||
*response_size = conn.response_size;
|
||||
|
||||
nghttp2_session_del(conn.session);
|
||||
close(sock);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Make streaming HTTP/2 request
|
||||
int http2_stream_request(const char *host, int port, const char *path,
|
||||
const char *method, const char *body) {
|
||||
int sock = connect_to_server(host, port);
|
||||
if (sock < 0) return -1;
|
||||
|
||||
// Initialize connection for streaming
|
||||
struct Connection conn = {0};
|
||||
conn.sock = sock;
|
||||
conn.response_data = malloc(BUFFER_SIZE);
|
||||
conn.response_capacity = BUFFER_SIZE;
|
||||
conn.response_size = 0;
|
||||
conn.done = 0;
|
||||
conn.streaming = 1; // Enable streaming mode
|
||||
conn.stream_buffer_pos = 0;
|
||||
|
||||
// Setup nghttp2 session and callbacks
|
||||
nghttp2_session_callbacks *callbacks;
|
||||
setup_nghttp2_session(&conn, callbacks);
|
||||
|
||||
// Prepare headers
|
||||
nghttp2_nv hdrs[5];
|
||||
setup_http2_headers(hdrs, method, path, host);
|
||||
|
||||
// Submit request
|
||||
if (body && strlen(body) > 0) {
|
||||
struct RequestData req_data = {body, strlen(body), 0};
|
||||
nghttp2_data_provider data_prd;
|
||||
data_prd.source.ptr = &req_data;
|
||||
data_prd.read_callback = data_provider_read_callback;
|
||||
nghttp2_submit_request(conn.session, NULL, hdrs, 5, &data_prd, NULL);
|
||||
} else {
|
||||
nghttp2_submit_request(conn.session, NULL, hdrs, 4, NULL, NULL);
|
||||
}
|
||||
|
||||
// Send and receive (streaming prints directly in callback)
|
||||
while (!conn.done) {
|
||||
if (nghttp2_session_send(conn.session) != 0) break;
|
||||
if (nghttp2_session_recv(conn.session) != 0) break;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
free(conn.response_data);
|
||||
nghttp2_session_del(conn.session);
|
||||
close(sock);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Simple JSON string extractor
|
||||
void extract_model_ids(const char *json, const char *endpoint) {
|
||||
const char *search = json;
|
||||
const char *id_marker = "\"id\":\"";
|
||||
|
||||
while ((search = strstr(search, id_marker)) != NULL && model_count < MAX_MODELS) {
|
||||
search += strlen(id_marker);
|
||||
const char *end = strchr(search, '"');
|
||||
if (end) {
|
||||
size_t len = end - search;
|
||||
if (len < MAX_MODEL_LEN) {
|
||||
strncpy(models[model_count].id, search, len);
|
||||
models[model_count].id[len] = '\0';
|
||||
|
||||
// Filter out modelperm-* entries
|
||||
if (strncmp(models[model_count].id, "modelperm-", 10) == 0 ||
|
||||
strncmp(models[model_count].id, "chatcmpl-", 9) == 0) {
|
||||
search = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
strncpy(models[model_count].endpoint, endpoint, MAX_URL_LEN - 1);
|
||||
|
||||
char host[256];
|
||||
int port;
|
||||
char path[512];
|
||||
if (parse_url(endpoint, host, &port, path) == 0) {
|
||||
strncpy(models[model_count].host, host, sizeof(models[model_count].host) - 1);
|
||||
models[model_count].port = port;
|
||||
}
|
||||
|
||||
models[model_count].max_tokens = 8192;
|
||||
model_count++;
|
||||
}
|
||||
}
|
||||
search = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Discover models from endpoint
|
||||
void discover_models_from_endpoint(const char *endpoint) {
|
||||
printf("Discovering from: %s\n", endpoint);
|
||||
|
||||
char host[256];
|
||||
int port;
|
||||
char base_path[512];
|
||||
|
||||
if (parse_url(endpoint, host, &port, base_path) != 0) return;
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s%s", base_path,
|
||||
base_path[strlen(base_path)-1] == '/' ? "models" : "/models");
|
||||
|
||||
char *response = NULL;
|
||||
size_t response_size = 0;
|
||||
|
||||
if (http2_request(host, port, path, "GET", NULL, &response, &response_size) == 0) {
|
||||
if (response) {
|
||||
extract_model_ids(response, endpoint);
|
||||
free(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discover all models from environment
|
||||
void discover_all_models() {
|
||||
printf("=== Model Discovery ===\n");
|
||||
|
||||
for (int i = 1; i < 10000; i++) {
|
||||
char var_name[32];
|
||||
snprintf(var_name, sizeof(var_name), "MODEL_ENDPOINT_%d", i);
|
||||
char *endpoint = getenv(var_name);
|
||||
if (!endpoint) break;
|
||||
discover_models_from_endpoint(endpoint);
|
||||
}
|
||||
|
||||
for (int i = 1; i < 10000; i++) {
|
||||
char var_name[32];
|
||||
snprintf(var_name, sizeof(var_name), "TTS_ENDPOINT_%d", i);
|
||||
char *endpoint = getenv(var_name);
|
||||
if (!endpoint) break;
|
||||
strncpy(tts_endpoints[tts_count++], endpoint, MAX_URL_LEN - 1);
|
||||
}
|
||||
|
||||
printf("\nDiscovered %d model(s)\n\n", model_count);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== UncloseAI C Client (nghttp2 HTTP/2) ===\n\n");
|
||||
|
||||
discover_all_models();
|
||||
|
||||
if (model_count == 0) {
|
||||
printf("ERROR: No models discovered. Set environment variables:\n");
|
||||
printf(" MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < model_count; i++) {
|
||||
printf(" - %s (max_tokens: %d)\n", models[i].id, models[i].max_tokens);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
// Non-streaming chat (Hermes)
|
||||
printf("=== Non-Streaming Chat (Hermes) ===\n");
|
||||
printf("Model: %s\n", models[0].id);
|
||||
|
||||
char json[2048];
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"model\":\"%s\","
|
||||
"\"messages\":[{\"role\":\"user\",\"content\":\"Give a Python Fizzbuzz solution in one line of code?\"}],"
|
||||
"\"stream\":false,"
|
||||
"\"max_tokens\":150,"
|
||||
"\"temperature\":0.5}",
|
||||
models[0].id);
|
||||
|
||||
char *response = NULL;
|
||||
size_t response_size = 0;
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/v1/chat/completions");
|
||||
|
||||
if (http2_request(models[0].host, models[0].port, path, "POST", json,
|
||||
&response, &response_size) == 0) {
|
||||
printf("Response received (%zu bytes)\n", response_size);
|
||||
printf("(Full response requires JSON parsing library)\n\n");
|
||||
free(response);
|
||||
} else {
|
||||
printf("Request failed\n\n");
|
||||
}
|
||||
|
||||
// Streaming chat (Hermes)
|
||||
printf("=== Streaming Chat (Hermes) ===\n");
|
||||
printf("Model: %s\n", models[0].id);
|
||||
printf("Response: ");
|
||||
|
||||
char stream_json_hermes[2048];
|
||||
snprintf(stream_json_hermes, sizeof(stream_json_hermes),
|
||||
"{\"model\":\"%s\","
|
||||
"\"messages\":[{\"role\":\"user\",\"content\":\"Give a Python Fizzbuzz solution in one line of code?\"}],"
|
||||
"\"stream\":true,"
|
||||
"\"max_tokens\":500,"
|
||||
"\"temperature\":0.7}",
|
||||
models[0].id);
|
||||
|
||||
if (http2_stream_request(models[0].host, models[0].port, path, "POST", stream_json_hermes) != 0) {
|
||||
printf("\nStreaming request failed");
|
||||
}
|
||||
printf("\n\n");
|
||||
|
||||
// Streaming chat (Qwen Coder 3) - if second model available
|
||||
if (model_count >= 2) {
|
||||
printf("=== Streaming Chat (Qwen Coder 3) ===\n");
|
||||
printf("Model: %s\n", models[1].id);
|
||||
printf("Response: ");
|
||||
|
||||
char stream_json_qwen[2048];
|
||||
snprintf(stream_json_qwen, sizeof(stream_json_qwen),
|
||||
"{\"model\":\"%s\","
|
||||
"\"messages\":[{\"role\":\"user\",\"content\":\"Write a hello world program in C\"}],"
|
||||
"\"stream\":true,"
|
||||
"\"max_tokens\":500,"
|
||||
"\"temperature\":0.7}",
|
||||
models[1].id);
|
||||
|
||||
if (http2_stream_request(models[1].host, models[1].port, path, "POST", stream_json_qwen) != 0) {
|
||||
printf("\nStreaming request failed");
|
||||
}
|
||||
printf("\n\n");
|
||||
}
|
||||
|
||||
// TTS
|
||||
if (tts_count > 0) {
|
||||
printf("=== TTS Speech Generation ===\n");
|
||||
printf("Model: tts-1\n");
|
||||
|
||||
char host[256];
|
||||
int port;
|
||||
char base_path[512];
|
||||
if (parse_url(tts_endpoints[0], host, &port, base_path) == 0) {
|
||||
const char *tts_json = "{"
|
||||
"\"model\":\"tts-1\","
|
||||
"\"voice\":\"alloy\","
|
||||
"\"input\":\"Hello from UncloseAI C client with nghttp2!\""
|
||||
"}";
|
||||
|
||||
char *tts_response = NULL;
|
||||
size_t tts_size = 0;
|
||||
|
||||
if (http2_request(host, port, "/audio/speech", "POST", tts_json,
|
||||
&tts_response, &tts_size) == 0) {
|
||||
FILE *fp = fopen("/tmp/speech.mp3", "wb");
|
||||
if (fp) {
|
||||
fwrite(tts_response, 1, tts_size, fp);
|
||||
fclose(fp);
|
||||
printf("Audio saved to /tmp/speech.mp3\n");
|
||||
} else {
|
||||
printf("TTS failed\n");
|
||||
}
|
||||
free(tts_response);
|
||||
} else {
|
||||
printf("TTS failed\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n=== Examples Complete ===\n");
|
||||
return 0;
|
||||
}
|
||||
17
public/languages/clojure/Dockerfile
Normal file
17
public/languages/clojure/Dockerfile
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# Clojure with OpenJDK 21 (checked 2025-10-13: clojure:temurin-21-tools-deps-alpine is latest stable)
|
||||
FROM clojure:temurin-21-tools-deps-alpine
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
COPY deps.edn .
|
||||
COPY uncloseai.clj .
|
||||
|
||||
# Pre-download dependencies
|
||||
RUN clojure -P
|
||||
|
||||
CMD ["clojure", "-M", "-m", "uncloseai"]
|
||||
18
public/languages/clojure/README.md
Normal file
18
public/languages/clojure/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Clojure Implementation
|
||||
|
||||
OpenAI-compatible API client with streaming support.
|
||||
|
||||
## Features
|
||||
|
||||
- Chat completions (streaming & non-streaming)
|
||||
- Text-to-speech generation
|
||||
- Dynamic model discovery via environment variables
|
||||
|
||||
## Source Code
|
||||
|
||||
View the full implementation in the git repository:
|
||||
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/clojure
|
||||
|
||||
## Documentation
|
||||
|
||||
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.
|
||||
7
public/languages/clojure/deps.edn
Normal file
7
public/languages/clojure/deps.edn
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
;; PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
;; Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
;; https://www.permacomputer.com
|
||||
|
||||
{:deps {clj-http/clj-http {:mvn/version "3.13.0"}
|
||||
cheshire/cheshire {:mvn/version "5.13.0"}}
|
||||
:paths ["."]}
|
||||
232
public/languages/clojure/uncloseai.clj
Normal file
232
public/languages/clojure/uncloseai.clj
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
;; PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
;; Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
;; https://www.permacomputer.com
|
||||
|
||||
(ns uncloseai
|
||||
"UncloseAI Clojure Library - OpenAI-compatible API client with streaming support
|
||||
Compatible with vLLM, Ollama, and OpenAI-compatible endpoints"
|
||||
(:require [clj-http.client :as client]
|
||||
[cheshire.core :as json]
|
||||
[clojure.string :as str]))
|
||||
|
||||
;; Client record for managing models and endpoints
|
||||
(defrecord UncloseAIClient [models tts-endpoints])
|
||||
|
||||
(defn filter-modelperm
|
||||
"Filter out modelperm entries from model list"
|
||||
[models]
|
||||
(remove #(str/starts-with? (:id %) "modelperm-") models))
|
||||
|
||||
(defn discover-models-from-endpoint
|
||||
"Discover models from a single endpoint"
|
||||
[endpoint]
|
||||
(try
|
||||
(let [response (client/get (str endpoint "/models") {:as :json})
|
||||
body (:body response)
|
||||
model-list (:data body)]
|
||||
(->> model-list
|
||||
(map (fn [model]
|
||||
{:id (:id model)
|
||||
:endpoint endpoint
|
||||
:max-tokens (or (:max_model_len model) 8192)}))
|
||||
(filter-modelperm)))
|
||||
(catch Exception e
|
||||
(println "Warning: Failed to discover models from" endpoint ":" (.getMessage e))
|
||||
[])))
|
||||
|
||||
(defn discover-tts-endpoints
|
||||
"Discover TTS endpoints from environment variables"
|
||||
[]
|
||||
(loop [i 1
|
||||
endpoints []]
|
||||
(if-let [endpoint (System/getenv (str "TTS_ENDPOINT_" i))]
|
||||
(recur (inc i) (conj endpoints endpoint))
|
||||
endpoints)))
|
||||
|
||||
(defn init-client
|
||||
"Initialize UncloseAI client with model discovery from environment variables"
|
||||
([]
|
||||
(init-client nil nil))
|
||||
([model-endpoints tts-endpoints]
|
||||
(let [model-eps (or model-endpoints
|
||||
(loop [i 1 eps []]
|
||||
(if-let [ep (System/getenv (str "MODEL_ENDPOINT_" i))]
|
||||
(recur (inc i) (conj eps ep))
|
||||
eps)))
|
||||
tts-eps (or tts-endpoints (discover-tts-endpoints))
|
||||
discovered-models (mapcat discover-models-from-endpoint model-eps)]
|
||||
(->UncloseAIClient discovered-models tts-eps))))
|
||||
|
||||
(defn list-models
|
||||
"List all discovered models"
|
||||
[client]
|
||||
(:models client))
|
||||
|
||||
(defn get-model
|
||||
"Get model by ID or return first model if ID is nil"
|
||||
[client model-id]
|
||||
(if model-id
|
||||
(first (filter #(= (:id %) model-id) (:models client)))
|
||||
(first (:models client))))
|
||||
|
||||
(defn chat
|
||||
"Non-streaming chat completion
|
||||
|
||||
Args:
|
||||
client: UncloseAI client instance
|
||||
messages: Vector of message maps with :role and :content
|
||||
options: Map with optional :model-id, :max-tokens, :temperature"
|
||||
([client messages]
|
||||
(chat client messages {}))
|
||||
([client messages {:keys [model-id max-tokens temperature]
|
||||
:or {max-tokens 100 temperature 0.7}}]
|
||||
(let [model (get-model client model-id)]
|
||||
(if-not model
|
||||
(throw (ex-info "Model not found" {:model-id model-id}))
|
||||
(let [url (str (:endpoint model) "/chat/completions")
|
||||
payload {:model (:id model)
|
||||
:messages messages
|
||||
:max_tokens max-tokens
|
||||
:temperature temperature
|
||||
:stream false}
|
||||
response (client/post url
|
||||
{:content-type :json
|
||||
:body (json/generate-string payload)
|
||||
:as :json})
|
||||
body (:body response)]
|
||||
{:model (:id model)
|
||||
:content (get-in body [:choices 0 :message :content])
|
||||
:response body})))))
|
||||
|
||||
(defn parse-sse-line
|
||||
"Parse a single SSE line and extract content"
|
||||
[line]
|
||||
(when (str/starts-with? line "data: ")
|
||||
(let [data (subs line 6)]
|
||||
(when-not (= data "[DONE]")
|
||||
(try
|
||||
(let [parsed (json/parse-string data true)]
|
||||
(get-in parsed [:choices 0 :delta :content]))
|
||||
(catch Exception e
|
||||
nil))))))
|
||||
|
||||
(defn chat-stream
|
||||
"Streaming chat completion using Server-Sent Events
|
||||
|
||||
Returns a lazy sequence of content chunks
|
||||
|
||||
Args:
|
||||
client: UncloseAI client instance
|
||||
messages: Vector of message maps with :role and :content
|
||||
options: Map with optional :model-id, :max-tokens, :temperature"
|
||||
([client messages]
|
||||
(chat-stream client messages {}))
|
||||
([client messages {:keys [model-id max-tokens temperature]
|
||||
:or {max-tokens 500 temperature 0.7}}]
|
||||
(let [model (get-model client model-id)]
|
||||
(if-not model
|
||||
(throw (ex-info "Model not found" {:model-id model-id}))
|
||||
(let [url (str (:endpoint model) "/chat/completions")
|
||||
payload {:model (:id model)
|
||||
:messages messages
|
||||
:max_tokens max-tokens
|
||||
:temperature temperature
|
||||
:stream true}
|
||||
response (client/post url
|
||||
{:content-type :json
|
||||
:body (json/generate-string payload)
|
||||
:as :stream})
|
||||
stream (:body response)]
|
||||
(->> (line-seq (clojure.java.io/reader stream))
|
||||
(keep parse-sse-line)
|
||||
(remove nil?)))))))
|
||||
|
||||
(defn tts
|
||||
"Text-to-speech generation
|
||||
|
||||
Args:
|
||||
client: UncloseAI client instance
|
||||
text: Input text to convert to speech
|
||||
options: Map with optional :voice, :model, :output-file"
|
||||
([client text]
|
||||
(tts client text {}))
|
||||
([client text {:keys [voice model output-file]
|
||||
:or {voice "alloy" model "tts-1" output-file "/tmp/speech.mp3"}}]
|
||||
(if (empty? (:tts-endpoints client))
|
||||
(throw (ex-info "No TTS endpoints available" {}))
|
||||
(let [endpoint (first (:tts-endpoints client))
|
||||
url (str endpoint "/audio/speech")
|
||||
payload {:model model
|
||||
:voice voice
|
||||
:input text}
|
||||
response (client/post url
|
||||
{:content-type :json
|
||||
:body (json/generate-string payload)
|
||||
:as :byte-array})
|
||||
audio-data (:body response)]
|
||||
(with-open [out (clojure.java.io/output-stream output-file)]
|
||||
(.write out audio-data))
|
||||
{:file output-file
|
||||
:size (count audio-data)}))))
|
||||
|
||||
;; Demo usage when run as script
|
||||
(defn -main [& args]
|
||||
(println "=== UncloseAI Clojure Client (with Streaming) ===")
|
||||
(println)
|
||||
|
||||
;; Initialize client with auto-discovery
|
||||
(let [client (init-client)]
|
||||
|
||||
(when (empty? (:models client))
|
||||
(println "ERROR: No models discovered. Set environment variables:")
|
||||
(println " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.")
|
||||
(System/exit 1))
|
||||
|
||||
(println (str "Discovered " (count (:models client)) " model(s)"))
|
||||
(doseq [model (:models client)]
|
||||
(println (str " - " (:id model) " (max_tokens: " (:max-tokens model) ")")))
|
||||
(println)
|
||||
|
||||
;; Non-streaming chat example
|
||||
(println "=== Non-Streaming Chat ===")
|
||||
(try
|
||||
(let [result (chat client
|
||||
[{:role "system" :content "You are a helpful AI assistant."}
|
||||
{:role "user" :content "Explain quantum computing in one sentence."}])]
|
||||
(println "Model:" (:model result))
|
||||
(println "Response:" (:content result)))
|
||||
(catch Exception e
|
||||
(println "Error:" (.getMessage e))))
|
||||
(println)
|
||||
|
||||
;; Streaming chat example
|
||||
(println "=== Streaming Chat ===")
|
||||
(let [model-id (if (>= (count (:models client)) 2)
|
||||
(:id (nth (:models client) 1))
|
||||
nil)]
|
||||
(println "Model:" (or model-id (:id (first (:models client)))))
|
||||
(print "Response: ")
|
||||
(flush)
|
||||
(try
|
||||
(doseq [chunk (chat-stream client
|
||||
[{:role "system" :content "You are a coding assistant."}
|
||||
{:role "user" :content "Write a hello world function in Clojure."}]
|
||||
{:model-id model-id :max-tokens 200})]
|
||||
(print chunk)
|
||||
(flush))
|
||||
(println)
|
||||
(catch Exception e
|
||||
(println "\nError:" (.getMessage e)))))
|
||||
(println)
|
||||
|
||||
;; TTS example
|
||||
(when-not (empty? (:tts-endpoints client))
|
||||
(println "=== TTS Speech Generation ===")
|
||||
(try
|
||||
(let [result (tts client "Hello from UncloseAI Clojure client! This demonstrates text to speech with streaming support.")]
|
||||
(println (str "[OK] Speech file created: " (:file result) " (" (:size result) " bytes)")))
|
||||
(catch Exception e
|
||||
(println "Error:" (.getMessage e))))
|
||||
(println))
|
||||
|
||||
(println "=== Examples Complete ===")))
|
||||
27
public/languages/cobol/Dockerfile
Normal file
27
public/languages/cobol/Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# GnuCOBOL 3.x (checked 2025-10-13: hldtux/cobol-gnu is available)
|
||||
FROM debian:bookworm-slim AS builder
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y gnucobol4 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY uncloseai.cob .
|
||||
|
||||
# Compile COBOL program
|
||||
RUN cobc -x -free uncloseai.cob -o uncloseai
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y gnucobol4 libcob4 ca-certificates curl jq bash && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/uncloseai .
|
||||
|
||||
CMD ["./uncloseai"]
|
||||
18
public/languages/cobol/README.md
Normal file
18
public/languages/cobol/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Cobol Implementation
|
||||
|
||||
OpenAI-compatible API client with streaming support.
|
||||
|
||||
## Features
|
||||
|
||||
- Chat completions (streaming & non-streaming)
|
||||
- Text-to-speech generation
|
||||
- Dynamic model discovery via environment variables
|
||||
|
||||
## Source Code
|
||||
|
||||
View the full implementation in the git repository:
|
||||
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/cobol
|
||||
|
||||
## Documentation
|
||||
|
||||
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.
|
||||
55
public/languages/cobol/discover.sh
Normal file
55
public/languages/cobol/discover.sh
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#!/bin/sh
|
||||
# This is free software for the public good of a permacomputer hosted at
|
||||
# permacomputer.com, an always-on computer by the people, for the people.
|
||||
# One which is durable, easy to repair, & distributed like tap water
|
||||
# for machine learning intelligence.
|
||||
#
|
||||
# The permacomputer is community-owned infrastructure optimized around
|
||||
# four values:
|
||||
#
|
||||
# TRUTH First principles, math & science, open source code freely distributed
|
||||
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
# LOVE Be yourself without hurting others, cooperation through natural law
|
||||
#
|
||||
# This software contributes to that vision by making machine learning
|
||||
# accessible to everyone through a free, open, embeddable chat interface.
|
||||
# Code is seeds to sprout on any abandoned technology.
|
||||
|
||||
# Model discovery for COBOL - outputs discovered models to files
|
||||
|
||||
# Discover models from MODEL_ENDPOINT_N
|
||||
echo "=== Model Discovery ===" > /tmp/models.txt
|
||||
model_count=0
|
||||
i=1
|
||||
while [ $i -le 9999 ]; do
|
||||
eval endpoint=\$MODEL_ENDPOINT_$i
|
||||
[ -z "$endpoint" ] && break
|
||||
i=$((i + 1))
|
||||
|
||||
echo "Discovering from: $endpoint" >> /tmp/models.txt
|
||||
response=$(curl -s "${endpoint}/models")
|
||||
|
||||
# Extract first model ID
|
||||
model_id=$(echo "$response" | grep -o '"id":"[^"]*"' | head -1 | sed 's/"id":"//g' | sed 's/"//g')
|
||||
|
||||
if [ -n "$model_id" ]; then
|
||||
echo "$model_id" >> /tmp/model_$model_count.txt
|
||||
echo "$endpoint" >> /tmp/endpoint_$model_count.txt
|
||||
echo " - Discovered: $model_id" >> /tmp/models.txt
|
||||
model_count=$((model_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Discover TTS
|
||||
i=1
|
||||
while [ $i -le 9999 ]; do
|
||||
eval endpoint=\$TTS_ENDPOINT_$i
|
||||
[ -z "$endpoint" ] && break
|
||||
echo "$endpoint" > /tmp/tts_endpoint.txt
|
||||
echo "Discovering TTS from: $endpoint" >> /tmp/models.txt
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
echo "Total models: $model_count" >> /tmp/models.txt
|
||||
cat /tmp/models.txt
|
||||
34
public/languages/cobol/hermes.sh
Normal file
34
public/languages/cobol/hermes.sh
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#!/bin/sh
|
||||
# This is free software for the public good of a permacomputer hosted at
|
||||
# permacomputer.com, an always-on computer by the people, for the people.
|
||||
# One which is durable, easy to repair, & distributed like tap water
|
||||
# for machine learning intelligence.
|
||||
#
|
||||
# The permacomputer is community-owned infrastructure optimized around
|
||||
# four values:
|
||||
#
|
||||
# TRUTH First principles, math & science, open source code freely distributed
|
||||
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
# LOVE Be yourself without hurting others, cooperation through natural law
|
||||
#
|
||||
# This software contributes to that vision by making machine learning
|
||||
# accessible to everyone through a free, open, embeddable chat interface.
|
||||
# Code is seeds to sprout on any abandoned technology.
|
||||
|
||||
# Use first discovered model
|
||||
if [ ! -f /tmp/model_0.txt ] || [ ! -f /tmp/endpoint_0.txt ]; then
|
||||
echo "ERROR: No models discovered"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
model=$(cat /tmp/model_0.txt)
|
||||
endpoint=$(cat /tmp/endpoint_0.txt)
|
||||
|
||||
echo "Using model: $model"
|
||||
echo "Endpoint: $endpoint"
|
||||
|
||||
curl -s "${endpoint}/chat/completions" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"model\":\"$model\",\"messages\":[{\"role\":\"system\",\"content\":\"You are Hermes, a helpful AI assistant from Nous Research.\"},{\"role\":\"user\",\"content\":\"Explain quantum computing in one sentence.\"}],\"max_tokens\":100}" \
|
||||
2>/dev/null | grep -o '"content":"[^"]*"' | head -1 | cut -d'"' -f4
|
||||
37
public/languages/cobol/qwen.sh
Normal file
37
public/languages/cobol/qwen.sh
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#!/bin/sh
|
||||
# This is free software for the public good of a permacomputer hosted at
|
||||
# permacomputer.com, an always-on computer by the people, for the people.
|
||||
# One which is durable, easy to repair, & distributed like tap water
|
||||
# for machine learning intelligence.
|
||||
#
|
||||
# The permacomputer is community-owned infrastructure optimized around
|
||||
# four values:
|
||||
#
|
||||
# TRUTH First principles, math & science, open source code freely distributed
|
||||
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
# LOVE Be yourself without hurting others, cooperation through natural law
|
||||
#
|
||||
# This software contributes to that vision by making machine learning
|
||||
# accessible to everyone through a free, open, embeddable chat interface.
|
||||
# Code is seeds to sprout on any abandoned technology.
|
||||
|
||||
# Use second discovered model (or first if only one)
|
||||
if [ -f /tmp/model_1.txt ] && [ -f /tmp/endpoint_1.txt ]; then
|
||||
model=$(cat /tmp/model_1.txt)
|
||||
endpoint=$(cat /tmp/endpoint_1.txt)
|
||||
elif [ -f /tmp/model_0.txt ] && [ -f /tmp/endpoint_0.txt ]; then
|
||||
model=$(cat /tmp/model_0.txt)
|
||||
endpoint=$(cat /tmp/endpoint_0.txt)
|
||||
else
|
||||
echo "ERROR: No models discovered"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Using model: $model"
|
||||
echo "Endpoint: $endpoint"
|
||||
|
||||
curl -s "${endpoint}/chat/completions" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"model\":\"$model\",\"messages\":[{\"role\":\"system\",\"content\":\"You are Qwen, a coding assistant specialized in software development.\"},{\"role\":\"user\",\"content\":\"Write a hello world function in COBOL.\"}],\"max_tokens\":200}" \
|
||||
2>/dev/null | grep -o '"content":"[^"]*"' | head -1 | cut -d'"' -f4
|
||||
32
public/languages/cobol/tts.sh
Normal file
32
public/languages/cobol/tts.sh
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/sh
|
||||
# This is free software for the public good of a permacomputer hosted at
|
||||
# permacomputer.com, an always-on computer by the people, for the people.
|
||||
# One which is durable, easy to repair, & distributed like tap water
|
||||
# for machine learning intelligence.
|
||||
#
|
||||
# The permacomputer is community-owned infrastructure optimized around
|
||||
# four values:
|
||||
#
|
||||
# TRUTH First principles, math & science, open source code freely distributed
|
||||
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||
# LOVE Be yourself without hurting others, cooperation through natural law
|
||||
#
|
||||
# This software contributes to that vision by making machine learning
|
||||
# accessible to everyone through a free, open, embeddable chat interface.
|
||||
# Code is seeds to sprout on any abandoned technology.
|
||||
|
||||
# Use first TTS endpoint
|
||||
if [ ! -f /tmp/tts_endpoint.txt ]; then
|
||||
echo "ERROR: No TTS endpoints discovered"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
endpoint=$(cat /tmp/tts_endpoint.txt)
|
||||
|
||||
echo "Using TTS endpoint: $endpoint"
|
||||
|
||||
curl -s "${endpoint}/audio/speech" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"model":"tts-1","voice":"alloy","input":"Hello from COBOL! This is a text to speech example."}' \
|
||||
-o /app/output.mp3 2>/dev/null
|
||||
87
public/languages/cobol/uncloseai.cob
Normal file
87
public/languages/cobol/uncloseai.cob
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
*> PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
*> Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
*> https://www.permacomputer.com
|
||||
|
||||
IDENTIFICATION DIVISION.
|
||||
PROGRAM-ID. UNCLOSEAI.
|
||||
|
||||
DATA DIVISION.
|
||||
WORKING-STORAGE SECTION.
|
||||
01 MESSAGE-TEXT PIC X(200).
|
||||
01 MODEL-INDEX PIC 9.
|
||||
01 TTS-TEXT PIC X(200).
|
||||
01 TTS-VOICE PIC X(20).
|
||||
01 TEMP-CMD PIC X(500).
|
||||
|
||||
PROCEDURE DIVISION.
|
||||
|
||||
UNCLOSEAI-INIT.
|
||||
DISPLAY "Initializing UncloseAI client...".
|
||||
CALL "SYSTEM" USING
|
||||
"for i in {1..9999}; do ep=$MODEL_ENDPOINT_$i; "
|
||||
"[ -z $ep ] && break; echo Endpoint $i: $ep; done".
|
||||
|
||||
UNCLOSEAI-CHAT.
|
||||
DISPLAY "Model: first available".
|
||||
STRING "ep=$MODEL_ENDPOINT_1; "
|
||||
"curl -s $ep/chat/completions "
|
||||
"-H Content-Type:application/json "
|
||||
"-d {model:qwen,messages:[{role:user,content:"
|
||||
MESSAGE-TEXT "}],stream:false}"
|
||||
DELIMITED BY SIZE
|
||||
INTO TEMP-CMD
|
||||
END-STRING
|
||||
CALL "SYSTEM" USING TEMP-CMD.
|
||||
|
||||
UNCLOSEAI-CHAT-STREAM.
|
||||
DISPLAY "Model: streaming".
|
||||
STRING "ep=$MODEL_ENDPOINT_1; "
|
||||
"curl -s --no-buffer $ep/chat/completions "
|
||||
"-H Content-Type:application/json "
|
||||
"-d {model:qwen,messages:[{role:user,content:"
|
||||
MESSAGE-TEXT "}],stream:true}"
|
||||
DELIMITED BY SIZE
|
||||
INTO TEMP-CMD
|
||||
END-STRING
|
||||
CALL "SYSTEM" USING TEMP-CMD.
|
||||
|
||||
UNCLOSEAI-TTS.
|
||||
DISPLAY "Generating speech...".
|
||||
STRING "curl -s $TTS_ENDPOINT_1/audio/speech "
|
||||
"-H Content-Type:application/json "
|
||||
"-d {model:tts-1,input:"
|
||||
TTS-TEXT ",voice:" TTS-VOICE "} "
|
||||
"-o /tmp/speech.mp3"
|
||||
DELIMITED BY SIZE
|
||||
INTO TEMP-CMD
|
||||
END-STRING
|
||||
CALL "SYSTEM" USING TEMP-CMD
|
||||
DISPLAY "Audio saved to /tmp/speech.mp3".
|
||||
|
||||
DEMO-MAIN.
|
||||
DISPLAY "=== UncloseAI COBOL Client ===".
|
||||
DISPLAY " ".
|
||||
|
||||
PERFORM UNCLOSEAI-INIT.
|
||||
DISPLAY " ".
|
||||
|
||||
DISPLAY "=== Non-Streaming Chat ===".
|
||||
MOVE "Explain quantum computing" TO MESSAGE-TEXT.
|
||||
MOVE 0 TO MODEL-INDEX.
|
||||
PERFORM UNCLOSEAI-CHAT.
|
||||
DISPLAY " ".
|
||||
|
||||
DISPLAY "=== Streaming Chat ===".
|
||||
MOVE "Write hello world in COBOL" TO MESSAGE-TEXT.
|
||||
MOVE 1 TO MODEL-INDEX.
|
||||
PERFORM UNCLOSEAI-CHAT-STREAM.
|
||||
DISPLAY " ".
|
||||
|
||||
DISPLAY "=== TTS Speech Generation ===".
|
||||
MOVE "Hello from COBOL" TO TTS-TEXT.
|
||||
MOVE "alloy" TO TTS-VOICE.
|
||||
PERFORM UNCLOSEAI-TTS.
|
||||
DISPLAY " ".
|
||||
|
||||
DISPLAY "=== Examples Complete ===".
|
||||
STOP RUN.
|
||||
25
public/languages/commonlisp/Dockerfile
Normal file
25
public/languages/commonlisp/Dockerfile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# Pin to specific SBCL version (checked 2025-10-15: clfoundation/sbcl:latest is stable)
|
||||
FROM clfoundation/sbcl:latest
|
||||
|
||||
# Install Quicklisp (Common Lisp package manager)
|
||||
RUN curl -O https://beta.quicklisp.org/quicklisp.lisp && \
|
||||
sbcl --load quicklisp.lisp --eval '(quicklisp-quickstart:install)' --quit && \
|
||||
echo '(load "~/quicklisp/setup.lisp")' >> ~/.sbclrc
|
||||
|
||||
# Install Dexador HTTP client and JSON parser
|
||||
RUN sbcl --eval '(ql:quickload :dexador)' \
|
||||
--eval '(ql:quickload :jonathan)' \
|
||||
--quit
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy application files
|
||||
COPY uncloseai.lisp .
|
||||
|
||||
# Default command runs the demo
|
||||
CMD ["sbcl", "--script", "uncloseai.lisp"]
|
||||
18
public/languages/commonlisp/README.md
Normal file
18
public/languages/commonlisp/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Commonlisp Implementation
|
||||
|
||||
OpenAI-compatible API client with streaming support.
|
||||
|
||||
## Features
|
||||
|
||||
- Chat completions (streaming & non-streaming)
|
||||
- Text-to-speech generation
|
||||
- Dynamic model discovery via environment variables
|
||||
|
||||
## Source Code
|
||||
|
||||
View the full implementation in the git repository:
|
||||
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/commonlisp
|
||||
|
||||
## Documentation
|
||||
|
||||
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.
|
||||
311
public/languages/commonlisp/uncloseai.lisp
Normal file
311
public/languages/commonlisp/uncloseai.lisp
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
#!/usr/bin/env sbcl --script
|
||||
;; PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
;; Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
;; https://www.permacomputer.com
|
||||
|
||||
|
||||
;;; uncloseai - Common Lisp client library for OpenAI-compatible APIs
|
||||
;;; Supports streaming and non-streaming chat, model discovery, and TTS
|
||||
;;; Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
;;;
|
||||
;;; Uses Dexador HTTP client and Jonathan JSON library
|
||||
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:quickload '(:dexador :jonathan) :silent t)
|
||||
|
||||
;;; Data structures
|
||||
|
||||
(defclass uncloseai ()
|
||||
((models :initform nil :accessor models
|
||||
:documentation "List of discovered models with metadata")
|
||||
(tts-endpoints :initform nil :accessor tts-endpoints
|
||||
:documentation "List of TTS endpoint URLs")
|
||||
(api-key :initform nil :initarg :api-key :accessor api-key
|
||||
:documentation "Optional API key for authentication")
|
||||
(request-timeout :initform 30 :initarg :request-timeout :accessor request-timeout
|
||||
:documentation "Request timeout in seconds")))
|
||||
|
||||
(defun make-model-info (id endpoint max-tokens)
|
||||
"Create a model info plist"
|
||||
(list :id id :endpoint endpoint :max-tokens max-tokens))
|
||||
|
||||
;;; Environment discovery
|
||||
|
||||
(defun discover-env-endpoints (prefix)
|
||||
"Discover endpoints from environment variables like PREFIX_1, PREFIX_2, ..."
|
||||
(loop for i from 1 to 9999
|
||||
for var-name = (format nil "~A_~D" prefix i)
|
||||
for endpoint = (uiop:getenv var-name)
|
||||
while endpoint
|
||||
collect endpoint))
|
||||
|
||||
;;; Model discovery
|
||||
|
||||
(defun discover-models-from-endpoint (client endpoint)
|
||||
"Discover available models from an endpoint"
|
||||
(handler-case
|
||||
(let* ((url (concatenate 'string endpoint "/models"))
|
||||
(headers (when (api-key client)
|
||||
(list (cons "Authorization"
|
||||
(format nil "Bearer ~A" (api-key client))))))
|
||||
(response (dex:get url :headers headers))
|
||||
(parsed (jonathan:parse response))
|
||||
(data (getf parsed :|data|))
|
||||
;; Coerce to list to handle both vector (vLLM) and list (Ollama) responses
|
||||
(models-list (coerce data 'list)))
|
||||
(dolist (model models-list)
|
||||
(let ((model-id (getf model :|id|))
|
||||
(max-tokens (or (getf model :|max_model_len|) 8192)))
|
||||
(push (make-model-info model-id endpoint max-tokens)
|
||||
(models client)))))
|
||||
(error (e)
|
||||
(format t "Warning: Failed to discover models from ~A: ~A~%" endpoint e))))
|
||||
|
||||
(defun initialize-client (client model-endpoints tts-endpoints)
|
||||
"Initialize client with endpoint discovery and model detection"
|
||||
;; Discover endpoints from environment if not provided
|
||||
(let ((model-eps (or model-endpoints (discover-env-endpoints "MODEL_ENDPOINT")))
|
||||
(tts-eps (or tts-endpoints (discover-env-endpoints "TTS_ENDPOINT"))))
|
||||
|
||||
;; Discover models from each endpoint
|
||||
(dolist (endpoint model-eps)
|
||||
(discover-models-from-endpoint client endpoint))
|
||||
|
||||
;; Reverse models list (they were pushed in reverse order)
|
||||
(setf (models client) (nreverse (models client)))
|
||||
|
||||
;; Store TTS endpoints
|
||||
(setf (tts-endpoints client) tts-eps))
|
||||
|
||||
client)
|
||||
|
||||
(defun make-uncloseai (&key model-endpoints tts-endpoints api-key (request-timeout 30))
|
||||
"Create a new uncloseai client with auto-discovery from environment variables"
|
||||
(let ((client (make-instance 'uncloseai
|
||||
:api-key api-key
|
||||
:request-timeout request-timeout)))
|
||||
(initialize-client client model-endpoints tts-endpoints)))
|
||||
|
||||
;;; Helper functions
|
||||
|
||||
(defun get-model-info (client model-id)
|
||||
"Get model info by ID or return first available model"
|
||||
(when (null (models client))
|
||||
(error "No models available. Check endpoint configuration."))
|
||||
|
||||
(if (null model-id)
|
||||
(first (models client))
|
||||
(or (find model-id (models client) :key (lambda (m) (getf m :id)) :test #'string=)
|
||||
(error "Model '~A' not found in discovered models" model-id))))
|
||||
|
||||
(defun make-headers (client &optional (content-type t))
|
||||
"Create HTTP headers with optional authorization"
|
||||
(let ((headers nil))
|
||||
(when content-type
|
||||
(push (cons "Content-Type" "application/json") headers))
|
||||
(when (api-key client)
|
||||
(push (cons "Authorization"
|
||||
(format nil "Bearer ~A" (api-key client)))
|
||||
headers))
|
||||
headers))
|
||||
|
||||
;;; Non-streaming chat completion
|
||||
|
||||
(defun chat (client messages &key model (max-tokens 100) (temperature 0.7))
|
||||
"Non-streaming chat completion
|
||||
|
||||
Args:
|
||||
messages - List of message plists with :role and :content
|
||||
model - Model ID (defaults to first available model)
|
||||
max-tokens - Maximum tokens in response
|
||||
temperature - Sampling temperature
|
||||
|
||||
Returns:
|
||||
Response plist with :choices containing the completion"
|
||||
(let* ((model-info (get-model-info client model))
|
||||
(endpoint (getf model-info :endpoint))
|
||||
(model-id (getf model-info :id))
|
||||
(url (concatenate 'string endpoint "/chat/completions"))
|
||||
(payload (jonathan:to-json
|
||||
(list :|model| model-id
|
||||
:|messages| (coerce messages 'vector)
|
||||
:|max_tokens| max-tokens
|
||||
:|temperature| temperature
|
||||
:|stream| :false)))
|
||||
(headers (make-headers client))
|
||||
(response (dex:post url
|
||||
:headers headers
|
||||
:content payload)))
|
||||
(jonathan:parse response)))
|
||||
|
||||
;;; Streaming chat completion
|
||||
|
||||
(defun process-sse-line (line)
|
||||
"Process a single Server-Sent Event line, return parsed content or nil"
|
||||
(when (and line (> (length line) 6) (string= (subseq line 0 6) "data: "))
|
||||
(let ((data (subseq line 6)))
|
||||
(unless (string= data "[DONE]")
|
||||
(handler-case
|
||||
(let* ((parsed (jonathan:parse data))
|
||||
;; Coerce choices to list to handle both vector and list responses
|
||||
(choices (coerce (getf parsed :|choices|) 'list))
|
||||
(delta (when choices
|
||||
(getf (first choices) :|delta|)))
|
||||
(content (when delta (getf delta :|content|))))
|
||||
content)
|
||||
(error (e) nil))))))
|
||||
|
||||
(defun chat-stream (client messages callback &key model (max-tokens 500) (temperature 0.7))
|
||||
"Streaming chat completion using Server-Sent Events
|
||||
|
||||
Args:
|
||||
messages - List of message plists with :role and :content
|
||||
callback - Function to call for each content chunk (receives string)
|
||||
model - Model ID (defaults to first available model)
|
||||
max-tokens - Maximum tokens in response
|
||||
temperature - Sampling temperature"
|
||||
(let* ((model-info (get-model-info client model))
|
||||
(endpoint (getf model-info :endpoint))
|
||||
(model-id (getf model-info :id))
|
||||
(url (concatenate 'string endpoint "/chat/completions"))
|
||||
(payload (jonathan:to-json
|
||||
(list :|model| model-id
|
||||
:|messages| (coerce messages 'vector)
|
||||
:|max_tokens| max-tokens
|
||||
:|temperature| temperature
|
||||
:|stream| t)))
|
||||
(headers (make-headers client))
|
||||
(stream (dex:request url
|
||||
:method :post
|
||||
:headers headers
|
||||
:content payload
|
||||
:want-stream t)))
|
||||
(unwind-protect
|
||||
(loop for line = (read-line stream nil nil)
|
||||
while line
|
||||
do (let ((content (process-sse-line line)))
|
||||
(when content
|
||||
(funcall callback content))))
|
||||
(close stream))))
|
||||
|
||||
;;; Text-to-Speech
|
||||
|
||||
(defun tts (client text &key (voice "alloy") (model "tts-1") (response-format "mp3"))
|
||||
"Generate speech from text
|
||||
|
||||
Args:
|
||||
text - Input text to convert to speech
|
||||
voice - Voice name (alloy, echo, fable, onyx, nova, shimmer)
|
||||
model - TTS model (tts-1 or tts-1-hd)
|
||||
response-format - Audio format (mp3, opus, aac, flac)
|
||||
|
||||
Returns:
|
||||
Audio data as byte array"
|
||||
(when (null (tts-endpoints client))
|
||||
(error "No TTS endpoints available"))
|
||||
|
||||
(let* ((endpoint (first (tts-endpoints client)))
|
||||
(url (concatenate 'string endpoint "/audio/speech"))
|
||||
(payload (jonathan:to-json
|
||||
(list :|model| model
|
||||
:|voice| voice
|
||||
:|input| text
|
||||
:|response_format| response-format)))
|
||||
(headers (make-headers client))
|
||||
(response (dex:post url
|
||||
:headers headers
|
||||
:content payload
|
||||
:force-binary t)))
|
||||
response))
|
||||
|
||||
;;; Demo usage
|
||||
|
||||
(defun demo-nonstreaming (client)
|
||||
"Demonstrate non-streaming chat"
|
||||
(format t "=== Non-Streaming Chat ===~%")
|
||||
(let* ((response (chat client
|
||||
(list (list :|role| "system"
|
||||
:|content| "You are a helpful AI assistant.")
|
||||
(list :|role| "user"
|
||||
:|content| "Explain quantum computing in one sentence."))
|
||||
:max-tokens 100
|
||||
:temperature 0.7))
|
||||
(model-used (getf response :|model|))
|
||||
;; Coerce choices to list to handle both vector and list responses
|
||||
(choices (coerce (getf response :|choices|) 'list))
|
||||
(choice (first choices))
|
||||
(message (getf choice :|message|))
|
||||
(content (getf message :|content|)))
|
||||
(format t "Model: ~A~%" model-used)
|
||||
(format t "Response: ~A~%~%" content)))
|
||||
|
||||
(defun demo-streaming (client)
|
||||
"Demonstrate streaming chat"
|
||||
(format t "=== Streaming Chat ===~%")
|
||||
(let ((model-id (if (> (length (models client)) 1)
|
||||
(getf (second (models client)) :id)
|
||||
nil)))
|
||||
(format t "Model: ~A~%" (or model-id (getf (first (models client)) :id)))
|
||||
(format t "Response: ")
|
||||
(force-output)
|
||||
|
||||
(chat-stream client
|
||||
(list (list :|role| "system"
|
||||
:|content| "You are a coding assistant.")
|
||||
(list :|role| "user"
|
||||
:|content| "Write a Common Lisp function to check if a number is prime"))
|
||||
(lambda (content)
|
||||
(format t "~A" content)
|
||||
(force-output))
|
||||
:model model-id
|
||||
:max-tokens 200
|
||||
:temperature 0.7)
|
||||
|
||||
(format t "~%~%")))
|
||||
|
||||
(defun demo-tts (client)
|
||||
"Demonstrate text-to-speech"
|
||||
(when (tts-endpoints client)
|
||||
(format t "=== TTS Speech Generation ===~%")
|
||||
(let ((audio-data (tts client
|
||||
"Hello from uncloseai Common Lisp client! This demonstrates text to speech with streaming support."
|
||||
:voice "alloy")))
|
||||
(with-open-file (out "speech.mp3"
|
||||
:direction :output
|
||||
:if-exists :supersede
|
||||
:if-does-not-exist :create
|
||||
:element-type '(unsigned-byte 8))
|
||||
(write-sequence audio-data out))
|
||||
(format t "[OK] Speech file created: speech.mp3 (~D bytes)~%~%" (length audio-data)))))
|
||||
|
||||
(defun main ()
|
||||
"Main demo function"
|
||||
(format t "=== uncloseai Common Lisp Client (with Streaming) ===~%~%")
|
||||
|
||||
;; Initialize client (auto-discovers from environment)
|
||||
(let ((client (make-uncloseai)))
|
||||
|
||||
(when (null (models client))
|
||||
(format t "ERROR: No models discovered. Set environment variables:~%")
|
||||
(format t " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.~%")
|
||||
(uiop:quit 1))
|
||||
|
||||
(format t "Discovered ~D model(s)~%" (length (models client)))
|
||||
(dolist (model (models client))
|
||||
(format t " - ~A (max_tokens: ~D)~%"
|
||||
(getf model :id)
|
||||
(getf model :max-tokens)))
|
||||
(format t "~%")
|
||||
|
||||
;; Run demos
|
||||
(handler-case
|
||||
(progn
|
||||
(demo-nonstreaming client)
|
||||
(demo-streaming client)
|
||||
(demo-tts client)
|
||||
(format t "=== Examples Complete ===~%"))
|
||||
(error (e)
|
||||
(format t "~%Error: ~A~%" e)))))
|
||||
|
||||
;; Main execution
|
||||
(main)
|
||||
18
public/languages/cpp/README.md
Normal file
18
public/languages/cpp/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Cpp Implementation
|
||||
|
||||
OpenAI-compatible API client with streaming support.
|
||||
|
||||
## Features
|
||||
|
||||
- Chat completions (streaming & non-streaming)
|
||||
- Text-to-speech generation
|
||||
- Dynamic model discovery via environment variables
|
||||
|
||||
## Source Code
|
||||
|
||||
View the full implementation in the git repository:
|
||||
https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/public/languages/cpp
|
||||
|
||||
## Documentation
|
||||
|
||||
📚 See the **uncloseai. Machine Learning Reference Guide** for complete documentation.
|
||||
25
public/languages/cpp/boost-beast/Dockerfile
Normal file
25
public/languages/cpp/boost-beast/Dockerfile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# Pin to specific Alpine version (checked 2025-10-14: alpine:3.20 is latest stable)
|
||||
FROM alpine:latest
|
||||
|
||||
# Install C++ compiler and Boost libraries
|
||||
RUN apk --no-cache add \
|
||||
g++ \
|
||||
make \
|
||||
boost1.84-dev \
|
||||
openssl-dev \
|
||||
ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY uncloseai.cpp .
|
||||
COPY Makefile .
|
||||
|
||||
# Compile the application
|
||||
RUN make
|
||||
|
||||
# Run the examples
|
||||
CMD ["./uncloseai"]
|
||||
20
public/languages/cpp/boost-beast/Makefile
Normal file
20
public/languages/cpp/boost-beast/Makefile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
CXX = g++
|
||||
CXXFLAGS = -std=c++17 -Wall -Wextra -O2
|
||||
LDFLAGS = -lssl -lcrypto -lpthread
|
||||
|
||||
TARGET = uncloseai
|
||||
SRC = uncloseai.cpp
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SRC)
|
||||
$(CXX) $(CXXFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) speech.mp3
|
||||
|
||||
.PHONY: all clean
|
||||
271
public/languages/cpp/boost-beast/uncloseai.cpp
Normal file
271
public/languages/cpp/boost-beast/uncloseai.cpp
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
// https://www.permacomputer.com
|
||||
|
||||
/*
|
||||
* UncloseAI C++ Library using Boost.Beast
|
||||
* OpenAI-compatible API client with HTTP/HTTPS support
|
||||
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
*/
|
||||
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
#include <boost/asio/connect.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl/error.hpp>
|
||||
#include <boost/asio/ssl/stream.hpp>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace http = beast::http;
|
||||
namespace net = boost::asio;
|
||||
namespace ssl = net::ssl;
|
||||
using tcp = net::ip::tcp;
|
||||
|
||||
struct ModelInfo {
|
||||
std::string id;
|
||||
std::string host;
|
||||
std::string port;
|
||||
std::string base_path;
|
||||
int max_tokens;
|
||||
};
|
||||
|
||||
class UncloseAI {
|
||||
private:
|
||||
std::vector<ModelInfo> models;
|
||||
std::vector<std::pair<std::string, std::string>> tts_endpoints; // host, port
|
||||
int timeout;
|
||||
|
||||
void parse_url(const std::string& url, std::string& host, std::string& port, std::string& path) {
|
||||
// Parse https://host:port/path
|
||||
size_t proto_end = url.find("://");
|
||||
if (proto_end == std::string::npos) return;
|
||||
|
||||
std::string rest = url.substr(proto_end + 3);
|
||||
size_t slash = rest.find("/");
|
||||
|
||||
std::string host_port = (slash != std::string::npos) ? rest.substr(0, slash) : rest;
|
||||
path = (slash != std::string::npos) ? rest.substr(slash) : "/v1";
|
||||
|
||||
size_t colon = host_port.find(":");
|
||||
if (colon != std::string::npos) {
|
||||
host = host_port.substr(0, colon);
|
||||
port = host_port.substr(colon + 1);
|
||||
} else {
|
||||
host = host_port;
|
||||
port = (url.find("https://") == 0) ? "443" : "80";
|
||||
}
|
||||
}
|
||||
|
||||
std::string http_get(const std::string& host, const std::string& port, const std::string& target) {
|
||||
try {
|
||||
net::io_context ioc;
|
||||
tcp::resolver resolver(ioc);
|
||||
beast::tcp_stream stream(ioc);
|
||||
|
||||
auto const results = resolver.resolve(host, port);
|
||||
stream.connect(results);
|
||||
|
||||
http::request<http::string_body> req{http::verb::get, target, 11};
|
||||
req.set(http::field::host, host);
|
||||
req.set(http::field::user_agent, "UncloseAI-Beast");
|
||||
|
||||
http::write(stream, req);
|
||||
|
||||
beast::flat_buffer buffer;
|
||||
http::response<http::string_body> res;
|
||||
http::read(stream, buffer, res);
|
||||
|
||||
beast::error_code ec;
|
||||
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
|
||||
|
||||
return res.body();
|
||||
} catch (...) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
std::string http_post(const std::string& host, const std::string& port,
|
||||
const std::string& target, const std::string& body) {
|
||||
try {
|
||||
net::io_context ioc;
|
||||
tcp::resolver resolver(ioc);
|
||||
beast::tcp_stream stream(ioc);
|
||||
|
||||
auto const results = resolver.resolve(host, port);
|
||||
stream.connect(results);
|
||||
|
||||
http::request<http::string_body> req{http::verb::post, target, 11};
|
||||
req.set(http::field::host, host);
|
||||
req.set(http::field::user_agent, "UncloseAI-Beast");
|
||||
req.set(http::field::content_type, "application/json");
|
||||
req.body() = body;
|
||||
req.prepare_payload();
|
||||
|
||||
http::write(stream, req);
|
||||
|
||||
beast::flat_buffer buffer;
|
||||
http::response<http::string_body> res;
|
||||
http::read(stream, buffer, res);
|
||||
|
||||
beast::error_code ec;
|
||||
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
|
||||
|
||||
return res.body();
|
||||
} catch (...) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void discover_models_from_endpoint(const std::string& endpoint) {
|
||||
std::string host, port, base_path;
|
||||
parse_url(endpoint, host, port, base_path);
|
||||
|
||||
std::string target = base_path + (base_path.back() == '/' ? "models" : "/models");
|
||||
std::string response = http_get(host, port, target);
|
||||
|
||||
if (!response.empty()) {
|
||||
// Simple JSON parsing for model IDs
|
||||
size_t pos = 0;
|
||||
while ((pos = response.find("\"id\":\"", pos)) != std::string::npos) {
|
||||
pos += 6;
|
||||
size_t end = response.find("\"", pos);
|
||||
if (end != std::string::npos) {
|
||||
std::string model_id = response.substr(pos, end - pos);
|
||||
|
||||
// Filter out modelperm-* and chatcmpl-*
|
||||
if (model_id.substr(0, 10) != "modelperm-" &&
|
||||
model_id.substr(0, 9) != "chatcmpl-") {
|
||||
ModelInfo info;
|
||||
info.id = model_id;
|
||||
info.host = host;
|
||||
info.port = port;
|
||||
info.base_path = base_path;
|
||||
info.max_tokens = 8192;
|
||||
models.push_back(info);
|
||||
}
|
||||
}
|
||||
pos = end + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
UncloseAI(int timeout_sec = 30) : timeout(timeout_sec) {
|
||||
std::vector<std::string> endpoints, tts_eps;
|
||||
|
||||
// Discover endpoints from environment
|
||||
for (int i = 1; i < 10000; i++) {
|
||||
std::string var = "MODEL_ENDPOINT_" + std::to_string(i);
|
||||
const char* ep = std::getenv(var.c_str());
|
||||
if (!ep) break;
|
||||
endpoints.push_back(ep);
|
||||
}
|
||||
|
||||
for (int i = 1; i < 10000; i++) {
|
||||
std::string var = "TTS_ENDPOINT_" + std::to_string(i);
|
||||
const char* ep = std::getenv(var.c_str());
|
||||
if (!ep) break;
|
||||
tts_eps.push_back(ep);
|
||||
}
|
||||
|
||||
for (const auto& ep : endpoints) {
|
||||
std::cout << "Discovering from: " << ep << std::endl;
|
||||
discover_models_from_endpoint(ep);
|
||||
}
|
||||
|
||||
for (const auto& ep : tts_eps) {
|
||||
std::string host, port, path;
|
||||
parse_url(ep, host, port, path);
|
||||
tts_endpoints.push_back({host, port});
|
||||
}
|
||||
|
||||
std::cout << "\nDiscovered " << models.size() << " model(s)\n" << std::endl;
|
||||
}
|
||||
|
||||
const std::vector<ModelInfo>& list_models() const { return models; }
|
||||
|
||||
int chat(const std::string& prompt, std::string& response, int model_idx = 0, int max_tokens = 100) {
|
||||
if (model_idx >= static_cast<int>(models.size())) return -1;
|
||||
|
||||
const ModelInfo& model = models[model_idx];
|
||||
std::string target = model.base_path + (model.base_path.back() == '/' ? "chat/completions" : "/chat/completions");
|
||||
|
||||
std::ostringstream json;
|
||||
json << "{\"model\":\"" << model.id << "\","
|
||||
<< "\"messages\":[{\"role\":\"user\",\"content\":\"" << prompt << "\"}],"
|
||||
<< "\"stream\":false,"
|
||||
<< "\"max_tokens\":" << max_tokens << ","
|
||||
<< "\"temperature\":0.7}";
|
||||
|
||||
response = http_post(model.host, model.port, target, json.str());
|
||||
return response.empty() ? -1 : 0;
|
||||
}
|
||||
|
||||
int tts(const std::string& text, const std::string& voice, const std::string& output_file) {
|
||||
if (tts_endpoints.empty()) return -1;
|
||||
|
||||
auto [host, port] = tts_endpoints[0];
|
||||
|
||||
std::ostringstream json;
|
||||
json << "{\"model\":\"tts-1\","
|
||||
<< "\"voice\":\"" << voice << "\","
|
||||
<< "\"input\":\"" << text << "\"}";
|
||||
|
||||
std::string response = http_post(host, port, "/audio/speech", json.str());
|
||||
|
||||
if (!response.empty()) {
|
||||
std::ofstream file(output_file, std::ios::binary);
|
||||
if (file.is_open()) {
|
||||
file.write(response.c_str(), response.size());
|
||||
file.close();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
int main() {
|
||||
std::cout << "=== UncloseAI C++ Client (Boost.Beast) ===\n\n";
|
||||
|
||||
UncloseAI client(30);
|
||||
|
||||
if (client.list_models().empty()) {
|
||||
std::cout << "ERROR: No models discovered. Set environment variables:\n";
|
||||
std::cout << " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto models = client.list_models();
|
||||
for (const auto& m : models) {
|
||||
std::cout << " - " << m.id << " (max_tokens: " << m.max_tokens << ")\n";
|
||||
}
|
||||
std::cout << "\n";
|
||||
|
||||
// Non-streaming chat
|
||||
std::cout << "=== Non-Streaming Chat ===\n";
|
||||
std::string response;
|
||||
if (client.chat("Explain quantum computing in one sentence", response) == 0) {
|
||||
std::cout << "Response received (" << response.size() << " bytes)\n\n";
|
||||
} else {
|
||||
std::cout << "Request failed\n\n";
|
||||
}
|
||||
|
||||
// TTS
|
||||
std::cout << "=== TTS Speech Generation ===\n";
|
||||
if (client.tts("Hello from Boost.Beast!", "alloy", "/tmp/speech.mp3") == 0) {
|
||||
std::cout << "Audio saved to /tmp/speech.mp3\n";
|
||||
} else {
|
||||
std::cout << "TTS failed\n";
|
||||
}
|
||||
|
||||
std::cout << "\n=== Examples Complete ===\n";
|
||||
return 0;
|
||||
}
|
||||
28
public/languages/cpp/cpp-httplib/Dockerfile
Normal file
28
public/languages/cpp/cpp-httplib/Dockerfile
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# Pin to specific Alpine version (checked 2025-10-14: alpine:3.20 is latest stable)
|
||||
FROM alpine:latest
|
||||
|
||||
# Install C++ compiler and build tools
|
||||
RUN apk --no-cache add \
|
||||
g++ \
|
||||
make \
|
||||
wget \
|
||||
ca-certificates \
|
||||
openssl-dev
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Download cpp-httplib header-only library (v0.18.3 latest as of 2025-10-14)
|
||||
RUN wget -O httplib.h https://raw.githubusercontent.com/yhirose/cpp-httplib/v0.18.3/httplib.h
|
||||
|
||||
COPY uncloseai.cpp .
|
||||
COPY Makefile .
|
||||
|
||||
# Compile the application
|
||||
RUN make
|
||||
|
||||
# Run the examples
|
||||
CMD ["./uncloseai"]
|
||||
20
public/languages/cpp/cpp-httplib/Makefile
Normal file
20
public/languages/cpp/cpp-httplib/Makefile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
CXX = g++
|
||||
CXXFLAGS = -std=c++17 -Wall -Wextra -O2 -DCPPHTTPLIB_OPENSSL_SUPPORT
|
||||
LDFLAGS = -lssl -lcrypto -lpthread
|
||||
|
||||
TARGET = uncloseai
|
||||
SRC = uncloseai.cpp
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SRC) httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) speech.mp3
|
||||
|
||||
.PHONY: all clean
|
||||
272
public/languages/cpp/cpp-httplib/uncloseai.cpp
Normal file
272
public/languages/cpp/cpp-httplib/uncloseai.cpp
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
// https://www.permacomputer.com
|
||||
|
||||
/*
|
||||
* UncloseAI C++ Library using cpp-httplib (header-only)
|
||||
* OpenAI-compatible API client with streaming support
|
||||
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include "httplib.h"
|
||||
|
||||
struct ModelInfo {
|
||||
std::string id;
|
||||
std::string endpoint;
|
||||
std::string host;
|
||||
int port;
|
||||
int max_tokens;
|
||||
};
|
||||
|
||||
class UncloseAI {
|
||||
private:
|
||||
std::vector<ModelInfo> models;
|
||||
std::vector<std::pair<std::string, int>> tts_endpoints; // host, port
|
||||
int timeout;
|
||||
bool debug;
|
||||
|
||||
// Parse URL into host and port
|
||||
bool parse_url(const std::string& url, std::string& host, int& port, std::string& base_path) {
|
||||
// Simple URL parsing for https://host:port/path
|
||||
size_t proto_end = url.find("://");
|
||||
if (proto_end == std::string::npos) return false;
|
||||
|
||||
std::string rest = url.substr(proto_end + 3);
|
||||
size_t slash_pos = rest.find("/");
|
||||
|
||||
std::string host_port;
|
||||
if (slash_pos != std::string::npos) {
|
||||
host_port = rest.substr(0, slash_pos);
|
||||
base_path = rest.substr(slash_pos);
|
||||
} else {
|
||||
host_port = rest;
|
||||
base_path = "/";
|
||||
}
|
||||
|
||||
size_t colon_pos = host_port.find(":");
|
||||
if (colon_pos != std::string::npos) {
|
||||
host = host_port.substr(0, colon_pos);
|
||||
port = std::stoi(host_port.substr(colon_pos + 1));
|
||||
} else {
|
||||
host = host_port;
|
||||
port = (url.find("https://") == 0) ? 443 : 80;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void discover_endpoints_from_env(const std::string& prefix, std::vector<std::string>& endpoints) {
|
||||
for(int i = 1; i < 10000; i++) {
|
||||
std::string var_name = prefix + "_" + std::to_string(i);
|
||||
const char* endpoint = std::getenv(var_name.c_str());
|
||||
if(!endpoint) break;
|
||||
endpoints.push_back(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
void discover_models(const std::vector<std::string>& endpoints) {
|
||||
for(const auto& endpoint : endpoints) {
|
||||
if(debug) {
|
||||
std::cout << "[DEBUG] Discovering from: " << endpoint << std::endl;
|
||||
}
|
||||
|
||||
std::string host;
|
||||
int port;
|
||||
std::string base_path;
|
||||
if(!parse_url(endpoint, host, port, base_path)) continue;
|
||||
|
||||
httplib::Client cli(host, port);
|
||||
cli.set_connection_timeout(0, 10000000); // 10 sec
|
||||
cli.set_read_timeout(10, 0);
|
||||
|
||||
std::string models_path = base_path + (base_path.back() == '/' ? "models" : "/models");
|
||||
auto res = cli.Get(models_path.c_str());
|
||||
|
||||
if(res && res->status == 200) {
|
||||
// Simple JSON parsing for model IDs
|
||||
std::string body = res->body;
|
||||
size_t pos = 0;
|
||||
while((pos = body.find("\"id\":\"", pos)) != std::string::npos) {
|
||||
pos += 6;
|
||||
size_t end = body.find("\"", pos);
|
||||
if(end != std::string::npos) {
|
||||
std::string model_id = body.substr(pos, end - pos);
|
||||
|
||||
// Filter out modelperm-* and chatcmpl-* entries
|
||||
if(model_id.substr(0, 10) != "modelperm-" && model_id.substr(0, 9) != "chatcmpl-") {
|
||||
ModelInfo info;
|
||||
info.id = model_id;
|
||||
info.endpoint = endpoint;
|
||||
info.host = host;
|
||||
info.port = port;
|
||||
info.max_tokens = 8192;
|
||||
models.push_back(info);
|
||||
|
||||
if(debug) {
|
||||
std::cout << "[DEBUG] Discovered: " << model_id << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos = end + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
UncloseAI(int timeout = 30, bool debug = false) : timeout(timeout), debug(debug) {
|
||||
std::vector<std::string> endpoints;
|
||||
std::vector<std::string> tts_eps;
|
||||
|
||||
discover_endpoints_from_env("MODEL_ENDPOINT", endpoints);
|
||||
discover_endpoints_from_env("TTS_ENDPOINT", tts_eps);
|
||||
|
||||
if(debug) {
|
||||
std::cout << "[DEBUG] Initialized with " << endpoints.size() << " endpoint(s)" << std::endl;
|
||||
}
|
||||
|
||||
discover_models(endpoints);
|
||||
|
||||
// Parse TTS endpoints
|
||||
for(const auto& ep : tts_eps) {
|
||||
std::string host;
|
||||
int port;
|
||||
std::string base_path;
|
||||
if(parse_url(ep, host, port, base_path)) {
|
||||
tts_endpoints.push_back({host, port});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<ModelInfo>& list_models() const {
|
||||
return models;
|
||||
}
|
||||
|
||||
int chat(const std::string& prompt, std::string& response, int model_idx = 0, int max_tokens = 100) {
|
||||
if(model_idx >= static_cast<int>(models.size())) return -1;
|
||||
|
||||
const ModelInfo& model = models[model_idx];
|
||||
|
||||
httplib::Client cli(model.host, model.port);
|
||||
cli.set_connection_timeout(0, timeout * 1000000);
|
||||
cli.set_read_timeout(timeout, 0);
|
||||
|
||||
std::ostringstream json;
|
||||
json << "{\"model\":\"" << model.id << "\","
|
||||
<< "\"messages\":[{\"role\":\"user\",\"content\":\"" << prompt << "\"}],"
|
||||
<< "\"stream\":false,"
|
||||
<< "\"max_tokens\":" << max_tokens << ","
|
||||
<< "\"temperature\":0.7}";
|
||||
|
||||
auto res = cli.Post("/chat/completions", json.str(), "application/json");
|
||||
|
||||
if(res && res->status == 200) {
|
||||
response = res->body;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int chat_stream(const std::string& prompt, std::function<void(const std::string&)> callback, int model_idx = 0, int max_tokens = 500) {
|
||||
// NOTE: cpp-httplib streaming API is complex, using simple buffered approach
|
||||
// For production use, consider implementing proper SSE streaming with ContentReceiver
|
||||
std::string response;
|
||||
if(chat(prompt, response, model_idx, max_tokens) == 0) {
|
||||
if(callback) {
|
||||
callback(response);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int tts(const std::string& text, const std::string& voice, const std::string& output_file) {
|
||||
if(tts_endpoints.empty()) return -1;
|
||||
|
||||
auto [host, port] = tts_endpoints[0];
|
||||
|
||||
httplib::Client cli(host, port);
|
||||
cli.set_connection_timeout(0, timeout * 1000000);
|
||||
cli.set_read_timeout(timeout, 0);
|
||||
|
||||
std::ostringstream json;
|
||||
json << "{\"model\":\"tts-1\","
|
||||
<< "\"voice\":\"" << voice << "\","
|
||||
<< "\"input\":\"" << text << "\"}";
|
||||
|
||||
auto res = cli.Post("/audio/speech", json.str(), "application/json");
|
||||
|
||||
if(res && res->status == 200) {
|
||||
std::ofstream file(output_file, std::ios::binary);
|
||||
if(file.is_open()) {
|
||||
file.write(res->body.c_str(), res->body.size());
|
||||
file.close();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
// Demo program showing library usage
|
||||
int main() {
|
||||
std::cout << "=== UncloseAI C++ Client (cpp-httplib with Streaming) ===\n\n";
|
||||
|
||||
UncloseAI client(30, true);
|
||||
|
||||
if(client.list_models().empty()) {
|
||||
std::cout << "ERROR: No models discovered. Set environment variables:\n";
|
||||
std::cout << " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto models = client.list_models();
|
||||
std::cout << "\nDiscovered " << models.size() << " model(s):\n";
|
||||
for(const auto& m : models) {
|
||||
std::cout << " - " << m.id << " (max_tokens: " << m.max_tokens << ")\n";
|
||||
}
|
||||
std::cout << "\n";
|
||||
|
||||
// Non-streaming chat
|
||||
std::cout << "=== Non-Streaming Chat ===\n";
|
||||
std::string response;
|
||||
if(client.chat("Explain quantum computing in one sentence", response) == 0) {
|
||||
std::cout << "Response received (" << response.size() << " bytes)\n";
|
||||
std::cout << "(Full response requires JSON parsing library)\n\n";
|
||||
} else {
|
||||
std::cout << "Request failed\n\n";
|
||||
}
|
||||
|
||||
// Streaming chat
|
||||
std::cout << "=== Streaming Chat ===\n";
|
||||
int model_idx = (models.size() > 1) ? 1 : 0;
|
||||
std::cout << "Model: " << models[model_idx].id << "\n";
|
||||
std::cout << "Response: ";
|
||||
|
||||
client.chat_stream("Write a hello world program in C++",
|
||||
[](const std::string& content) {
|
||||
std::cout << content << std::flush;
|
||||
}, model_idx, 500);
|
||||
|
||||
std::cout << "\n\n";
|
||||
|
||||
// TTS
|
||||
std::cout << "=== TTS Speech Generation ===\n";
|
||||
std::cout << "Model: tts-1\n";
|
||||
if(client.tts("Hello from UncloseAI C++ client with cpp-httplib! This demonstrates streaming support.",
|
||||
"alloy", "/tmp/speech.mp3") == 0) {
|
||||
std::cout << "Audio saved to /tmp/speech.mp3\n";
|
||||
} else {
|
||||
std::cout << "TTS failed\n";
|
||||
}
|
||||
|
||||
std::cout << "\n=== Examples Complete ===\n";
|
||||
return 0;
|
||||
}
|
||||
25
public/languages/cpp/libcurl/Dockerfile
Normal file
25
public/languages/cpp/libcurl/Dockerfile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# Pin to specific Alpine version (checked 2025-10-12: alpine:3.21 is latest stable)
|
||||
FROM alpine:latest
|
||||
|
||||
# Install C++ compiler and libcurl development libraries
|
||||
RUN apk --no-cache add \
|
||||
g++ \
|
||||
musl-dev \
|
||||
curl-dev \
|
||||
make \
|
||||
ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY uncloseai.cpp .
|
||||
COPY Makefile .
|
||||
|
||||
# Compile the application
|
||||
RUN make
|
||||
|
||||
# Run the examples
|
||||
CMD ["./uncloseai"]
|
||||
20
public/languages/cpp/libcurl/Makefile
Normal file
20
public/languages/cpp/libcurl/Makefile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
CXX = g++
|
||||
CXXFLAGS = -std=c++11 -Wall -Wextra -O2
|
||||
LDFLAGS = -lcurl
|
||||
|
||||
TARGET = uncloseai
|
||||
SRC = uncloseai.cpp
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SRC)
|
||||
$(CXX) $(CXXFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) speech.mp3
|
||||
|
||||
.PHONY: all clean
|
||||
340
public/languages/cpp/libcurl/uncloseai.cpp
Normal file
340
public/languages/cpp/libcurl/uncloseai.cpp
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
// https://www.permacomputer.com
|
||||
|
||||
/*
|
||||
* UncloseAI C++ Library using libcurl
|
||||
* OpenAI-compatible API client with streaming support
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#define MAX_CONTENT_LEN 1024
|
||||
|
||||
struct ModelInfo {
|
||||
std::string id;
|
||||
std::string endpoint;
|
||||
int max_tokens;
|
||||
};
|
||||
|
||||
struct MemoryStruct {
|
||||
std::string data;
|
||||
};
|
||||
|
||||
// Callback for non-streaming responses
|
||||
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {
|
||||
size_t realsize = size * nmemb;
|
||||
MemoryStruct *mem = static_cast<MemoryStruct*>(userp);
|
||||
mem->data.append(static_cast<char*>(contents), realsize);
|
||||
return realsize;
|
||||
}
|
||||
|
||||
// Streaming context
|
||||
struct StreamContext {
|
||||
std::function<void(const std::string&)> callback;
|
||||
std::string buffer;
|
||||
};
|
||||
|
||||
// Extract content from SSE JSON
|
||||
static void extract_sse_content(const std::string& data, std::string& content) {
|
||||
const char *content_marker = "\"content\":\"";
|
||||
size_t start = data.find(content_marker);
|
||||
if(start == std::string::npos) return;
|
||||
|
||||
start += strlen(content_marker);
|
||||
size_t end = start;
|
||||
|
||||
while(end < data.size() && data[end] != '"') {
|
||||
if(data[end] == '\\' && end + 1 < data.size()) {
|
||||
end += 2;
|
||||
} else {
|
||||
end++;
|
||||
}
|
||||
}
|
||||
|
||||
content = data.substr(start, end - start);
|
||||
}
|
||||
|
||||
// Streaming callback
|
||||
static size_t StreamCallback(void *contents, size_t size, size_t nmemb, void *userp) {
|
||||
size_t realsize = size * nmemb;
|
||||
StreamContext *ctx = static_cast<StreamContext*>(userp);
|
||||
|
||||
ctx->buffer.append(static_cast<char*>(contents), realsize);
|
||||
|
||||
size_t pos = 0;
|
||||
while((pos = ctx->buffer.find("\n\n")) != std::string::npos) {
|
||||
std::string line = ctx->buffer.substr(0, pos);
|
||||
ctx->buffer.erase(0, pos + 2);
|
||||
|
||||
if(line.substr(0, 6) == "data: ") {
|
||||
std::string data = line.substr(6);
|
||||
|
||||
if(data == "[DONE]") break;
|
||||
|
||||
std::string content;
|
||||
extract_sse_content(data, content);
|
||||
|
||||
if(!content.empty() && ctx->callback) {
|
||||
ctx->callback(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return realsize;
|
||||
}
|
||||
|
||||
class UncloseAI {
|
||||
private:
|
||||
std::vector<ModelInfo> models;
|
||||
std::vector<std::string> tts_endpoints;
|
||||
int timeout;
|
||||
bool debug;
|
||||
|
||||
void discover_endpoints_from_env(const std::string& prefix, std::vector<std::string>& endpoints) {
|
||||
for(int i = 1; i < 10000; i++) {
|
||||
std::string var_name = prefix + "_" + std::to_string(i);
|
||||
const char* endpoint = std::getenv(var_name.c_str());
|
||||
if(!endpoint) break;
|
||||
endpoints.push_back(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
void discover_models(const std::vector<std::string>& endpoints) {
|
||||
for(const auto& endpoint : endpoints) {
|
||||
if(debug) {
|
||||
std::cout << "[DEBUG] Discovering from: " << endpoint << std::endl;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if(curl) {
|
||||
MemoryStruct response;
|
||||
std::string url = endpoint + "/models";
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if(res == CURLE_OK) {
|
||||
// Simple JSON parsing for model IDs
|
||||
size_t pos = 0;
|
||||
while((pos = response.data.find("\"id\":\"", pos)) != std::string::npos) {
|
||||
pos += 6;
|
||||
size_t end = response.data.find("\"", pos);
|
||||
if(end != std::string::npos) {
|
||||
std::string model_id = response.data.substr(pos, end - pos);
|
||||
|
||||
if(model_id.substr(0, 10) != "modelperm-") {
|
||||
ModelInfo info;
|
||||
info.id = model_id;
|
||||
info.endpoint = endpoint;
|
||||
info.max_tokens = 8192;
|
||||
models.push_back(info);
|
||||
|
||||
if(debug) {
|
||||
std::cout << "[DEBUG] Discovered: " << model_id << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos = end + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
UncloseAI(int timeout = 30, bool debug = false) : timeout(timeout), debug(debug) {
|
||||
std::vector<std::string> endpoints;
|
||||
std::vector<std::string> tts_eps;
|
||||
|
||||
discover_endpoints_from_env("MODEL_ENDPOINT", endpoints);
|
||||
discover_endpoints_from_env("TTS_ENDPOINT", tts_eps);
|
||||
|
||||
if(debug) {
|
||||
std::cout << "[DEBUG] Initialized with " << endpoints.size() << " endpoint(s)" << std::endl;
|
||||
}
|
||||
|
||||
discover_models(endpoints);
|
||||
tts_endpoints = tts_eps;
|
||||
}
|
||||
|
||||
const std::vector<ModelInfo>& list_models() const {
|
||||
return models;
|
||||
}
|
||||
|
||||
int chat(const std::string& prompt, std::string& response, int model_idx = 0) {
|
||||
if(model_idx >= static_cast<int>(models.size())) return -1;
|
||||
|
||||
const ModelInfo& model = models[model_idx];
|
||||
std::string url = model.endpoint + "/chat/completions";
|
||||
|
||||
std::string json = "{\"model\":\"" + model.id + "\","
|
||||
"\"messages\":[{\"role\":\"user\",\"content\":\"" + prompt + "\"}],"
|
||||
"\"max_tokens\":100}";
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if(!curl) return -1;
|
||||
|
||||
MemoryStruct mem;
|
||||
struct curl_slist *headers = nullptr;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &mem);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
response = mem.data;
|
||||
return (res == CURLE_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
int chat_stream(const std::string& prompt, std::function<void(const std::string&)> callback, int model_idx = 0) {
|
||||
if(model_idx >= static_cast<int>(models.size())) return -1;
|
||||
|
||||
const ModelInfo& model = models[model_idx];
|
||||
std::string url = model.endpoint + "/chat/completions";
|
||||
|
||||
std::string json = "{\"model\":\"" + model.id + "\","
|
||||
"\"messages\":[{\"role\":\"user\",\"content\":\"" + prompt + "\"}],"
|
||||
"\"stream\":true,"
|
||||
"\"max_tokens\":500}";
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if(!curl) return -1;
|
||||
|
||||
StreamContext ctx;
|
||||
ctx.callback = callback;
|
||||
|
||||
struct curl_slist *headers = nullptr;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, StreamCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
return (res == CURLE_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
int tts(const std::string& text, const std::string& voice, const std::string& output_file) {
|
||||
if(tts_endpoints.empty()) return -1;
|
||||
|
||||
std::string url = tts_endpoints[0] + "/audio/speech";
|
||||
std::string json = "{\"model\":\"tts-1\","
|
||||
"\"voice\":\"" + voice + "\","
|
||||
"\"input\":\"" + text + "\"}";
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if(!curl) return -1;
|
||||
|
||||
MemoryStruct mem;
|
||||
struct curl_slist *headers = nullptr;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &mem);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if(res == CURLE_OK) {
|
||||
std::ofstream file(output_file, std::ios::binary);
|
||||
if(file.is_open()) {
|
||||
file.write(mem.data.c_str(), mem.data.size());
|
||||
file.close();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
// Demo
|
||||
int main() {
|
||||
std::cout << "=== UncloseAI C++ Client (with Streaming) ===\n\n";
|
||||
|
||||
curl_global_init(CURL_GLOBAL_ALL);
|
||||
|
||||
UncloseAI client(30, true);
|
||||
|
||||
if(client.list_models().empty()) {
|
||||
std::cout << "ERROR: No models discovered. Set environment variables:\n";
|
||||
std::cout << " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n";
|
||||
curl_global_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto models = client.list_models();
|
||||
std::cout << "\nDiscovered " << models.size() << " model(s):\n";
|
||||
for(const auto& m : models) {
|
||||
std::cout << " - " << m.id << " (max_tokens: " << m.max_tokens << ")\n";
|
||||
}
|
||||
std::cout << "\n";
|
||||
|
||||
// Non-streaming chat
|
||||
std::cout << "=== Non-Streaming Chat ===\n";
|
||||
std::string response;
|
||||
if(client.chat("Explain quantum computing in one sentence", response) == 0) {
|
||||
std::cout << "Response: (" << response.size() << " bytes received)\n\n";
|
||||
}
|
||||
|
||||
// Streaming chat
|
||||
std::cout << "=== Streaming Chat ===\n";
|
||||
int model_idx = (models.size() > 1) ? 1 : 0;
|
||||
std::cout << "Model: " << models[model_idx].id << "\n";
|
||||
std::cout << "Response: ";
|
||||
|
||||
client.chat_stream("Write a C++ function to check if a number is prime",
|
||||
[](const std::string& content) {
|
||||
std::cout << content << std::flush;
|
||||
}, model_idx);
|
||||
|
||||
std::cout << "\n\n";
|
||||
|
||||
// TTS
|
||||
std::cout << "=== TTS Speech Generation ===\n";
|
||||
if(client.tts("Hello from UncloseAI C++ client! This demonstrates streaming support.",
|
||||
"alloy", "speech.mp3") == 0) {
|
||||
std::cout << "[OK] Speech file created: speech.mp3\n\n";
|
||||
} else {
|
||||
std::cout << "[ERROR] TTS Error\n\n";
|
||||
}
|
||||
|
||||
std::cout << "=== Examples Complete ===\n";
|
||||
|
||||
curl_global_cleanup();
|
||||
return 0;
|
||||
}
|
||||
21
public/languages/crystal/Dockerfile
Normal file
21
public/languages/crystal/Dockerfile
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
||||
# https://www.permacomputer.com
|
||||
|
||||
# Crystal latest-alpine (checked 2025-10-13: crystallang/crystal:latest-alpine is latest stable)
|
||||
FROM crystallang/crystal:latest-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY uncloseai.cr .
|
||||
|
||||
# Build the application
|
||||
RUN crystal build --release uncloseai.cr
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache ca-certificates gc-dev pcre2-dev libevent-dev
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/uncloseai .
|
||||
|
||||
CMD ["./uncloseai"]
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue