mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-13 20:12:57 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29834f02c0 | ||
|
|
0bcbc8f784 | ||
|
|
abd66309c8 | ||
|
|
a2413b12b0 | ||
|
|
7fca9b79ea | ||
|
|
ffaa91f849 | ||
|
|
b32011f10f | ||
|
|
50d5713e8f | ||
|
|
1133c4656a | ||
|
|
fd387597bb | ||
|
|
5169d4bb40 | ||
|
|
b74ec1f69c | ||
|
|
74319b44af | ||
|
|
c2c5d88336 | ||
|
|
fa4b38cd02 | ||
|
|
e49af481ba | ||
|
|
a803d55cfb | ||
|
|
ab704ce4d7 | ||
|
|
70fde95255 | ||
|
|
e5c3769bd8 | ||
|
|
390230c5a0 | ||
|
|
2000b8909f | ||
|
|
ad2ed295b1 | ||
|
|
a3513c22a2 | ||
|
|
4d380ffa8a | ||
|
|
6dcb3777ca | ||
|
|
ca17f38929 | ||
|
|
735f275e7c | ||
|
|
29cd14a01d | ||
|
|
84b2400650 | ||
|
|
d4b4df06fd | ||
|
|
1e5deabcdb | ||
|
|
949d3ab17c | ||
|
|
62cf10072a | ||
|
|
f7147ab312 | ||
|
|
16e2729b1d | ||
|
|
ce0ab02f39 | ||
|
|
5e30958a14 | ||
|
|
2b0e173975 | ||
|
|
12806a2395 | ||
|
|
26666fe666 | ||
|
|
c8bed6eaee | ||
|
|
d4eff82d3c | ||
|
|
da4a1a182b | ||
|
|
50383aab59 | ||
|
|
e6fc773706 | ||
|
|
9dbba292c4 | ||
|
|
484bb5c201 | ||
|
|
d2358d234b | ||
|
|
29c0241119 | ||
|
|
8f27742038 | ||
|
|
e2797c38d0 | ||
|
|
747614c28b | ||
|
|
437467e0ce | ||
|
|
0ce8fdc9f6 | ||
|
|
4e17908664 | ||
|
|
f54b7b7b3d | ||
|
|
f0434435c4 | ||
|
|
57231d31e4 | ||
|
|
cfa3fde436 | ||
|
|
55d052cfc2 | ||
|
|
e281dc92e3 | ||
|
|
dc90999fee | ||
|
|
d88dedb66c | ||
|
|
40aa52225f | ||
|
|
e257dceb93 | ||
|
|
6c2fa4d398 | ||
|
|
2d12949a08 | ||
|
|
60eef2fc67 | ||
|
|
af3d732fc4 | ||
|
|
d0eca6dafb | ||
|
|
760eb55b69 | ||
|
|
8ffd0004b3 | ||
|
|
05ced4e625 | ||
|
|
3bce60893e | ||
|
|
0c42613ff0 | ||
|
|
babe9f8ede | ||
|
|
7151723894 | ||
|
|
0009ededfa | ||
|
|
e9279ffb36 | ||
|
|
e6ad54be19 | ||
|
|
cbe02a4489 | ||
|
|
77d6bb2d8c | ||
|
|
dcd47bc4cf | ||
|
|
913c1b7836 | ||
|
|
1dad2e8716 | ||
|
|
c852a287e8 | ||
|
|
4005e9667c | ||
|
|
5718071c08 | ||
|
|
0fac03a05a | ||
|
|
c96d3b546c | ||
|
|
3451308e48 | ||
|
|
5b04d1afe1 | ||
|
|
97f86e564f | ||
|
|
3f99d975c0 | ||
|
|
81ee52b00f | ||
|
|
4d826c0602 | ||
|
|
ecc89678d1 | ||
|
|
5ad53e061a | ||
|
|
d00061ae2f | ||
|
|
52c1adb7eb | ||
|
|
bb348e4bd5 | ||
|
|
6083e3ac52 | ||
|
|
db9597a7b5 | ||
|
|
aae18e6878 | ||
|
|
c7f3e0b937 | ||
|
|
1fde01aa04 | ||
|
|
e9f6266062 | ||
|
|
40030ebc94 | ||
|
|
54a104276b | ||
|
|
bcdec32ba2 | ||
|
|
06452f5283 | ||
|
|
36dc9664ad | ||
|
|
3f5b34cd34 | ||
|
|
ee136f4168 | ||
|
|
7909cae76e | ||
|
|
6f7ab2ab17 | ||
|
|
bfdf815f61 | ||
|
|
925077916e | ||
|
|
9cbb741a8a | ||
|
|
1256614ec2 | ||
|
|
cf7aa27543 | ||
|
|
61824ea04b | ||
|
|
7f50da5082 | ||
|
|
8143e5700f | ||
|
|
2b505caf98 | ||
|
|
6f39365b24 | ||
|
|
591c6c0fd3 | ||
|
|
4c72e9d285 | ||
|
|
44c6f57c7a | ||
|
|
e8f5450cb0 | ||
|
|
4e578e9b28 | ||
|
|
55f3f07ee5 | ||
|
|
f13fa2553c | ||
|
|
ed2eea7c6b | ||
|
|
967da6ba24 | ||
|
|
d03952b237 | ||
|
|
f27ef665ee | ||
|
|
01fc301f1e | ||
|
|
5798fcd042 | ||
|
|
17d0738d62 | ||
|
|
7d24fa2b32 | ||
|
|
3883af2e96 | ||
|
|
56ae760bea | ||
|
|
38ccc82c8e | ||
|
|
37ca89228c | ||
|
|
8cff90bdc3 | ||
|
|
f177732a0e | ||
|
|
d638f371d0 | ||
|
|
b35cf776aa | ||
|
|
4fd6da9903 | ||
|
|
98be5c1323 | ||
|
|
6261d7c90c | ||
|
|
365fac907c | ||
|
|
a5e4840920 | ||
|
|
766c235cc7 | ||
|
|
8d5860d733 | ||
|
|
78ac033a1c | ||
|
|
7fa0822584 | ||
|
|
113ed31336 | ||
|
|
ca9e74ece4 | ||
|
|
4f3a6f7bbc | ||
|
|
5bf11adff3 | ||
|
|
202e7f06e2 | ||
|
|
d9822a4f09 | ||
|
|
f359ae193e | ||
|
|
807c3ba5ec | ||
|
|
fb7025164e | ||
|
|
5ef75e5df3 | ||
|
|
9215937064 | ||
|
|
c9215d9e86 | ||
|
|
9d9d97fd00 | ||
|
|
6f4b1bf94a | ||
|
|
706a2403e1 | ||
|
|
d6bbba7bf0 | ||
|
|
6a6bfa034f | ||
|
|
14cc8f7bc0 | ||
|
|
e1bdee1abe | ||
|
|
2993b887bd | ||
|
|
7293fdbbfa | ||
|
|
28c5800d06 | ||
|
|
75aaaa66a8 | ||
|
|
7aa886d573 | ||
|
|
a295420f58 | ||
|
|
8fabf014a9 | ||
|
|
eac2ddf8da | ||
|
|
eb59c1a102 | ||
|
|
d65ee6cfb1 | ||
|
|
1ed3bccaa5 | ||
|
|
d9a8ea095c | ||
|
|
dd86724941 | ||
|
|
405d9b35aa | ||
|
|
44ad3c5781 | ||
|
|
f60c008158 | ||
|
|
6d196d59f0 | ||
|
|
740fdb592e | ||
|
|
3097b86597 | ||
|
|
8963eb0c4f | ||
|
|
686d3de518 | ||
|
|
a13af7e31a | ||
|
|
63b3376b48 | ||
|
|
626c453f19 | ||
|
|
3856ec476d | ||
|
|
0859543ca3 | ||
|
|
38adf9fce3 | ||
|
|
f2e2473a7d |
No files matched your search
@@ -1,5 +1,69 @@
|
||||
# Changelog
|
||||
|
||||
## v2.5.1 (2025-02-13)
|
||||
|
||||
### Puter
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- phoenix changelog ([0bcbc8f](https://github.com/HeyPuter/puter/commit/0bcbc8f7845de99305f53c6da2bb1f365b87ac50))
|
||||
- update package.json ([c2c5d88](https://github.com/HeyPuter/puter/commit/c2c5d883365ae33749709d11e0c2de9050ca144e))
|
||||
- oops, no export (putility.libs.event) ([fa4b38c](https://github.com/HeyPuter/puter/commit/fa4b38cd028be4b19ec98bcf588227e0fc92af9d))
|
||||
- broken test in putility ([a803d55](https://github.com/HeyPuter/puter/commit/a803d55cfbdd5b15e7fe48df3f4363c1658f0930))
|
||||
- parse body before auth for /down ([70fde95](https://github.com/HeyPuter/puter/commit/70fde95255532a7fe0d99c64a4efb1ae625776a4))
|
||||
- fix previous fix ([e5c3769](https://github.com/HeyPuter/puter/commit/e5c3769bd813b1510dd0429e1e4eca8e277af7c7))
|
||||
- potential fix for /down auth ([390230c](https://github.com/HeyPuter/puter/commit/390230c5a07b1774f84a1b3505f7531ce81dc2cc))
|
||||
- allow command provider to not implement complete method ([2000b89](https://github.com/HeyPuter/puter/commit/2000b8909f08d91147b86fce22fe006e0c3152d2))
|
||||
- unfixed fix from earlier ([e6fc773](https://github.com/HeyPuter/puter/commit/e6fc7737066d09509f0c7b38e4c51f25e86e12d0))
|
||||
- parser error for empty json buffer ([484bb5c](https://github.com/HeyPuter/puter/commit/484bb5c201e17bf45e1a1d97b1e9b2d61d6087dc))
|
||||
- fix name and id for openai tool calls ([d2358d2](https://github.com/HeyPuter/puter/commit/d2358d234b45d719a2cc4e92582ed89d2d1832ab))
|
||||
- let messages with tool_calls have content=null ([29c0241](https://github.com/HeyPuter/puter/commit/29c024111943267b741b1b4a8933e1ea1a35a65e))
|
||||
- repair stream end ([8f27742](https://github.com/HeyPuter/puter/commit/8f277420380e9c6fa8a9925a3e9651f48b8734e6))
|
||||
- add type=text ([e2797c3](https://github.com/HeyPuter/puter/commit/e2797c38d0754930033780d5270cc64cbba2c94e))
|
||||
- various issues with Mail module ([55d052c](https://github.com/HeyPuter/puter/commit/55d052cfc2549bfdf72f3a8b27cdc7dc4294bc54))
|
||||
- buffer incomplete JSON objects from AI stream ([60eef2f](https://github.com/HeyPuter/puter/commit/60eef2fc6734f88df06e2f85db9b9368cc8c227f))
|
||||
- mistake in 0c42613 ([8ffd000](https://github.com/HeyPuter/puter/commit/8ffd0004b3b7b34cd6a9c43c6ca960c7a1cbbe15))
|
||||
- fix microcents to USD conversion in AIChatService ([dcd47bc](https://github.com/HeyPuter/puter/commit/dcd47bc4cfc5f8a67ea86e0485d08c2417f899ed))
|
||||
- claude duplicate messages in stream ([0fac03a](https://github.com/HeyPuter/puter/commit/0fac03a05a4f597f7ed531651c830e44012b646b))
|
||||
- skip request-count usage check via AIChatService ([6083e3a](https://github.com/HeyPuter/puter/commit/6083e3ac52fcde7f598c838bc49085e6b3de7162))
|
||||
- remove log from InternetModule ([c7f3e0b](https://github.com/HeyPuter/puter/commit/c7f3e0b937f5d72d6f30dba25d7c351e2e14f289))
|
||||
- small workaround for duplicate close ([06452f5](https://github.com/HeyPuter/puter/commit/06452f5283085b18266ee7fb89136b9c23879243))
|
||||
- race condition and buffer issue in puter.http ([36dc966](https://github.com/HeyPuter/puter/commit/36dc9664ad5520b21c07a1b5c85c8aff7cbe423b))
|
||||
- missing some buffer contents in no-keepalive ([3f5b34c](https://github.com/HeyPuter/puter/commit/3f5b34cd341b9063d01baba72e708a9ebb16485b))
|
||||
- new edge cases with function calls / tools ([9cbb741](https://github.com/HeyPuter/puter/commit/9cbb741a8ae8ea6b869b6ccf64cd3152b28c2b8c))
|
||||
- oops, we're passing negative values; let's just remove this ([cf7aa27](https://github.com/HeyPuter/puter/commit/cf7aa27543700d6268ee709f127e73f7cfe12a5a))
|
||||
- oops we still need that ([61824ea](https://github.com/HeyPuter/puter/commit/61824ea04b0cb7611d2acdf45e0a1ecc2856901a))
|
||||
- remove hard-coded token limit for OpenAI ([8143e57](https://github.com/HeyPuter/puter/commit/8143e5700f53279a5a18d21b7c5466f3b9bb6ce6))
|
||||
- wisp relay authentication ([6f39365](https://github.com/HeyPuter/puter/commit/6f39365b24cda53a6cac7e203b9d8cbc09bb0ba3))
|
||||
- reduce code paths for querystrings ([e8f5450](https://github.com/HeyPuter/puter/commit/e8f5450cb05213c3c06802442103f5c414eee5cc))
|
||||
- icons ([d03952b](https://github.com/HeyPuter/puter/commit/d03952b23712ae8a61c7f2c7582d297691e0ecc1))
|
||||
- subdomains to deleted files tried to deref fs node ([38ccc82](https://github.com/HeyPuter/puter/commit/38ccc82c8e95636ee4b7c5ca2f761098f12affa2))
|
||||
- app icon empty string should be skipped ([37ca892](https://github.com/HeyPuter/puter/commit/37ca89228cc2f978602098ee4aae1ecb3d333526))
|
||||
- save_account case for disable_user_signup ([766c235](https://github.com/HeyPuter/puter/commit/766c235cc738051588a67ff5ab4230e76b64173c))
|
||||
- use .get() for Map lookup. fix: correctly set url and url_paths. fix: null check to throw error. ([78ac033](https://github.com/HeyPuter/puter/commit/78ac033a1ca4f51b71c2bcb185b305903f7be495))
|
||||
- ensure puter.signup emit resolves ([113ed31](https://github.com/HeyPuter/puter/commit/113ed31336c494a3f7a9e744a34de35b3785c033))
|
||||
- --onlycase param broke cartesian tests ([d9822a4](https://github.com/HeyPuter/puter/commit/d9822a4f09e3e0c5fbed8c655435f534af949290))
|
||||
- empty response when mkdir is a no-op ([f359ae1](https://github.com/HeyPuter/puter/commit/f359ae193e87552b3a2e2aafa3fda389478fca38))
|
||||
- mkdir with create_missing when some parents exist ([807c3ba](https://github.com/HeyPuter/puter/commit/807c3ba5eca02f69b5e6ce547420312b68c7993f))
|
||||
- possible out-or-order response objects from batch ([fb70251](https://github.com/HeyPuter/puter/commit/fb7025164e3f42cae1365ec65960019b24f4360d))
|
||||
- app data check error in write ([5ef75e5](https://github.com/HeyPuter/puter/commit/5ef75e5df35ae95242da97235512495b7585bd0d))
|
||||
- missing parent dirs created in move ([9d9d97f](https://github.com/HeyPuter/puter/commit/9d9d97fd0074058506b0506d5027b0c6b8a26845))
|
||||
- missing changes to run-selfhosted.js ([6f4b1bf](https://github.com/HeyPuter/puter/commit/6f4b1bf94a031b3324f5ecd51557b1298a1c3175))
|
||||
- appease mocha's import requirements ([d6bbba7](https://github.com/HeyPuter/puter/commit/d6bbba7bf064991d59fbfe74db5221e0118a781c))
|
||||
- error msg for invalid puter-ocr urls ([6a6bfa0](https://github.com/HeyPuter/puter/commit/6a6bfa034fe16dba7172ab5adbf23f00df38301d))
|
||||
- improper 500 in wisp token verify ([75aaaa6](https://github.com/HeyPuter/puter/commit/75aaaa66a8c7df00e1fb80c353d890269296839c))
|
||||
- actor param in legacy /write ([7aa886d](https://github.com/HeyPuter/puter/commit/7aa886d573362e6739bd99bbed02f4831557ccb4))
|
||||
- new desktop height calculation when resizing browser window ([a295420](https://github.com/HeyPuter/puter/commit/a295420f58326b04c976cf92bd2d582d2eafa71b))
|
||||
- circular imports ([8fabf01](https://github.com/HeyPuter/puter/commit/8fabf014a9eb783183e87489ae2b6c6bbc42c99a))
|
||||
- test and improve boolify ([44ad3c5](https://github.com/HeyPuter/puter/commit/44ad3c578106d2b01007240188db57760c15af96))
|
||||
- skip test files in mod lib loading ([f60c008](https://github.com/HeyPuter/puter/commit/f60c008158127458e02e3bb92287617d9f1f9514))
|
||||
- shortcut issue ([6d196d5](https://github.com/HeyPuter/puter/commit/6d196d59f026bec4acb0296d8f0f38c7cee2e8c2))
|
||||
- test for get-launch-apps ([740fdb5](https://github.com/HeyPuter/puter/commit/740fdb592e494bf5b197493774cef6559bfb50b9))
|
||||
- add package-lock.json ([3097b86](https://github.com/HeyPuter/puter/commit/3097b86597218de9e59b450b70185634a94be210))
|
||||
- try redundant npm install after build stage ([8963eb0](https://github.com/HeyPuter/puter/commit/8963eb0c4f1220dd515ac6ed7a2a8f1de26655ae))
|
||||
- I'ma buy GitHub a coffee and spill it on their servers if this works ([686d3de](https://github.com/HeyPuter/puter/commit/686d3de518e6e090d683294ad3dd856db26856a0))
|
||||
- oh, right; there's two of them ([a13af7e](https://github.com/HeyPuter/puter/commit/a13af7e31aa4cd36457a90a7d75878b6d39ba73b))
|
||||
|
||||
## v2.5.0 (2025-01-07)
|
||||
|
||||
### Puter
|
||||
|
||||
+6
-2
@@ -8,7 +8,7 @@
|
||||
# worry about Docker unless the build/run process changes.
|
||||
|
||||
# Build stage
|
||||
FROM node:21-alpine AS build
|
||||
FROM node:22-alpine AS build
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache git python3 make g++ \
|
||||
@@ -41,7 +41,7 @@ RUN npm cache clean --force && \
|
||||
RUN cd src/gui && npm run build && cd -
|
||||
|
||||
# Production stage
|
||||
FROM node:21-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
# Set labels
|
||||
LABEL repo="https://github.com/HeyPuter/puter"
|
||||
@@ -71,4 +71,8 @@ HEALTHCHECK --interval=30s --timeout=3s \
|
||||
|
||||
ENV NO_VAR_RUNTUME=1
|
||||
|
||||
# Attempt to fix `[email protected]` missing after build stage
|
||||
# by doing a redundant `npm install` at this stage
|
||||
RUN npm install
|
||||
|
||||
CMD ["npm", "start"]
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
# Trademark Guidelines
|
||||
|
||||
Version 1.0 dated January 1, 2025
|
||||
|
||||
<img src="https://puter.com/images/logo.png" alt="Puter Technologies Inc. Logo" width="150"/>
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
This trademark policy was prepared to help you understand how to use the Puter trademarks, service marks and logos with Puter Technologies Inc.'s Puter software.
|
||||
|
||||
While some of our software is available under a free and open source software license, that copyright license does not include a license to use our trademark, and this Policy is intended to explain how to use our marks consistent with background law and community expectation.
|
||||
|
||||
This Policy covers:
|
||||
|
||||
1. Our **word** trademarks and service marks: Puter Technologies Inc., Puter, Puter.com
|
||||
2. Our **logos**: The Puter Technologies Inc. logo at the top of this policy
|
||||
|
||||
This policy encompasses all trademarks and service marks, whether they are registered or not.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## 1. GENERAL GUIDELINES
|
||||
|
||||
Whenever you use one of our marks, you must always do so in a way that does not mislead anyone about what they are getting and from whom. For example, you cannot say you are providing the Puter software when you're providing a modified version of it, because recipients may not understand the differences between your modified versions and our own.
|
||||
|
||||
You also cannot use our logo on your website in a way that suggests that your website is an official website or that we endorse your website.
|
||||
|
||||
You can, though, say you like the Puter software, that you participate in the Puter community, that you are providing an unmodified version of the Puter software.
|
||||
|
||||
You may not use or register our marks, or variations of them as part of your own trademark, service mark, domain name, company name, trade name, product name or service name.
|
||||
|
||||
Trademark law does not allow your use of names or trademarks that are too similar to ours. You therefore may not use an obvious variation of any of our marks or any phonetic equivalent, foreign language equivalent, takeoff, or abbreviation for a similar or compatible product or service. We would consider the following too similar to one of our Marks:
|
||||
|
||||
- MyPuter
|
||||
- PuterFooBar
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## 2. ACCEPTABLE USES
|
||||
|
||||
<br>
|
||||
|
||||
### Distribution of Unmodified Software
|
||||
|
||||
When you redistribute an unmodified copy of Puter software, you must retain all trademarks, logos, and notices we have placed on the software to identify its origin. This includes:
|
||||
|
||||
* Binary distributions exactly as we provide them
|
||||
* Source code distributions exactly as we provide them
|
||||
* Documentation and other materials directly from our official repositories
|
||||
|
||||
<br>
|
||||
|
||||
### Distribution of Modified Software
|
||||
|
||||
If you distribute a modified version of Puter software, you:
|
||||
|
||||
* Must remove all Puter logos from the modified software
|
||||
* May use our word marks (but not logos) to accurately describe the software's origin
|
||||
* Must clearly indicate that the software has been modified
|
||||
* Must include a notice stating: "This software is a modified version of Puter software and is not endorsed by Puter Technologies Inc."
|
||||
|
||||
Example of acceptable description: "This software is derived from Puter software and includes modifications for [describe your changes]."
|
||||
|
||||
<br>
|
||||
|
||||
### Compatibility Statements
|
||||
|
||||
You may use our word marks (but not logos) to accurately describe your software's compatibility with Puter software under these conditions:
|
||||
|
||||
* Your statements about compatibility must be accurate and not misleading
|
||||
* You must include the following notice: "Puter is a trademark of Puter Technologies Inc. This [product/service] is not affiliated with or endorsed by Puter Technologies Inc."
|
||||
* You may not suggest that Puter Technologies Inc. has certified or approved your software
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
### Products Built for Puter
|
||||
|
||||
You may describe your product as working with or being built for Puter if:
|
||||
|
||||
* Your product is fully compatible with the documented Puter APIs
|
||||
* Your product name follows this format: "[Your Product Name] for Puter"
|
||||
* You include this notice in all materials: "Puter is a trademark of Puter Technologies Inc. [Your Product Name] is not affiliated with or endorsed by Puter Technologies Inc."
|
||||
* Your branding and marketing materials do not create confusion about the source of your product
|
||||
|
||||
<br>
|
||||
|
||||
### Open Source Projects
|
||||
|
||||
For open source projects that interact with or extend Puter software:
|
||||
|
||||
* You may use "puter" as part of your project name only if:
|
||||
* The name is in the format "[descriptor]-puter" (e.g., "auth-puter", "backup-puter")
|
||||
* The project's README clearly states it's not officially associated with Puter
|
||||
* The project maintains compatibility with current Puter APIs
|
||||
* You must not use our logos without explicit permission
|
||||
* You must include appropriate trademark attribution notices
|
||||
|
||||
<br>
|
||||
|
||||
### Community Activities
|
||||
|
||||
You may use our word marks (but not logos) for non-commercial community activities:
|
||||
|
||||
* User groups and meetups focused on Puter software
|
||||
* Educational content about Puter software
|
||||
* Blog posts, videos, articles, or tutorials about Puter software
|
||||
|
||||
Conditions for community use:
|
||||
|
||||
* Activities must be non-commercial
|
||||
* Any fees charged must only cover actual costs
|
||||
* You must include appropriate trademark attribution
|
||||
* You must not suggest official endorsement without explicit permission
|
||||
|
||||
<br>
|
||||
|
||||
### Merchandise and Promotional Items
|
||||
|
||||
You may not create merchandise or promotional items bearing our marks without explicit written permission from Puter Technologies Inc.
|
||||
|
||||
<br>
|
||||
|
||||
### Academic and Research Use
|
||||
|
||||
You may use our word marks (but not logos) in:
|
||||
|
||||
* Academic papers
|
||||
* Research publications
|
||||
* Technical documentation
|
||||
* Educational materials
|
||||
|
||||
Include appropriate citations and trademark attributions in such uses.
|
||||
|
||||
<br>
|
||||
|
||||
### Online Content and Social Media
|
||||
|
||||
When using our marks in online content:
|
||||
|
||||
* You may use our word marks in hashtags, handles, or usernames if:
|
||||
* The content is clearly about Puter software
|
||||
* You don't imply official status
|
||||
* You include appropriate trademark attribution
|
||||
* You must not register social media accounts that could be confused with official Puter accounts
|
||||
|
||||
<br>
|
||||
|
||||
### APIs and Development
|
||||
|
||||
When developing with Puter APIs:
|
||||
|
||||
* You may use our word marks to accurately describe your integration
|
||||
* You must not use our marks in a way that suggests your API or service is endorse by
|
||||
Puter or provided by Puter
|
||||
* You must include appropriate trademark attribution
|
||||
|
||||
All uses described above must also comply with the General Guidelines section of this policy and maintain the integrity of our marks as described in the How to Display Our Marks section.
|
||||
|
||||
<br>
|
||||
|
||||
### No Domain Names
|
||||
|
||||
You must not register any domain that includes our word marks or any variant or combination of them.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## 3. HOW TO DISPLAY OUR MARKS
|
||||
|
||||
When you have the right to use our mark, here is how to display it.
|
||||
|
||||
<br>
|
||||
|
||||
### Trademark marking and legends
|
||||
|
||||
The first or most prominent mention of a mark on a webpage, document, or documentation should be accompanied by a symbol indicating whether the mark is a registered trademark ("®") or an unregistered trademark ("™"). If you don't know which applies, contact us.
|
||||
|
||||
Place the following notice at the foot of the page where you have used the mark: "Puter is trademark of Puter Technologies Inc."
|
||||
|
||||
<br>
|
||||
|
||||
### Use of trademarks in text
|
||||
|
||||
**Always use trademarks in their exact form with the correct spelling, neither abbreviated, hyphenated, or combined with any other word or words.**
|
||||
|
||||
Unacceptable: Puter-DB
|
||||
|
||||
**Don't pluralize a trademark.**
|
||||
|
||||
Unacceptable: I have seventeen Puters running in my lab.
|
||||
|
||||
**Always use a trademark as an adjective modifying a noun.**
|
||||
|
||||
Unacceptable: This is a Puter.
|
||||
Acceptable: This is a Puter software application.
|
||||
|
||||
<br>
|
||||
|
||||
### Use of Logos
|
||||
|
||||
You may not change any logo except to scale it. This means you may not add decorative elements, change the colors, change the proportions, distort it, add elements, or combine it with other logos.
|
||||
|
||||
However, when the context requires the use of black-and-white graphics and the logo is color, you may reproduce the logo in a manner that produces a black-and-white image.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## This Document
|
||||
|
||||
These guidelines are based on the Model Trademark Guidelines, available at http://www.modeltrademarkguidelines.org., used under a Creative Commons Attribution 3.0 Unported license: https://creativecommons.org/licenses/by/3.0/deed.en_US.
|
||||
Generated
+41
-8
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "puter.com",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "puter.com",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.1",
|
||||
"license": "AGPL-3.0-only",
|
||||
"workspaces": [
|
||||
"src/*",
|
||||
@@ -2327,6 +2327,14 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/generative-ai": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz",
|
||||
"integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.10.10",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.10.tgz",
|
||||
@@ -2461,9 +2469,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@heyputer/kv.js": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@heyputer/kv.js/-/kv.js-0.1.8.tgz",
|
||||
"integrity": "sha512-bNj2qo1keALKR6luFXRWJJ1mtoN2oozmq9SD6/U2O3uEcrnR1NhWKYuwl4pORixA5NL1ez4jbkwUxuTRT0V+iQ==",
|
||||
"version": "0.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@heyputer/kv.js/-/kv.js-0.1.9.tgz",
|
||||
"integrity": "sha512-4zmS/kMp/glMmw4h+OYkD+AribMAdfFj1/1AyLpO/bgQELjM7ZsGX7VeaqtbgiNAT5loeDd3ER9PtP9KUULuHg==",
|
||||
"dependencies": {
|
||||
"minimatch": "^9.0.0"
|
||||
}
|
||||
@@ -2511,6 +2519,10 @@
|
||||
"resolved": "src/phoenix",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@heyputer/puter-api-test": {
|
||||
"resolved": "tools/api-tester",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@heyputer/puter-wisp": {
|
||||
"resolved": "src/puter-wisp",
|
||||
"link": true
|
||||
@@ -9103,6 +9115,14 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/convertapi": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/convertapi/-/convertapi-1.15.0.tgz",
|
||||
"integrity": "sha512-wu1pJ27SuIc/mNlbjs8lP1hAsEN16AmKzMZNo9Qx/Z3CrH1ozGQYF2jGXaceXWSRNvKFoCuF0m5ek+vz8Nswrw==",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
@@ -17690,13 +17710,14 @@
|
||||
},
|
||||
"src/backend": {
|
||||
"name": "@heyputer/backend",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.1",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.26.1",
|
||||
"@aws-sdk/client-polly": "^3.622.0",
|
||||
"@aws-sdk/client-textract": "^3.621.0",
|
||||
"@heyputer/kv.js": "^0.1.8",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@heyputer/kv.js": "^0.1.9",
|
||||
"@heyputer/multest": "^0.0.2",
|
||||
"@heyputer/putility": "^1.0.0",
|
||||
"@mistralai/mistralai": "^1.3.4",
|
||||
@@ -17717,6 +17738,7 @@
|
||||
"clean-css": "^5.3.2",
|
||||
"composite-error": "^1.0.2",
|
||||
"compression": "^1.7.4",
|
||||
"convertapi": "^1.15.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dedent": "^1.5.3",
|
||||
"express": "^4.18.2",
|
||||
@@ -17979,7 +18001,7 @@
|
||||
},
|
||||
"src/putility": {
|
||||
"name": "@heyputer/putility",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"license": "AGPL-3.0-only"
|
||||
},
|
||||
"src/strataparse": {
|
||||
@@ -18010,6 +18032,17 @@
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-only"
|
||||
},
|
||||
"tools/api-tester": {
|
||||
"name": "@heyputer/puter-api-test",
|
||||
"version": "0.1.0",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"yaml": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"tools/comment-parser": {
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "puter.com",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"author": "Puter Technologies Inc.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"description": "Desktop environment in the browser!",
|
||||
|
||||
+12
-1
@@ -33,7 +33,12 @@ const { TemplateModule } = require("./src/modules/template/TemplateModule.js");
|
||||
const { PuterFSModule } = require("./src/modules/puterfs/PuterFSModule.js");
|
||||
const { PerfMonModule } = require("./src/modules/perfmon/PerfMonModule.js");
|
||||
const { AppsModule } = require("./src/modules/apps/AppsModule.js");
|
||||
|
||||
const { DevelopmentModule } = require("./src/modules/development/DevelopmentModule.js");
|
||||
const { HostOSModule } = require("./src/modules/hostos/HostOSModule.js");
|
||||
const { InternetModule } = require("./src/modules/internet/InternetModule.js");
|
||||
const { PuterExecModule } = require("./src/modules/puterexec/PuterExecModule.js");
|
||||
const { MailModule } = require("./src/modules/mail/MailModule.js");
|
||||
const { ConvertModule } = require("./src/modules/convert/ConvertModule.js");
|
||||
|
||||
module.exports = {
|
||||
helloworld: () => {
|
||||
@@ -51,6 +56,7 @@ module.exports = {
|
||||
EssentialModules: [
|
||||
Core2Module,
|
||||
PuterFSModule,
|
||||
HostOSModule,
|
||||
CoreModule,
|
||||
WebModule,
|
||||
TemplateModule,
|
||||
@@ -65,8 +71,13 @@ module.exports = {
|
||||
SelfHostedModule,
|
||||
TestDriversModule,
|
||||
PuterAIModule,
|
||||
PuterExecModule,
|
||||
BroadcastModule,
|
||||
InternetModule,
|
||||
MailModule,
|
||||
ConvertModule,
|
||||
|
||||
// Development modules
|
||||
PerfMonModule,
|
||||
DevelopmentModule,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@heyputer/backend",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.1",
|
||||
"description": "Backend/Kernel for Puter",
|
||||
"main": "exports.js",
|
||||
"scripts": {
|
||||
@@ -10,7 +10,8 @@
|
||||
"@anthropic-ai/sdk": "^0.26.1",
|
||||
"@aws-sdk/client-polly": "^3.622.0",
|
||||
"@aws-sdk/client-textract": "^3.621.0",
|
||||
"@heyputer/kv.js": "^0.1.8",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@heyputer/kv.js": "^0.1.9",
|
||||
"@heyputer/multest": "^0.0.2",
|
||||
"@heyputer/putility": "^1.0.0",
|
||||
"@mistralai/mistralai": "^1.3.4",
|
||||
@@ -31,6 +32,7 @@
|
||||
"clean-css": "^5.3.2",
|
||||
"composite-error": "^1.0.2",
|
||||
"compression": "^1.7.4",
|
||||
"convertapi": "^1.15.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dedent": "^1.5.3",
|
||||
"express": "^4.18.2",
|
||||
|
||||
@@ -360,7 +360,10 @@ const install = async ({ services, app, useapi, modapi }) => {
|
||||
services.registerService('performance-monitor', PerformanceMonitor);
|
||||
|
||||
const { WispService } = require('./services/WispService');
|
||||
services.registerService('wisp', WispService);
|
||||
services.registerService('wisp', WispService);
|
||||
|
||||
const { RequestMeasureService } = require('./services/RequestMeasureService');
|
||||
services.registerService('request-measure', RequestMeasureService);
|
||||
}
|
||||
|
||||
const install_legacy = async ({ services }) => {
|
||||
|
||||
@@ -23,6 +23,7 @@ const { Endpoint } = require("./util/expressutil");
|
||||
const configurable_auth = require("./middleware/configurable_auth");
|
||||
const { Context } = require("./util/context");
|
||||
const { DB_READ, DB_WRITE } = require("./services/database/consts");
|
||||
const { Actor } = require("./services/auth/Actor");
|
||||
|
||||
/**
|
||||
* State shared with the default service and the `extension` global so that
|
||||
@@ -96,6 +97,39 @@ class ExtensionService extends BaseService {
|
||||
|
||||
svc_event.emit(key, data, meta);
|
||||
});
|
||||
|
||||
this.state.extension.kv = (() => {
|
||||
const impls = this.services.get_implementors('puter-kvstore');
|
||||
const impl_kv = impls[0].impl;
|
||||
|
||||
return new Proxy(impl_kv, {
|
||||
get: (target, prop) => {
|
||||
if ( typeof target[prop] !== 'function' ) {
|
||||
return target[prop];
|
||||
}
|
||||
|
||||
return (...args) => {
|
||||
if ( typeof args[0] !== 'object' ) {
|
||||
// Luckily named parameters don't have positional
|
||||
// overlaps between the different kv methods, so
|
||||
// we can just set them all.
|
||||
args[0] = {
|
||||
key: args[0],
|
||||
as: args[0],
|
||||
value: args[1],
|
||||
amount: args[2],
|
||||
timestamp: args[2],
|
||||
ttl: args[2],
|
||||
};
|
||||
}
|
||||
return Context.sub({
|
||||
actor: Actor.get_system_actor(),
|
||||
}).arun(() => target[prop](...args));
|
||||
};
|
||||
},
|
||||
});
|
||||
})();
|
||||
console.log('set kv on', this.state.extension);
|
||||
}
|
||||
|
||||
['__on_install.routes'] (_, { app }) {
|
||||
|
||||
@@ -336,6 +336,9 @@ class Kernel extends AdvancedBase {
|
||||
if ( ! item.endsWith('.js') ) {
|
||||
continue;
|
||||
}
|
||||
if ( item.endsWith('.test.js') ) {
|
||||
continue;
|
||||
}
|
||||
const stat = fs.statSync(path_.join(fullpath, item));
|
||||
if ( ! stat.isFile() ) {
|
||||
continue;
|
||||
|
||||
@@ -57,6 +57,10 @@ module.exports = class APIError {
|
||||
return `offering ${quot(name)} was not recognized.`;
|
||||
},
|
||||
},
|
||||
'error_400_from_delegate': {
|
||||
status: 400,
|
||||
message: ({ delegate, message }) => `Error 400 from delegate ${quot(delegate)}: ${message}`,
|
||||
},
|
||||
// Things
|
||||
'disallowed_thing': {
|
||||
status: 400,
|
||||
|
||||
@@ -25,10 +25,11 @@ let config = {};
|
||||
// Static defaults
|
||||
config.servers = [];
|
||||
|
||||
config.disable_user_signup = false;
|
||||
config.default_user_group = '78b1b1dd-c959-44d2-b02c-8735671f9997';
|
||||
|
||||
// Will disable the auto-generated temp users. If a user lands on the site, they will be required to sign up or log in.
|
||||
config.disable_temp_users = false;
|
||||
|
||||
config.default_user_group = '78b1b1dd-c959-44d2-b02c-8735671f9997';
|
||||
config.default_temp_group = 'b7220104-7905-4985-b996-649fdcdb3c8f';
|
||||
|
||||
config.max_file_size = 100_000_000_000;
|
||||
|
||||
@@ -97,6 +97,7 @@ const hardcoded_user_group_permissions = {
|
||||
'service': {},
|
||||
'feature': {},
|
||||
'kernel-info': {},
|
||||
'local-terminal:access': {},
|
||||
},
|
||||
'b7220104-7905-4985-b996-649fdcdb3c8f': {
|
||||
'service:hello-world:ii:hello-world': policy_perm('temp.es'),
|
||||
|
||||
@@ -181,6 +181,7 @@ class FilesystemService extends BaseService {
|
||||
|
||||
const { _path, uuidv4 } = this.modules;
|
||||
const svc_fsEntry = this.services.get('fsEntryService');
|
||||
const resourceService = this.services.get('resourceService');
|
||||
|
||||
const ts = Math.round(Date.now() / 1000);
|
||||
const uid = uuidv4();
|
||||
|
||||
@@ -331,7 +331,7 @@ class HLMkdir extends HLFilesystemOperation {
|
||||
}
|
||||
this.created = existing;
|
||||
this.used_existing = true;
|
||||
return {};
|
||||
return await this.created.getSafeEntry();
|
||||
} else {
|
||||
throw APIError.create('item_with_same_name_exists', null, {
|
||||
entry_name: target_basename,
|
||||
@@ -386,13 +386,35 @@ class HLMkdir extends HLFilesystemOperation {
|
||||
}
|
||||
|
||||
async _create_parents ({ parent_node }) {
|
||||
const { values } = this;
|
||||
const { context, values } = this;
|
||||
const { _path } = this.modules;
|
||||
|
||||
const fs = context.get('services').get('filesystem');
|
||||
|
||||
// Determine the deepest existing node
|
||||
let deepest_existing = parent_node;
|
||||
let remaining_path = _path.dirname(values.path).split('/').filter(Boolean);
|
||||
{
|
||||
const parts = remaining_path.slice();
|
||||
for (;;) {
|
||||
if ( remaining_path.length === 0 ) {
|
||||
return deepest_existing;
|
||||
}
|
||||
const component = remaining_path[0];
|
||||
const next_selector = new NodeChildSelector(deepest_existing.selector, component);
|
||||
const next_node = await fs.node(next_selector);
|
||||
if ( ! await next_node.exists() ) {
|
||||
break;
|
||||
}
|
||||
deepest_existing = next_node;
|
||||
remaining_path.shift();
|
||||
}
|
||||
}
|
||||
|
||||
const tree_op = new MkTree();
|
||||
await tree_op.run({
|
||||
parent: parent_node,
|
||||
tree: [_path.dirname(values.path)],
|
||||
parent: deepest_existing,
|
||||
tree: [remaining_path.join('/')],
|
||||
});
|
||||
|
||||
this.parent_directories_created = tree_op.directories_created;
|
||||
|
||||
@@ -30,6 +30,11 @@ class HLMove extends HLFilesystemOperation {
|
||||
static MODULES = {
|
||||
_path: require('path'),
|
||||
}
|
||||
|
||||
static PROPERTIES = {
|
||||
parent_directories_created: () => [],
|
||||
}
|
||||
|
||||
async _run () {
|
||||
const { _path } = this.modules;
|
||||
|
||||
@@ -82,6 +87,8 @@ class HLMove extends HLFilesystemOperation {
|
||||
tree: [parent.path],
|
||||
});
|
||||
|
||||
this.parent_directories_created = tree_op.directories_created;
|
||||
|
||||
parent = tree_op.leaves[0];
|
||||
}
|
||||
|
||||
@@ -187,11 +194,19 @@ class HLMove extends HLFilesystemOperation {
|
||||
await source_new.awaitStableEntry();
|
||||
await source_new.fetchSuggestedApps();
|
||||
await source_new.fetchOwner();
|
||||
return {
|
||||
|
||||
const response = {
|
||||
moved: await source_new.getSafeEntry({ thumbnail: true }),
|
||||
overwritten,
|
||||
old_path,
|
||||
}
|
||||
|
||||
response.parent_dirs_created = [];
|
||||
for ( const node of this.parent_directories_created ) {
|
||||
response.parent_dirs_created.push(await node.getSafeEntry());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@ class HLWrite extends HLFilesystemOperation {
|
||||
this.checkpoint('before thumbnail');
|
||||
|
||||
let thumbnail_promise = new TeePromise();
|
||||
if ( await destination.isAppDataDirectory() || values.no_thumbnail ) {
|
||||
if ( await parent.isAppDataDirectory() || values.no_thumbnail ) {
|
||||
thumbnail_promise.resolve(undefined);
|
||||
} else (async () => {
|
||||
const reason = await (async () => {
|
||||
|
||||
@@ -37,13 +37,19 @@ class LLMkdir extends LLFilesystemOperation {
|
||||
this.checkpoint('lock requested');
|
||||
this.log.noticeme('GET FSLOCK');
|
||||
const svc_fslock = context.get('services').get('fslock');
|
||||
this.log.noticeme('REQUESTING LOCK');
|
||||
this.log.noticeme('REQUESTING LOCK', {
|
||||
parent: await parent.get('path'),
|
||||
name,
|
||||
});
|
||||
const lock_handle = await svc_fslock.lock_child(
|
||||
await parent.get('path'),
|
||||
name,
|
||||
MODE_WRITE,
|
||||
);
|
||||
this.log.noticeme('GOT LOCK');
|
||||
this.log.noticeme('GOT LOCK', {
|
||||
parent: await parent.get('path'),
|
||||
name,
|
||||
});
|
||||
this.checkpoint('lock acquired');
|
||||
|
||||
try {
|
||||
|
||||
@@ -24,10 +24,8 @@ class LLMove extends LLFilesystemOperation {
|
||||
}
|
||||
|
||||
async _run () {
|
||||
const { _path } = this.modules;
|
||||
const { context } = this;
|
||||
const { source, parent, user, actor, target_name, metadata } = this.values;
|
||||
const svc = context.get('services');
|
||||
const { source, parent, actor, target_name, metadata } = this.values;
|
||||
|
||||
// Access Control
|
||||
{
|
||||
@@ -45,36 +43,13 @@ class LLMove extends LLFilesystemOperation {
|
||||
}
|
||||
}
|
||||
|
||||
const old_path = await source.get('path');
|
||||
|
||||
const svc_fsEntry = svc.get('fsEntryService');
|
||||
const op_update = await svc_fsEntry.update(source.uid, {
|
||||
...(
|
||||
await source.get('parent_uid') !== await parent.get('uid')
|
||||
? { parent_uid: await parent.get('uid') }
|
||||
: {}
|
||||
),
|
||||
path: _path.join(await parent.get('path'), target_name),
|
||||
name: target_name,
|
||||
...(metadata ? { metadata } : {}),
|
||||
});
|
||||
|
||||
source.entry.name = target_name;
|
||||
source.entry.path = _path.join(await parent.get('path'), target_name);
|
||||
|
||||
await op_update.awaitDone();
|
||||
|
||||
const svc_fs = svc.get('filesystem');
|
||||
await svc_fs.update_child_paths(old_path, source.entry.path, user.id);
|
||||
|
||||
const svc_event = svc.get('event');
|
||||
|
||||
await svc_event.emit('fs.move.file', {
|
||||
await source.provider.move({
|
||||
context: this.context,
|
||||
moved: source,
|
||||
old_path,
|
||||
node: source,
|
||||
new_parent: parent,
|
||||
new_name: target_name,
|
||||
metadata,
|
||||
});
|
||||
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,17 +98,10 @@ class LLRmDir extends LLFilesystemOperation {
|
||||
});
|
||||
}
|
||||
|
||||
if ( ! descendants_only ) {
|
||||
tasks.add(`fs:rm:rm-self`, async () => {
|
||||
const ll_rm = new LLRmNode();
|
||||
await ll_rm.run({
|
||||
target,
|
||||
user,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await tasks.awaitAll();
|
||||
if ( ! descendants_only ) {
|
||||
await target.provider.rmdir({ context, node: target });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,37 +40,7 @@ class LLRmNode extends LLFilesystemOperation {
|
||||
}
|
||||
}
|
||||
|
||||
if ( await target.get('immutable') ) {
|
||||
throw new APIError(403, 'File is immutable.');
|
||||
}
|
||||
|
||||
const svc_size = svc.get('sizeService');
|
||||
const svc_fsEntry = svc.get('fsEntryService');
|
||||
|
||||
svc_size.change_usage(
|
||||
await target.get('user_id'),
|
||||
-1 * await target.get('size')
|
||||
);
|
||||
|
||||
const tracer = svc.get('traceService').tracer;
|
||||
const tasks = new ParallelTasks({ tracer, max: 4 });
|
||||
|
||||
tasks.add(`remove-fsentry`, async () => {
|
||||
await svc_fsEntry.delete(await target.get('uid'));
|
||||
});
|
||||
|
||||
if ( await target.get('has-s3') ) {
|
||||
tasks.add(`remove-from-s3`, async () => {
|
||||
// const storage = new PuterS3StorageStrategy({ services: svc });
|
||||
const storage = Context.get('storage');
|
||||
const state_delete = storage.create_delete();
|
||||
await state_delete.run({
|
||||
node: target,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await tasks.awaitAll();
|
||||
await target.provider.unlink({ context, node: target });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,24 @@ async function is_shared_with(fsentry_id, recipient_user_id){
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if temp_users is disabled and return a boolean
|
||||
* @returns {boolean}
|
||||
*/
|
||||
async function is_temp_users_disabled() {
|
||||
const svc_feature_flag = await services.get("feature-flag");
|
||||
return await svc_feature_flag.check("temp-users-disabled");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if user_signup is disabled and return a boolean
|
||||
* @returns {boolean}
|
||||
*/
|
||||
async function is_user_signup_disabled() {
|
||||
const svc_feature_flag = await services.get("feature-flag");
|
||||
return await svc_feature_flag.check("user-signup-disabled");
|
||||
}
|
||||
|
||||
const chkperm = spanify('chkperm', async (target_fsentry, requester_user_id, action) => {
|
||||
// basic cases where false is the default response
|
||||
if(!target_fsentry)
|
||||
@@ -1661,6 +1679,8 @@ module.exports = {
|
||||
is_valid_uuid4,
|
||||
is_valid_uuid,
|
||||
is_specifically_uuidv4,
|
||||
is_temp_users_disabled,
|
||||
is_user_signup_disabled,
|
||||
is_valid_url,
|
||||
jwt_auth,
|
||||
mv,
|
||||
|
||||
@@ -48,8 +48,11 @@ const configurable_auth = options => async (req, res, next) => {
|
||||
// auth middleware, it makes more sense to put it here.
|
||||
|
||||
let token;
|
||||
// Auth token in body
|
||||
if(req.body && req.body.auth_token)
|
||||
token = req.body.auth_token;
|
||||
// HTTML Auth header
|
||||
if(req.header && req.header('Authorization'))
|
||||
else if(req.header && req.header('Authorization'))
|
||||
token = req.header('Authorization');
|
||||
// Cookie
|
||||
else if(req.cookies && req.cookies[config.cookie_name])
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
const express = require('express');
|
||||
const { PassThrough } = require('stream');
|
||||
const { pausing_tee } = require('../util/streamutil');
|
||||
const putility = require('@heyputer/putility');
|
||||
|
||||
const _intercept_req = ({ data, req }) => {
|
||||
if ( ! req.readable ) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const [req_monitor, req_pass] = pausing_tee(req, 2);
|
||||
|
||||
req_monitor.on('data', (chunk) => {
|
||||
data.sz_incoming += chunk.length;
|
||||
});
|
||||
|
||||
const replaces = ['readable', 'pipe', 'on', 'once', 'removeListener'];
|
||||
for ( const replace of replaces ) {
|
||||
const replacement = req_pass[replace]
|
||||
Object.defineProperty(req, replace, {
|
||||
get () {
|
||||
if ( typeof replacement === 'function' ) {
|
||||
return replacement.bind(req_pass);
|
||||
}
|
||||
return replacement;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return next();
|
||||
}
|
||||
};
|
||||
|
||||
const _intercept_res = ({ data, res }) => {
|
||||
if ( ! res.writable ) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const org_write = res.write;
|
||||
const org_end = res.end;
|
||||
|
||||
// Override the `write` method
|
||||
res.write = function (chunk, ...args) {
|
||||
if (Buffer.isBuffer(chunk)) {
|
||||
data.sz_outgoing += chunk.length;
|
||||
} else if (typeof chunk === 'string') {
|
||||
data.sz_outgoing += Buffer.byteLength(chunk);
|
||||
}
|
||||
return org_write.apply(res, [chunk, ...args]);
|
||||
};
|
||||
|
||||
// Override the `end` method
|
||||
res.end = function (chunk, ...args) {
|
||||
if (chunk) {
|
||||
if (Buffer.isBuffer(chunk)) {
|
||||
data.sz_outgoing += chunk.length;
|
||||
} else if (typeof chunk === 'string') {
|
||||
data.sz_outgoing += Buffer.byteLength(chunk);
|
||||
}
|
||||
}
|
||||
const result = org_end.apply(res, [chunk, ...args]);
|
||||
return result;
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return next();
|
||||
}
|
||||
};
|
||||
|
||||
function measure () {
|
||||
return async (req, res, next) => {
|
||||
const data = {
|
||||
sz_incoming: 0,
|
||||
sz_outgoing: 0,
|
||||
};
|
||||
|
||||
_intercept_req({ data, req });
|
||||
_intercept_res({ data, res });
|
||||
|
||||
req.measurements = new putility.libs.promise.TeePromise();
|
||||
|
||||
// Wait for the request to finish processing
|
||||
res.on('finish', () => {
|
||||
req.measurements.resolve(data);
|
||||
// console.log(`Incoming Data: ${data.sz_incoming} bytes`);
|
||||
// console.log(`Outgoing Data: ${data.sz_outgoing} bytes`); // future
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = measure;
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// const { HLWrite } = require("../filesystem/hl_operations/hl_write");
|
||||
const { HLWrite } = require("../../filesystem/hl_operations/hl_write");
|
||||
const { LLMkdir } = require("../../filesystem/ll_operations/ll_mkdir");
|
||||
const { LLRead } = require("../../filesystem/ll_operations/ll_read");
|
||||
const { NodePathSelector } = require("../../filesystem/node/selectors");
|
||||
@@ -145,12 +145,11 @@ class AppIconService extends BaseService {
|
||||
|
||||
const get_fallback_icon = async () => {
|
||||
// Use database-stored icon as a fallback
|
||||
app_icon = app_icon ?? await (async () => {
|
||||
app_icon = app_icon || await (async () => {
|
||||
const app = await get_app({ uid: app_uid });
|
||||
return app.icon ?? DEFAULT_APP_ICON;
|
||||
return app.icon || DEFAULT_APP_ICON;
|
||||
})()
|
||||
const [metadata, base64] = app_icon.split(',');
|
||||
console.log('METADATA', metadata);
|
||||
const mime = metadata.split(';')[0].split(':')[1];
|
||||
const img = Buffer.from(base64, 'base64');
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
const { HLWrite } = require("../../filesystem/hl_operations/hl_write");
|
||||
const { LLRead } = require("../../filesystem/ll_operations/ll_read");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const { TypedValue } = require("../../services/drivers/meta/Runtime");
|
||||
const { Context } = require("../../util/context");
|
||||
|
||||
const mime = require('mime-types');
|
||||
|
||||
class ConvertAPIService extends BaseService {
|
||||
async ['__on_driver.register.interfaces'] () {
|
||||
const svc_registry = this.services.get('registry');
|
||||
const col_interfaces = svc_registry.get('interfaces');
|
||||
|
||||
col_interfaces.set('convert-files', {
|
||||
description: 'Convert between various data formats.',
|
||||
methods: {
|
||||
convert: {
|
||||
description: 'Convert data from one format to another.',
|
||||
parameters: {
|
||||
from: {
|
||||
type: 'string',
|
||||
description: 'Source data format.',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
description: 'Destination data format.',
|
||||
required: true,
|
||||
},
|
||||
source: {
|
||||
type: 'file',
|
||||
required: true,
|
||||
},
|
||||
|
||||
// File output mode
|
||||
dest: {
|
||||
type: 'file',
|
||||
},
|
||||
overwrite: { type: 'flag' },
|
||||
dedupe_name: { type: 'flag' },
|
||||
},
|
||||
result_choices: [
|
||||
{
|
||||
type: {
|
||||
$: 'stream',
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static MODULES = {
|
||||
axios: require('axios'),
|
||||
convertapi: require('convertapi'),
|
||||
}
|
||||
|
||||
async _init () {
|
||||
this.convertapi = this.require('convertapi')(this.config.token);
|
||||
}
|
||||
|
||||
static IMPLEMENTS = {
|
||||
['convert-files']: {
|
||||
async convert ({
|
||||
// Require parameters
|
||||
from, to, source,
|
||||
|
||||
// File Output Mode
|
||||
dest, overwrite, dedupe_name,
|
||||
}) {
|
||||
const convertapi = this.convertapi;
|
||||
const axios = this.require('axios');
|
||||
|
||||
const fsNode = await source.get('fs-node');
|
||||
const ll_read = new LLRead();
|
||||
const stream = await ll_read.run({
|
||||
actor: Context.get('actor'),
|
||||
fsNode,
|
||||
});
|
||||
|
||||
const name = await fsNode.get('name');
|
||||
|
||||
const uploadResult =
|
||||
await convertapi.upload(stream, name);
|
||||
|
||||
const convertResult =
|
||||
await convertapi.convert(to, { File: uploadResult },
|
||||
...( from ? [ from ] : [] ));
|
||||
|
||||
const fileInfo = convertResult?.response?.Files?.[0];
|
||||
|
||||
const downloadResponse = await axios.get(fileInfo.Url, {
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
if ( dest !== undefined ) {
|
||||
const hl_write = new HLWrite();
|
||||
return await hl_write.run({
|
||||
destination_or_parent: await dest.get('fs-node'),
|
||||
fallback_name: fileInfo.FileName,
|
||||
overwrite,
|
||||
dedupe_name,
|
||||
file: {
|
||||
originalname: fileInfo.FileName,
|
||||
stream: downloadResponse.data,
|
||||
size: fileInfo.FileSize,
|
||||
},
|
||||
actor: Context.get('actor'),
|
||||
});
|
||||
}
|
||||
|
||||
return new TypedValue({
|
||||
$: 'stream',
|
||||
content_type: mime.contentType(fileInfo.FileName),
|
||||
}, downloadResponse.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = ConvertAPIService;
|
||||
@@ -0,0 +1,12 @@
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
|
||||
class ConvertModule extends AdvancedBase {
|
||||
async install (context) {
|
||||
const services = context.get('services');
|
||||
|
||||
const ConvertAPIService = require('./ConvertAPIService');
|
||||
services.registerService('convert-api', ConvertAPIService);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ConvertModule };
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter 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/>.
|
||||
*/
|
||||
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
|
||||
/**
|
||||
* Enable this module when you want performance monitoring.
|
||||
*
|
||||
* Performance monitoring requires additional setup. Jaegar should be installed
|
||||
* and running.
|
||||
*/
|
||||
class DevelopmentModule extends AdvancedBase {
|
||||
async install (context) {
|
||||
const services = context.get('services');
|
||||
|
||||
const LocalTerminalService = require("./LocalTerminalService");
|
||||
services.registerService('local-terminal', LocalTerminalService);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DevelopmentModule,
|
||||
};
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter 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/>.
|
||||
*/
|
||||
|
||||
const { spawn } = require("child_process");
|
||||
const APIError = require("../../api/APIError");
|
||||
const configurable_auth = require("../../middleware/configurable_auth");
|
||||
const { Endpoint } = require("../../util/expressutil");
|
||||
|
||||
|
||||
const PERM_LOCAL_TERMINAL = 'local-terminal:access';
|
||||
|
||||
const path_ = require('path');
|
||||
const { Actor } = require("../../services/auth/Actor");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const { Context } = require("../../util/context");
|
||||
|
||||
class LocalTerminalService extends BaseService {
|
||||
_construct () {
|
||||
this.sessions_ = {};
|
||||
}
|
||||
get_profiles () {
|
||||
return {
|
||||
['api-test']: {
|
||||
cwd: path_.join(
|
||||
__dirname,
|
||||
'../../../../../',
|
||||
'tools/api-tester',
|
||||
),
|
||||
shell: [
|
||||
'/usr/bin/env', 'node',
|
||||
'apitest.js',
|
||||
'--config=config.yml'
|
||||
],
|
||||
allow_args: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
['__on_install.routes'] (_, { app }) {
|
||||
const r_group = (() => {
|
||||
const require = this.require;
|
||||
const express = require('express');
|
||||
return express.Router()
|
||||
})();
|
||||
app.use('/local-terminal', r_group);
|
||||
|
||||
Endpoint({
|
||||
route: '/new',
|
||||
methods: ['POST'],
|
||||
mw: [configurable_auth()],
|
||||
handler: async (req, res) => {
|
||||
const term_uuid = require('uuid').v4();
|
||||
|
||||
const svc_permission = this.services.get('permission');
|
||||
const actor = Context.get('actor');
|
||||
const can_access = actor &&
|
||||
await svc_permission.check(actor, PERM_LOCAL_TERMINAL);
|
||||
|
||||
if ( ! can_access ) {
|
||||
throw APIError.create('permission_denied', null, {
|
||||
permission: PERM_LOCAL_TERMINAL,
|
||||
});
|
||||
}
|
||||
|
||||
const profiles = this.get_profiles();
|
||||
if ( ! profiles[req.body.profile] ) {
|
||||
throw APIError.create('invalid_profile', null, {
|
||||
profile: req.body.profile,
|
||||
});
|
||||
}
|
||||
|
||||
const profile = profiles[req.body.profile];
|
||||
|
||||
const args = profile.shell.slice(1);
|
||||
if ( profile.allow_args && req.body.args ) {
|
||||
args.push(...req.body.args);
|
||||
}
|
||||
const proc = spawn(profile.shell[0], args, {
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
...(profile.env ?? {}),
|
||||
},
|
||||
cwd: profile.cwd,
|
||||
});
|
||||
|
||||
console.log('process??', proc);
|
||||
|
||||
// stdout to websocket
|
||||
{
|
||||
const svc_socketio = req.services.get('socketio');
|
||||
proc.stdout.on('data', data => {
|
||||
const base64 = data.toString('base64');
|
||||
console.log('---------------------- CHUNK?', base64);
|
||||
svc_socketio.send(
|
||||
{ room: req.user.id },
|
||||
'local-terminal.stdout',
|
||||
{
|
||||
term_uuid,
|
||||
base64,
|
||||
},
|
||||
);
|
||||
});
|
||||
proc.stderr.on('data', data => {
|
||||
const base64 = data.toString('base64');
|
||||
console.log('---------------------- CHUNK?', base64);
|
||||
svc_socketio.send(
|
||||
{ room: req.user.id },
|
||||
'local-terminal.stderr',
|
||||
{
|
||||
term_uuid,
|
||||
base64,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
proc.on('exit', () => {
|
||||
this.log.noticeme(`[${term_uuid}] Process exited (${proc.exitCode})`);
|
||||
delete this.sessions_[term_uuid];
|
||||
|
||||
const svc_socketio = req.services.get('socketio');
|
||||
svc_socketio.send(
|
||||
{ room: req.user.id },
|
||||
'local-terminal.exit',
|
||||
{
|
||||
term_uuid,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
this.sessions_[term_uuid] = {
|
||||
uuid: term_uuid,
|
||||
proc,
|
||||
};
|
||||
|
||||
res.json({ term_uuid });
|
||||
},
|
||||
}).attach(r_group);
|
||||
}
|
||||
async _init () {
|
||||
const svc_event = this.services.get('event');
|
||||
svc_event.on('web.socket.user-connected', async (_, {
|
||||
socket,
|
||||
user,
|
||||
}) => {
|
||||
const svc_permission = this.services.get('permission');
|
||||
const actor = Actor.adapt(user);
|
||||
const can_access = actor &&
|
||||
await svc_permission.check(actor, PERM_LOCAL_TERMINAL);
|
||||
|
||||
if ( ! can_access ) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.on('local-terminal.stdin', async msg => {
|
||||
console.log('local term message', msg);
|
||||
|
||||
const session = this.sessions_[msg.term_uuid];
|
||||
if ( ! session ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const base64 = Buffer.from(msg.data, 'base64');
|
||||
session.proc.stdin.write(base64);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LocalTerminalService;
|
||||
@@ -0,0 +1,21 @@
|
||||
## Mountpounts hurdles
|
||||
|
||||
- [ ] subdomains use integer IDs to to reference files, which
|
||||
only works with PuterFS. This means other filesystem
|
||||
providers will not be usable for subdomains.
|
||||
|
||||
Possible solutions:
|
||||
- GUI logic to disable subdomains feature for other providers
|
||||
- Add a new column to associate subdomains with paths
|
||||
- Map non-puterfs nodes to (1B + path_id), where path_id is
|
||||
a numeric identifier that is associated with the path, and
|
||||
the association is stored in the database or system runtime
|
||||
directory.
|
||||
|
||||
- [ ] permissions are associated with UUIDs, but will need to
|
||||
be able to be associated with paths instead for non-puterfs
|
||||
mountpoints.
|
||||
|
||||
- Make path-to-uuid re-writer act on puter-fs only.
|
||||
- ACL needs to be able to check path-based permissions
|
||||
on non-puterfs mountpoints.
|
||||
@@ -0,0 +1,14 @@
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
|
||||
class HostOSModule extends AdvancedBase {
|
||||
async install (context) {
|
||||
const services = context.get('services');
|
||||
|
||||
const ProcessService = require('./ProcessService');
|
||||
services.registerService('process', ProcessService);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HostOSModule,
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
const BaseService = require("../../services/BaseService");
|
||||
|
||||
class ProxyLogger {
|
||||
constructor (log) {
|
||||
this.log = log;
|
||||
}
|
||||
attach (stream) {
|
||||
let buffer = '';
|
||||
stream.on('data', (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
let lineEndIndex = buffer.indexOf('\n');
|
||||
while (lineEndIndex !== -1) {
|
||||
const line = buffer.substring(0, lineEndIndex);
|
||||
this.log(line);
|
||||
buffer = buffer.substring(lineEndIndex + 1);
|
||||
lineEndIndex = buffer.indexOf('\n');
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
if (buffer.length) {
|
||||
this.log(buffer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ProcessService extends BaseService {
|
||||
static MODULES = {
|
||||
path: require('path'),
|
||||
spawn: require('child_process').spawn,
|
||||
};
|
||||
|
||||
_construct () {
|
||||
this.instances = [];
|
||||
}
|
||||
|
||||
async _init (args) {
|
||||
this.args = args;
|
||||
|
||||
process.on('exit', () => {
|
||||
this.exit_all_();
|
||||
})
|
||||
}
|
||||
|
||||
log_ (name, isErr, line) {
|
||||
let txt = `[${name}:`;
|
||||
txt += isErr
|
||||
? `\x1B[34;1m2\x1B[0m`
|
||||
: `\x1B[32;1m1\x1B[0m`;
|
||||
txt += '] ' + line;
|
||||
this.log.info(txt);
|
||||
}
|
||||
|
||||
async exit_all_ () {
|
||||
for ( const { proc } of this.instances ) {
|
||||
proc.kill();
|
||||
}
|
||||
}
|
||||
|
||||
async start ({ name, fullpath, command, args, env }) {
|
||||
this.log.info(`Starting ${name} in ${fullpath}`);
|
||||
const env_processed = { ...(env ?? {}) };
|
||||
for ( const k in env_processed ) {
|
||||
if ( typeof env_processed[k] !== 'function' ) continue;
|
||||
env_processed[k] = env_processed[k]({
|
||||
global_config: this.global_config
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
'command',
|
||||
command,
|
||||
...args
|
||||
)
|
||||
const proc = this.modules.spawn(command, args, {
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
...env_processed,
|
||||
},
|
||||
cwd: fullpath,
|
||||
});
|
||||
this.instances.push({
|
||||
name, proc,
|
||||
});
|
||||
const out = new ProxyLogger((line) => this.log_(name, false, line));
|
||||
out.attach(proc.stdout);
|
||||
const err = new ProxyLogger((line) => this.log_(name, true, line));
|
||||
err.attach(proc.stderr);
|
||||
proc.on('exit', () => {
|
||||
this.log.info(`[${name}:exit] Process exited (${proc.exitCode})`);
|
||||
this.instances = this.instances.filter((inst) => inst.proc !== proc);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProcessService;
|
||||
@@ -0,0 +1,16 @@
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
const config = require("../../config.js");
|
||||
|
||||
class InternetModule extends AdvancedBase {
|
||||
async install (context) {
|
||||
const services = context.get('services');
|
||||
|
||||
if ( !! config?.services?.['wisp-relay'] ) {
|
||||
const WispRelayService = require('./WispRelayService.js');
|
||||
services.registerService('wisp-relay', WispRelayService);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { InternetModule };
|
||||
@@ -0,0 +1,20 @@
|
||||
const BaseService = require("../../services/BaseService");
|
||||
|
||||
class WispRelayService extends BaseService {
|
||||
_init () {
|
||||
const path_ = require('path');
|
||||
const svc_process = this.services.get('process');
|
||||
svc_process.start({
|
||||
name: 'internet.js',
|
||||
command: this.config.node_path,
|
||||
fullpath: this.config.wisp_relay_path,
|
||||
args: ['index.js'],
|
||||
env: {
|
||||
PORT: this.config.wisp_relay_port,
|
||||
WISP_AUTH_SERVER: this.config.origin,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WispRelayService;
|
||||
@@ -0,0 +1,14 @@
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
|
||||
class MailModule extends AdvancedBase {
|
||||
async install (context) {
|
||||
const services = context.get('services');
|
||||
|
||||
const { UserSendMailService } = require('./UserSendMailService');
|
||||
services.registerService('user-send-mail', UserSendMailService);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MailModule,
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
const APIError = require("../../api/APIError");
|
||||
const { LLRead } = require("../../filesystem/ll_operations/ll_read");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const { Context } = require("../../util/context");
|
||||
const validator = require('validator')
|
||||
|
||||
class UserSendMailService extends BaseService {
|
||||
async ['__on_driver.register.interfaces'] () {
|
||||
const svc_registry = this.services.get('registry');
|
||||
const col_interfaces = svc_registry.get('interfaces');
|
||||
|
||||
col_interfaces.set('puter-send-mail', {
|
||||
description: 'Send an email.',
|
||||
methods: {
|
||||
send: {
|
||||
description: 'Recognize text in an image or document.',
|
||||
parameters: {
|
||||
to: {
|
||||
type: 'string',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
},
|
||||
body: {
|
||||
type: 'string',
|
||||
},
|
||||
encoding: {
|
||||
type: 'string',
|
||||
},
|
||||
attachments: {
|
||||
type: 'json',
|
||||
}
|
||||
},
|
||||
result: { type: 'json' },
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
static IMPLEMENTS = {
|
||||
'puter-send-mail': {
|
||||
async send ({
|
||||
to, subject, body,
|
||||
encoding,
|
||||
attachments = [],
|
||||
}) {
|
||||
const actor = Context.get('actor');
|
||||
const svc_email = this.services.get('email');
|
||||
|
||||
if ( ! actor.type.user ) {
|
||||
throw new Error('Only users can send email.');
|
||||
}
|
||||
const user = actor.type.user;
|
||||
|
||||
encoding = encoding ?? 'html';
|
||||
if ( ! ['html', 'text'].includes(encoding) ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'encoding',
|
||||
expected: 'html or text',
|
||||
got: encoding,
|
||||
});
|
||||
}
|
||||
|
||||
if ( ! validator.isEmail(to) ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'to',
|
||||
expected: 'a valid email address',
|
||||
got: to,
|
||||
});
|
||||
}
|
||||
|
||||
// We're going to disallow subject lines over or including 998,
|
||||
// as nobody would ever do this unless they're trying to
|
||||
// exploit a faulty email client.
|
||||
if ( subject.length >= 998 ) {
|
||||
throw APIError.create('field_too_long', null, {
|
||||
key: 'subject',
|
||||
max_length: 997,
|
||||
});
|
||||
}
|
||||
|
||||
const transporter = svc_email.get_transport_();
|
||||
const o = {
|
||||
from: `${user.username}@${this.config.domain}`, // sender address
|
||||
to,
|
||||
subject,
|
||||
[encoding === 'html' ? 'html' : 'text']: body,
|
||||
};
|
||||
|
||||
for ( const attachment of attachments ) {
|
||||
if ( attachment.path ) {
|
||||
const svc_fs = this.services.get('filesystem');
|
||||
const node = await svc_fs.node(attachment.path);
|
||||
const ll_read = new LLRead();
|
||||
const stream = await ll_read.run({
|
||||
actor: Context.get('actor'),
|
||||
fsNode: node,
|
||||
});
|
||||
attachment.content = stream;
|
||||
delete attachment.path;
|
||||
}
|
||||
}
|
||||
|
||||
if ( attachments.length > 0 ) {
|
||||
o.attachments = attachments;
|
||||
}
|
||||
|
||||
await transporter.sendMail(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UserSendMailService,
|
||||
};
|
||||
@@ -18,7 +18,9 @@
|
||||
*/
|
||||
|
||||
// METADATA // {"ai-commented":{"service":"claude"}}
|
||||
const { PassThrough } = require("stream");
|
||||
const APIError = require("../../api/APIError");
|
||||
const config = require("../../config");
|
||||
const { PermissionUtil } = require("../../services/auth/PermissionService");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const { DB_WRITE } = require("../../services/database/consts");
|
||||
@@ -26,6 +28,9 @@ const { TypeSpec } = require("../../services/drivers/meta/Construct");
|
||||
const { TypedValue } = require("../../services/drivers/meta/Runtime");
|
||||
const { Context } = require("../../util/context");
|
||||
const { AsModeration } = require("./lib/AsModeration");
|
||||
const FunctionCalling = require("./lib/FunctionCalling");
|
||||
const Messages = require("./lib/Messages");
|
||||
const Streaming = require("./lib/Streaming");
|
||||
|
||||
// Maximum number of fallback attempts when a model fails, including the first attempt
|
||||
const MAX_FALLBACKS = 3 + 1; // includes first attempt
|
||||
@@ -80,37 +85,60 @@ class AIChatService extends BaseService {
|
||||
app_id: details.actor?.type?.app?.id ?? null,
|
||||
service_name: details.service_used,
|
||||
model_name: details.model_used,
|
||||
value_uint_1: details.usage?.input_tokens,
|
||||
value_uint_2: details.usage?.output_tokens,
|
||||
};
|
||||
|
||||
let model_details = this.detail_model_map[details.model_used];
|
||||
if ( Array.isArray(model_details) ) {
|
||||
for ( const model of model_details ) {
|
||||
if ( model.provider === details.service_used ) {
|
||||
model_details = model;
|
||||
break;
|
||||
// New format
|
||||
if ( Array.isArray(details.usage) ) {
|
||||
values.cost = details.usage.reduce((acc, u) => {
|
||||
return acc + u.cost;
|
||||
}, 0);
|
||||
} else {
|
||||
values.value_uint_1 = details.usage?.input_tokens;
|
||||
values.value_uint_2 = details.usage?.output_tokens;
|
||||
|
||||
let model_details = this.detail_model_map[details.model_used];
|
||||
if ( Array.isArray(model_details) ) {
|
||||
for ( const model of model_details ) {
|
||||
if ( model.provider === details.service_used ) {
|
||||
model_details = model;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( Array.isArray(model_details) ) {
|
||||
model_details = model_details[0];
|
||||
}
|
||||
if ( model_details ) {
|
||||
values.cost = 0 + // for formatting
|
||||
if ( Array.isArray(model_details) ) {
|
||||
model_details = model_details[0];
|
||||
}
|
||||
if ( model_details ) {
|
||||
values.cost = 0 + // for formatting
|
||||
|
||||
model_details.cost.input * details.usage.input_tokens
|
||||
// cents/MTok tokens
|
||||
+
|
||||
model_details.cost.input * details.usage.input_tokens
|
||||
// cents/MTok tokens
|
||||
+
|
||||
|
||||
model_details.cost.output * details.usage.output_tokens
|
||||
// cents/MTok tokens
|
||||
;
|
||||
} else {
|
||||
this.log.error('could not find model details', { details });
|
||||
model_details.cost.output * details.usage.output_tokens
|
||||
// cents/MTok tokens
|
||||
;
|
||||
} else {
|
||||
this.log.error('could not find model details', { details });
|
||||
}
|
||||
}
|
||||
|
||||
this.log.noticeme('USAGE INFO', { usage: details.usage });
|
||||
this.log.noticeme('COST INFO', values);
|
||||
|
||||
|
||||
await this.db.insert('ai_usage', values);
|
||||
|
||||
// USD cost from microcents
|
||||
const cost_usc = values.cost / 1000000;
|
||||
const cost_usd = cost_usc / 100;
|
||||
|
||||
// Add to TrackSpendingService
|
||||
const svc_spending = this.services.get('spending');
|
||||
svc_spending.record_cost(`${details.service_used}:chat-completion`, {
|
||||
timestamp: Date.now(),
|
||||
cost: cost_usd,
|
||||
});
|
||||
});
|
||||
|
||||
const svc_apiErrpr = this.services.get('api-error');
|
||||
@@ -339,6 +367,11 @@ class AIChatService extends BaseService {
|
||||
test_mode = true;
|
||||
}
|
||||
|
||||
if ( parameters.messages ) {
|
||||
parameters.messages =
|
||||
Messages.normalize_messages(parameters.messages);
|
||||
}
|
||||
|
||||
if ( ! test_mode && ! await this.moderate(parameters) ) {
|
||||
test_mode = true;
|
||||
}
|
||||
@@ -354,10 +387,14 @@ class AIChatService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if ( parameters.tools ) {
|
||||
FunctionCalling.normalize_tools_object(parameters.tools);
|
||||
}
|
||||
|
||||
if ( intended_service === this.service_name ) {
|
||||
throw new Error('Calling ai-chat directly is not yet supported');
|
||||
}
|
||||
|
||||
|
||||
const svc_driver = this.services.get('driver');
|
||||
let ret, error;
|
||||
let service_used = intended_service;
|
||||
@@ -373,6 +410,7 @@ class AIChatService extends BaseService {
|
||||
ret = await svc_driver.call_new_({
|
||||
actor: Context.get('actor'),
|
||||
service_name: intended_service,
|
||||
skip_usage: true,
|
||||
iface: 'puter-chat-completion',
|
||||
method: 'complete',
|
||||
args: parameters,
|
||||
@@ -385,7 +423,40 @@ class AIChatService extends BaseService {
|
||||
tried.push(model);
|
||||
|
||||
error = e;
|
||||
|
||||
// Distinguishing between user errors and service errors
|
||||
// is very messy because of different conventions between
|
||||
// services. This is a best-effort attempt to catch user
|
||||
// errors and throw them as 400s.
|
||||
const is_request_error = (() => {
|
||||
if ( e instanceof APIError ) {
|
||||
return true;
|
||||
}
|
||||
if ( e.type === 'invalid_request_error' ) {
|
||||
return true;
|
||||
}
|
||||
let some_error = e;
|
||||
while ( some_error ) {
|
||||
if ( some_error.type === 'invalid_request_error' ) {
|
||||
return true;
|
||||
}
|
||||
some_error = some_error.error ?? some_error.cause;
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
if ( is_request_error ) {
|
||||
throw APIError.create('error_400_from_delegate', null, {
|
||||
delegate: intended_service,
|
||||
message: e.message,
|
||||
})
|
||||
}
|
||||
console.error(e);
|
||||
|
||||
if ( config.disable_fallback_mechanisms ) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.log.error('error calling service', {
|
||||
intended_service,
|
||||
model,
|
||||
@@ -420,6 +491,7 @@ class AIChatService extends BaseService {
|
||||
ret = await svc_driver.call_new_({
|
||||
actor: Context.get('actor'),
|
||||
service_name: fallback_service_name,
|
||||
skip_usage: true,
|
||||
iface: 'puter-chat-completion',
|
||||
method: 'complete',
|
||||
args: {
|
||||
@@ -468,6 +540,24 @@ class AIChatService extends BaseService {
|
||||
usage,
|
||||
});
|
||||
})();
|
||||
|
||||
if ( ret.result.value.init_chat_stream ) {
|
||||
const stream = new PassThrough();
|
||||
const retval = new TypedValue({
|
||||
$: 'stream',
|
||||
content_type: 'application/x-ndjson',
|
||||
chunked: true,
|
||||
}, stream);
|
||||
|
||||
const chatStream = new Streaming.AIChatStream({
|
||||
stream,
|
||||
});
|
||||
|
||||
ret.result.value.init_chat_stream({ chatStream });
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
return ret.result.value.response;
|
||||
} else {
|
||||
await svc_event.emit('ai.prompt.report-usage', {
|
||||
@@ -489,6 +579,17 @@ class AIChatService extends BaseService {
|
||||
service_used,
|
||||
});
|
||||
|
||||
|
||||
if ( parameters.response?.normalize ) {
|
||||
ret.result.message =
|
||||
Messages.normalize_single_message(ret.result.message);
|
||||
ret.result = {
|
||||
message: ret.result.message,
|
||||
via_ai_chat_service: true,
|
||||
normalized: true,
|
||||
};
|
||||
}
|
||||
|
||||
return ret.result;
|
||||
}
|
||||
}
|
||||
@@ -551,6 +652,10 @@ class AIChatService extends BaseService {
|
||||
async moderate ({ messages }) {
|
||||
for ( const msg of messages ) {
|
||||
const texts = [];
|
||||
|
||||
// Function calls have no content
|
||||
if ( msg.content === null ) continue;
|
||||
|
||||
if ( typeof msg.content === 'string' ) texts.push(msg.content);
|
||||
else if ( typeof msg.content === 'object' ) {
|
||||
if ( Array.isArray(msg.content) ) {
|
||||
|
||||
@@ -75,8 +75,10 @@ class AIInterfaceService extends BaseService {
|
||||
description: 'Get completions for a chat log.',
|
||||
parameters: {
|
||||
messages: { type: 'json' },
|
||||
tools: { type: 'json' },
|
||||
vision: { type: 'flag' },
|
||||
stream: { type: 'flag' },
|
||||
response: { type: 'json' },
|
||||
model: { type: 'string' },
|
||||
},
|
||||
result: { type: 'json' },
|
||||
|
||||
@@ -23,6 +23,8 @@ const BaseService = require("../../services/BaseService");
|
||||
const { whatis } = require("../../util/langutil");
|
||||
const { PassThrough } = require("stream");
|
||||
const { TypedValue } = require("../../services/drivers/meta/Runtime");
|
||||
const FunctionCalling = require("./lib/FunctionCalling");
|
||||
const Messages = require("./lib/Messages");
|
||||
const { TeePromise } = require('@heyputer/putility').libs.promise;
|
||||
|
||||
const PUTER_PROMPT = `
|
||||
@@ -114,60 +116,29 @@ class ClaudeService extends BaseService {
|
||||
* @param {string} [options.model] - The Claude model to use, defaults to service default
|
||||
* @returns {TypedValue|Object} Returns either a TypedValue with streaming response or a completion object
|
||||
*/
|
||||
async complete ({ messages, stream, model }) {
|
||||
const adapted_messages = [];
|
||||
async complete ({ messages, stream, model, tools }) {
|
||||
tools = FunctionCalling.make_claude_tools(tools);
|
||||
|
||||
const system_prompts = [];
|
||||
let previous_was_user = false;
|
||||
for ( const message of messages ) {
|
||||
if ( typeof message.content === 'string' ) {
|
||||
message.content = {
|
||||
type: 'text',
|
||||
text: message.content,
|
||||
};
|
||||
}
|
||||
if ( whatis(message.content) !== 'array' ) {
|
||||
message.content = [message.content];
|
||||
}
|
||||
if ( ! message.role ) message.role = 'user';
|
||||
if ( message.role === 'user' && previous_was_user ) {
|
||||
const last_msg = adapted_messages[adapted_messages.length-1];
|
||||
last_msg.content.push(
|
||||
...(Array.isArray ? message.content : [message.content])
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ( message.role === 'system' ) {
|
||||
system_prompts.push(...message.content);
|
||||
continue;
|
||||
}
|
||||
adapted_messages.push(message);
|
||||
if ( message.role === 'user' ) {
|
||||
previous_was_user = true;
|
||||
} else {
|
||||
previous_was_user = false;
|
||||
}
|
||||
}
|
||||
let system_prompts;
|
||||
[system_prompts, messages] = Messages.extract_and_remove_system_messages(messages);
|
||||
|
||||
if ( stream ) {
|
||||
let usage_promise = new TeePromise();
|
||||
|
||||
const stream = new PassThrough();
|
||||
const retval = new TypedValue({
|
||||
$: 'stream',
|
||||
content_type: 'application/x-ndjson',
|
||||
chunked: true,
|
||||
}, stream);
|
||||
(async () => {
|
||||
const init_chat_stream = async ({ chatStream }) => {
|
||||
const completion = await this.anthropic.messages.stream({
|
||||
model: model ?? this.get_default_model(),
|
||||
max_tokens: (model === 'claude-3-5-sonnet-20241022' || model === 'claude-3-5-sonnet-20240620') ? 8192 : 4096,
|
||||
temperature: 0,
|
||||
system: PUTER_PROMPT + JSON.stringify(system_prompts),
|
||||
messages: adapted_messages,
|
||||
messages,
|
||||
...(tools ? { tools } : {}),
|
||||
});
|
||||
const counts = { input_tokens: 0, output_tokens: 0 };
|
||||
|
||||
let message, contentBlock;
|
||||
for await ( const event of completion ) {
|
||||
// console.log('EVENT', event);
|
||||
const input_tokens =
|
||||
(event?.usage ?? event?.message?.usage)?.input_tokens;
|
||||
const output_tokens =
|
||||
@@ -176,22 +147,55 @@ class ClaudeService extends BaseService {
|
||||
if ( input_tokens ) counts.input_tokens += input_tokens;
|
||||
if ( output_tokens ) counts.output_tokens += output_tokens;
|
||||
|
||||
if (
|
||||
event.type !== 'content_block_delta' ||
|
||||
event.delta.type !== 'text_delta'
|
||||
) continue;
|
||||
const str = JSON.stringify({
|
||||
text: event.delta.text,
|
||||
});
|
||||
stream.write(str + '\n');
|
||||
if ( event.type === 'message_start' ) {
|
||||
message = chatStream.message();
|
||||
continue;
|
||||
}
|
||||
if ( event.type === 'message_stop' ) {
|
||||
message.end();
|
||||
message = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( event.type === 'content_block_start' ) {
|
||||
if ( event.content_block.type === 'tool_use' ) {
|
||||
contentBlock = message.contentBlock({
|
||||
type: event.content_block.type,
|
||||
id: event.content_block.id,
|
||||
name: event.content_block.name,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
contentBlock = message.contentBlock({
|
||||
type: event.content_block.type,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( event.type === 'content_block_stop' ) {
|
||||
contentBlock.end();
|
||||
contentBlock = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( event.type === 'content_block_delta' ) {
|
||||
if ( event.delta.type === 'input_json_delta' ) {
|
||||
contentBlock.addPartialJSON(event.delta.partial_json);
|
||||
continue;
|
||||
}
|
||||
if ( event.delta.type === 'text_delta' ) {
|
||||
contentBlock.addText(event.delta.text);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
stream.end();
|
||||
chatStream.end();
|
||||
usage_promise.resolve(counts);
|
||||
})();
|
||||
};
|
||||
|
||||
return new TypedValue({ $: 'ai-chat-intermediate' }, {
|
||||
init_chat_stream,
|
||||
stream: true,
|
||||
response: retval,
|
||||
usage_promise: usage_promise,
|
||||
});
|
||||
}
|
||||
@@ -201,7 +205,8 @@ class ClaudeService extends BaseService {
|
||||
max_tokens: (model === 'claude-3-5-sonnet-20241022' || model === 'claude-3-5-sonnet-20240620') ? 8192 : 4096,
|
||||
temperature: 0,
|
||||
system: PUTER_PROMPT + JSON.stringify(system_prompts),
|
||||
messages: adapted_messages,
|
||||
messages,
|
||||
...(tools ? { tools } : {}),
|
||||
});
|
||||
return {
|
||||
message: msg,
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter 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/>.
|
||||
*/
|
||||
|
||||
// METADATA // {"ai-commented":{"service":"claude"}}
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const { whatis, nou } = require("../../util/langutil");
|
||||
const { PassThrough } = require("stream");
|
||||
const { TypedValue } = require("../../services/drivers/meta/Runtime");
|
||||
const OpenAIUtil = require("./lib/OpenAIUtil");
|
||||
const { TeePromise } = require('@heyputer/putility').libs.promise;
|
||||
const dedent = require('dedent');
|
||||
|
||||
const PUTER_PROMPT = `
|
||||
You are running on an open-source platform called Puter,
|
||||
as the DeepSeek implementation for a driver interface
|
||||
called puter-chat-completion.
|
||||
`.replace('\n', ' ').trim();
|
||||
|
||||
|
||||
/**
|
||||
* DeepSeekService class - Provides integration with X.AI's API for chat completions
|
||||
* Extends BaseService to implement the puter-chat-completion interface.
|
||||
* Handles model management, message adaptation, streaming responses,
|
||||
* and usage tracking for X.AI's language models like Grok.
|
||||
* @extends BaseService
|
||||
*/
|
||||
class DeepSeekService extends BaseService {
|
||||
static MODULES = {
|
||||
openai: require('openai'),
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the system prompt used for AI interactions
|
||||
* @returns {string} The base system prompt that identifies the AI as running on Puter
|
||||
*/
|
||||
get_system_prompt () {
|
||||
return PUTER_PROMPT;
|
||||
}
|
||||
|
||||
adapt_model (model) {
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the XAI service by setting up the OpenAI client and registering with the AI chat provider
|
||||
* @private
|
||||
* @returns {Promise<void>} Resolves when initialization is complete
|
||||
*/
|
||||
async _init () {
|
||||
this.openai = new this.modules.openai.OpenAI({
|
||||
apiKey: this.global_config.services.deepseek.apiKey,
|
||||
baseURL: 'https://api.deepseek.com',
|
||||
});
|
||||
|
||||
const svc_aiChat = this.services.get('ai-chat');
|
||||
svc_aiChat.register_provider({
|
||||
service_name: this.service_name,
|
||||
alias: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the default model identifier for the XAI service
|
||||
* @returns {string} The default model ID 'grok-beta'
|
||||
*/
|
||||
get_default_model () {
|
||||
return 'grok-beta';
|
||||
}
|
||||
|
||||
static IMPLEMENTS = {
|
||||
['puter-chat-completion']: {
|
||||
/**
|
||||
* Returns a list of available models and their details.
|
||||
* See AIChatService for more information.
|
||||
*
|
||||
* @returns Promise<Array<Object>> Array of model details
|
||||
*/
|
||||
async models () {
|
||||
return await this.models_();
|
||||
},
|
||||
/**
|
||||
* Returns a list of available model names including their aliases
|
||||
* @returns {Promise<string[]>} Array of model identifiers and their aliases
|
||||
* @description Retrieves all available model IDs and their aliases,
|
||||
* flattening them into a single array of strings that can be used for model selection
|
||||
*/
|
||||
async list () {
|
||||
const models = await this.models_();
|
||||
const model_names = [];
|
||||
for ( const model of models ) {
|
||||
model_names.push(model.id);
|
||||
if ( model.aliases ) {
|
||||
model_names.push(...model.aliases);
|
||||
}
|
||||
}
|
||||
return model_names;
|
||||
},
|
||||
|
||||
/**
|
||||
* AI Chat completion method.
|
||||
* See AIChatService for more details.
|
||||
*/
|
||||
async complete ({ messages, stream, model, tools }) {
|
||||
model = this.adapt_model(model);
|
||||
|
||||
messages = await OpenAIUtil.process_input_messages(messages);
|
||||
for ( const message of messages ) {
|
||||
// DeepSeek doesn't appreciate arrays here
|
||||
if ( message.tool_calls && Array.isArray(message.content) ) {
|
||||
message.content = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Function calling is just broken on DeepSeek - it never awknowledges
|
||||
// the tool results and instead keeps calling the function over and over.
|
||||
// (see https://github.com/deepseek-ai/DeepSeek-V3/issues/15)
|
||||
// To fix this, we inject a message that tells DeepSeek what happened.
|
||||
const TOOL_TEXT = message => dedent(`
|
||||
Hi DeepSeek V3, your tool calling is broken and you are not able to
|
||||
obtain tool results in the expected way. That's okay, we can work
|
||||
around this.
|
||||
|
||||
Please do not repeat this tool call.
|
||||
|
||||
We have provided the tool call results below:
|
||||
|
||||
Tool call ${message.tool_call_id} returned: ${message.content}.
|
||||
`);
|
||||
for ( let i=messages.length-1; i >= 0 ; i-- ) {
|
||||
const message = messages[i];
|
||||
if ( message.role === 'tool' ) {
|
||||
messages.splice(i+1, 0, {
|
||||
role: 'system',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: TOOL_TEXT(message),
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
messages.unshift({
|
||||
role: 'system',
|
||||
content: PUTER_PROMPT,
|
||||
})
|
||||
|
||||
const completion = await this.openai.chat.completions.create({
|
||||
messages,
|
||||
model: model ?? this.get_default_model(),
|
||||
...(tools ? { tools } : {}),
|
||||
max_tokens: 1000,
|
||||
stream,
|
||||
...(stream ? {
|
||||
stream_options: { include_usage: true },
|
||||
} : {}),
|
||||
});
|
||||
|
||||
return OpenAIUtil.handle_completion_output({
|
||||
stream, completion,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves available AI models and their specifications
|
||||
* @returns {Promise<Array>} Array of model objects containing:
|
||||
* - id: Model identifier string
|
||||
* - name: Human readable model name
|
||||
* - context: Maximum context window size
|
||||
* - cost: Pricing information object with currency and rates
|
||||
* @private
|
||||
*/
|
||||
async models_ () {
|
||||
return [
|
||||
{
|
||||
id: 'deepseek-chat',
|
||||
name: 'DeepSeek Chat',
|
||||
context: 64000,
|
||||
cost: {
|
||||
currency: 'usd-cents',
|
||||
tokens: 1_000_000,
|
||||
input: 14,
|
||||
output: 28,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'deepseek-reasoner',
|
||||
name: 'DeepSeek Reasoner',
|
||||
context: 64000,
|
||||
cost: {
|
||||
currency: 'usd-cents',
|
||||
tokens: 1_000_000,
|
||||
input: 55,
|
||||
output: 219,
|
||||
},
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DeepSeekService,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
||||
const GeminiSquareHole = require("./lib/GeminiSquareHole");
|
||||
const { TypedValue } = require("../../services/drivers/meta/Runtime");
|
||||
const putility = require("@heyputer/putility");
|
||||
|
||||
class GeminiService extends BaseService {
|
||||
async _init () {
|
||||
const svc_aiChat = this.services.get('ai-chat');
|
||||
svc_aiChat.register_provider({
|
||||
service_name: this.service_name,
|
||||
alias: true,
|
||||
});
|
||||
}
|
||||
|
||||
static IMPLEMENTS = {
|
||||
['puter-chat-completion']: {
|
||||
async models () {
|
||||
return await this.models_();
|
||||
},
|
||||
async list () {
|
||||
const models = await this.models_();
|
||||
const model_names = [];
|
||||
for ( const model of models ) {
|
||||
model_names.push(model.id);
|
||||
if ( model.aliases ) {
|
||||
model_names.push(...model.aliases);
|
||||
}
|
||||
}
|
||||
return model_names;
|
||||
},
|
||||
|
||||
async complete ({ messages, stream, model, tools }) {
|
||||
const genAI = new GoogleGenerativeAI(this.config.apiKey);
|
||||
const genModel = genAI.getGenerativeModel({
|
||||
model: model ?? 'gemini-2.0-flash',
|
||||
});
|
||||
|
||||
messages = await GeminiSquareHole.process_input_messages(messages);
|
||||
|
||||
// History is separate, so the last message gets special treatment.
|
||||
const last_message = messages.pop();
|
||||
const last_message_parts = last_message.parts.map(
|
||||
part => typeof part === 'string' ? part : part.text
|
||||
);
|
||||
|
||||
const chat = genModel.startChat({
|
||||
history: messages,
|
||||
});
|
||||
|
||||
const usage_calculator = GeminiSquareHole.create_usage_calculator({
|
||||
model_details: (await this.models_()).find(m => m.id === model),
|
||||
});
|
||||
|
||||
if ( stream ) {
|
||||
const genResult = await chat.sendMessageStream(last_message_parts)
|
||||
const stream = genResult.stream;
|
||||
|
||||
const usage_promise = new putility.libs.promise.TeePromise();
|
||||
return new TypedValue({ $: 'ai-chat-intermediate' }, {
|
||||
stream: true,
|
||||
init_chat_stream:
|
||||
GeminiSquareHole.create_chat_stream_handler({
|
||||
stream, usage_promise,
|
||||
}),
|
||||
usage_promise: usage_promise.then(usageMetadata => {
|
||||
return usage_calculator({ usageMetadata });
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
const genResult = await chat.sendMessage(last_message_parts)
|
||||
|
||||
const message = genResult.response.candidates[0];
|
||||
message.content = message.content.parts;
|
||||
message.role = 'assistant';
|
||||
|
||||
const result = { message };
|
||||
result.usage = usage_calculator(genResult.response);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async models_ () {
|
||||
return [
|
||||
{
|
||||
id: 'gemini-1.5-flash',
|
||||
name: 'Gemini 1.5 Flash',
|
||||
context: 131072,
|
||||
cost: {
|
||||
currency: 'usd-cents',
|
||||
tokens: 1_000_000,
|
||||
input: 7.5,
|
||||
output: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gemini-2.0-flash',
|
||||
name: 'Gemini 2.0 Flash',
|
||||
context: 131072,
|
||||
cost: {
|
||||
currency: 'usd-cents',
|
||||
tokens: 1_000_000,
|
||||
input: 10,
|
||||
output: 40,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { GeminiService };
|
||||
@@ -22,7 +22,7 @@ const { PassThrough } = require("stream");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const { TypedValue } = require("../../services/drivers/meta/Runtime");
|
||||
const { nou } = require("../../util/langutil");
|
||||
const { TeePromise } = require('@heyputer/putility').libs.promise;
|
||||
const OpenAIUtil = require("./lib/OpenAIUtil");
|
||||
|
||||
|
||||
/**
|
||||
@@ -97,67 +97,34 @@ class GroqAIService extends BaseService {
|
||||
* @param {boolean} [options.stream] - Whether to stream the response
|
||||
* @returns {TypedValue|Object} Returns either a TypedValue with streaming response or completion object with usage stats
|
||||
*/
|
||||
async complete ({ messages, model, stream }) {
|
||||
for ( let i = 0; i < messages.length; i++ ) {
|
||||
const message = messages[i];
|
||||
if ( ! message.role ) message.role = 'user';
|
||||
}
|
||||
|
||||
async complete ({ messages, model, stream, tools }) {
|
||||
model = model ?? this.get_default_model();
|
||||
|
||||
messages = await OpenAIUtil.process_input_messages(messages);
|
||||
for ( const message of messages ) {
|
||||
// Curiously, DeepSeek has the exact same deviation
|
||||
if ( message.tool_calls && Array.isArray(message.content) ) {
|
||||
message.content = "";
|
||||
}
|
||||
}
|
||||
|
||||
const completion = await this.client.chat.completions.create({
|
||||
messages,
|
||||
model,
|
||||
stream,
|
||||
tools,
|
||||
});
|
||||
|
||||
if ( stream ) {
|
||||
const usage_promise = new TeePromise();
|
||||
|
||||
const stream = new PassThrough();
|
||||
const retval = new TypedValue({
|
||||
$: 'stream',
|
||||
content_type: 'application/x-ndjson',
|
||||
chunked: true,
|
||||
}, stream);
|
||||
(async () => {
|
||||
for await ( const chunk of completion ) {
|
||||
let usage = chunk?.x_groq?.usage ?? chunk.usage;
|
||||
if ( usage ) {
|
||||
usage_promise.resolve({
|
||||
input_tokens: usage.prompt_tokens,
|
||||
output_tokens: usage.completion_tokens,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( chunk.choices.length < 1 ) continue;
|
||||
if ( chunk.choices[0].finish_reason ) {
|
||||
stream.end();
|
||||
break;
|
||||
}
|
||||
if ( nou(chunk.choices[0].delta.content) ) continue;
|
||||
const str = JSON.stringify({
|
||||
text: chunk.choices[0].delta.content
|
||||
});
|
||||
stream.write(str + '\n');
|
||||
}
|
||||
stream.end();
|
||||
})();
|
||||
|
||||
return new TypedValue({ $: 'ai-chat-intermediate' }, {
|
||||
stream: true,
|
||||
response: retval,
|
||||
usage_promise: usage_promise,
|
||||
});
|
||||
}
|
||||
|
||||
const ret = completion.choices[0];
|
||||
ret.usage = {
|
||||
input_tokens: completion.usage.prompt_tokens,
|
||||
output_tokens: completion.usage.completion_tokens,
|
||||
};
|
||||
return ret;
|
||||
return OpenAIUtil.handle_completion_output({
|
||||
deviations: {
|
||||
index_usage_from_stream_chunk: chunk =>
|
||||
chunk.x_groq?.usage,
|
||||
},
|
||||
usage_calculator: OpenAIUtil.create_usage_calculator({
|
||||
model_details: (await this.models_()).find(m => m.id === model),
|
||||
}),
|
||||
stream, completion,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ const { TypedValue } = require("../../services/drivers/meta/Runtime");
|
||||
const { nou } = require("../../util/langutil");
|
||||
|
||||
const axios = require('axios');
|
||||
const OpenAIUtil = require("./lib/OpenAIUtil");
|
||||
const { TeePromise } = require('@heyputer/putility').libs.promise;
|
||||
|
||||
|
||||
@@ -215,71 +216,51 @@ class MistralAIService extends BaseService {
|
||||
* AI Chat completion method.
|
||||
* See AIChatService for more details.
|
||||
*/
|
||||
async complete ({ messages, stream, model }) {
|
||||
async complete ({ messages, stream, model, tools }) {
|
||||
|
||||
for ( let i = 0; i < messages.length; i++ ) {
|
||||
const message = messages[i];
|
||||
if ( ! message.role ) message.role = 'user';
|
||||
messages = await OpenAIUtil.process_input_messages(messages);
|
||||
for ( const message of messages ) {
|
||||
if ( message.tool_calls ) {
|
||||
message.toolCalls = message.tool_calls;
|
||||
delete message.tool_calls;
|
||||
}
|
||||
if ( message.tool_call_id ) {
|
||||
message.toolCallId = message.tool_call_id;
|
||||
delete message.tool_call_id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( stream ) {
|
||||
let usage_promise = new TeePromise();
|
||||
console.log('MESSAGES TO MISTRAL', messages);
|
||||
|
||||
const stream = new PassThrough();
|
||||
const retval = new TypedValue({
|
||||
$: 'stream',
|
||||
content_type: 'application/x-ndjson',
|
||||
chunked: true,
|
||||
}, stream);
|
||||
const completion = await this.client.chat.stream({
|
||||
model: model ?? this.get_default_model(),
|
||||
messages,
|
||||
});
|
||||
(async () => {
|
||||
for await ( let chunk of completion ) {
|
||||
// just because Mistral wants to be different
|
||||
chunk = chunk.data;
|
||||
|
||||
if ( chunk.usage ) {
|
||||
usage_promise.resolve({
|
||||
input_tokens: chunk.usage.promptTokens,
|
||||
output_tokens: chunk.usage.completionTokens,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( chunk.choices.length < 1 ) continue;
|
||||
if ( chunk.choices[0].finish_reason ) {
|
||||
stream.end();
|
||||
break;
|
||||
}
|
||||
if ( nou(chunk.choices[0].delta.content) ) continue;
|
||||
const str = JSON.stringify({
|
||||
text: chunk.choices[0].delta.content
|
||||
});
|
||||
stream.write(str + '\n');
|
||||
}
|
||||
stream.end();
|
||||
})();
|
||||
|
||||
return new TypedValue({ $: 'ai-chat-intermediate' }, {
|
||||
stream: true,
|
||||
response: retval,
|
||||
usage_promise: usage_promise,
|
||||
});
|
||||
}
|
||||
|
||||
const completion = await this.client.chat.complete({
|
||||
const completion = await this.client.chat[
|
||||
stream ? 'stream' : 'complete'
|
||||
]({
|
||||
model: model ?? this.get_default_model(),
|
||||
...(tools ? { tools } : {}),
|
||||
messages,
|
||||
});
|
||||
// Expected case when mistralai/client-ts#23 is fixed
|
||||
const ret = completion.choices[0];
|
||||
ret.usage = {
|
||||
input_tokens: completion.usage.promptTokens,
|
||||
output_tokens: completion.usage.completionTokens,
|
||||
};
|
||||
return ret;
|
||||
|
||||
return await OpenAIUtil.handle_completion_output({
|
||||
deviations: {
|
||||
index_usage_from_stream_chunk: chunk => {
|
||||
if ( ! chunk.usage ) return;
|
||||
|
||||
const snake_usage = {};
|
||||
for ( const key in chunk.usage ) {
|
||||
const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
|
||||
snake_usage[snakeKey] = chunk.usage[key];
|
||||
}
|
||||
|
||||
return snake_usage;
|
||||
},
|
||||
chunk_but_like_actually: chunk => chunk.data,
|
||||
index_tool_calls_from_stream_choice: choice => choice.delta.toolCalls,
|
||||
},
|
||||
completion, stream,
|
||||
usage_calculator: OpenAIUtil.create_usage_calculator({
|
||||
model_details: this.models_array_.find(m => m.id === model),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,14 @@ const { TypedValue } = require('../../services/drivers/meta/Runtime');
|
||||
const { Context } = require('../../util/context');
|
||||
const smol = require('@heyputer/putility').libs.smol;
|
||||
const { nou } = require('../../util/langutil');
|
||||
const OpenAIUtil = require('./lib/OpenAIUtil');
|
||||
const { TeePromise } = require('@heyputer/putility').libs.promise;
|
||||
|
||||
const PUTER_PROMPT = `
|
||||
You are running on an open-source platform called Puter,
|
||||
as the DeepSeek implementation for a driver interface
|
||||
called puter-chat-completion.
|
||||
`.replace('\n', ' ').trim();
|
||||
|
||||
/**
|
||||
* OpenAICompletionService class provides an interface to OpenAI's chat completion API.
|
||||
@@ -115,6 +121,15 @@ class OpenAICompletionService extends BaseService {
|
||||
output: 1200,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'o3-mini',
|
||||
cost: {
|
||||
currency: 'usd-cents',
|
||||
tokens: 1_000_000,
|
||||
input: 110,
|
||||
output: 440,
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -148,53 +163,10 @@ class OpenAICompletionService extends BaseService {
|
||||
return model_names;
|
||||
},
|
||||
|
||||
/**
|
||||
* AI Chat completion method.
|
||||
* See AIChatService for more details.
|
||||
*/
|
||||
async complete ({ messages, test_mode, stream, model }) {
|
||||
|
||||
// for now this code (also in AIChatService.js) needs to be
|
||||
// duplicated because this hasn't been moved to be under
|
||||
// the centralised controller yet
|
||||
const svc_event = this.services.get('event');
|
||||
const event = {
|
||||
allow: true,
|
||||
intended_service: 'openai',
|
||||
parameters: { messages }
|
||||
};
|
||||
await svc_event.emit('ai.prompt.validate', event);
|
||||
if ( ! event.allow ) {
|
||||
test_mode = true;
|
||||
}
|
||||
|
||||
if ( test_mode ) {
|
||||
const { LoremIpsum } = require('lorem-ipsum');
|
||||
const li = new LoremIpsum({
|
||||
sentencesPerParagraph: {
|
||||
max: 8,
|
||||
min: 4
|
||||
},
|
||||
wordsPerSentence: {
|
||||
max: 20,
|
||||
min: 12
|
||||
},
|
||||
});
|
||||
return {
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": li.generateParagraphs(
|
||||
Math.floor(Math.random() * 3) + 1
|
||||
),
|
||||
},
|
||||
"logprobs": null,
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
}
|
||||
|
||||
async complete ({ messages, test_mode, stream, model, tools }) {
|
||||
return await this.complete(messages, {
|
||||
model: model,
|
||||
tools,
|
||||
moderation: true,
|
||||
stream,
|
||||
});
|
||||
@@ -242,7 +214,7 @@ class OpenAICompletionService extends BaseService {
|
||||
* @returns {Promise<Object>} The completion response containing message and usage info
|
||||
* @throws {Error} If messages are invalid or content is flagged by moderation
|
||||
*/
|
||||
async complete (messages, { stream, moderation, model }) {
|
||||
async complete (messages, { stream, moderation, model, tools }) {
|
||||
// Validate messages
|
||||
if ( ! Array.isArray(messages) ) {
|
||||
throw new Error('`messages` must be an array');
|
||||
@@ -250,48 +222,9 @@ class OpenAICompletionService extends BaseService {
|
||||
|
||||
model = model ?? this.get_default_model();
|
||||
|
||||
for ( let i = 0; i < messages.length; i++ ) {
|
||||
let msg = messages[i];
|
||||
if ( typeof msg === 'string' ) msg = { content: msg };
|
||||
if ( typeof msg !== 'object' ) {
|
||||
throw new Error('each message must be a string or an object');
|
||||
}
|
||||
if ( ! msg.role ) msg.role = 'user';
|
||||
if ( ! msg.content ) {
|
||||
throw new Error('each message must have a `content` property');
|
||||
}
|
||||
|
||||
const texts = [];
|
||||
if ( typeof msg.content === 'string' ) texts.push(msg.content);
|
||||
else if ( typeof msg.content === 'object' ) {
|
||||
if ( Array.isArray(msg.content) ) {
|
||||
texts.push(...msg.content.filter(o => (
|
||||
( ! o.type && o.hasOwnProperty('text') ) ||
|
||||
o.type === 'text')).map(o => o.text));
|
||||
}
|
||||
else texts.push(msg.content.text);
|
||||
}
|
||||
|
||||
this.log.noticeme('OPENAI MODERATION CHECK', {
|
||||
moderation,
|
||||
context_value: Context.get('moderated'),
|
||||
})
|
||||
if ( moderation && ! Context.get('moderated') ) {
|
||||
console.log('RAN MODERATION');
|
||||
for ( const text of texts ) {
|
||||
const moderation_result = await this.check_moderation(text);
|
||||
if ( moderation_result.flagged ) {
|
||||
throw new Error('message is not allowed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages[i] = msg;
|
||||
}
|
||||
|
||||
messages.unshift({
|
||||
role: 'system',
|
||||
content: 'You are running inside a Puter app.',
|
||||
content: PUTER_PROMPT,
|
||||
})
|
||||
// messages.unshift({
|
||||
// role: 'system',
|
||||
@@ -312,166 +245,27 @@ class OpenAICompletionService extends BaseService {
|
||||
// Here's something fun; the documentation shows `type: 'image_url'` in
|
||||
// objects that contain an image url, but everything still works if
|
||||
// that's missing. We normalise it here so the token count code works.
|
||||
for ( const msg of messages ) {
|
||||
if ( ! msg.content ) continue;
|
||||
if ( typeof msg.content !== 'object' ) continue;
|
||||
|
||||
const content = smol.ensure_array(msg.content);
|
||||
|
||||
for ( const o of content ) {
|
||||
if ( ! o.hasOwnProperty('image_url') ) continue;
|
||||
if ( o.type ) continue;
|
||||
o.type = 'image_url';
|
||||
}
|
||||
}
|
||||
|
||||
console.log('DATA GOING IN', messages);
|
||||
|
||||
// Count tokens
|
||||
let token_count = 0;
|
||||
{
|
||||
const enc = this.modules.tiktoken.encoding_for_model(model);
|
||||
const text = JSON.stringify(messages)
|
||||
const tokens = enc.encode(text);
|
||||
token_count += tokens.length;
|
||||
}
|
||||
|
||||
// Subtract image urls
|
||||
for ( const msg of messages ) {
|
||||
// console.log('msg and content', msg, msg.content);
|
||||
if ( ! msg.content ) continue;
|
||||
if ( typeof msg.content !== 'object' ) continue;
|
||||
|
||||
const content = smol.ensure_array(msg.content);
|
||||
|
||||
for ( const o of content ) {
|
||||
// console.log('part of content', o);
|
||||
if ( o.type !== 'image_url' ) continue;
|
||||
const enc = this.modules.tiktoken.encoding_for_model(model);
|
||||
const text = o.image_url?.url ?? '';
|
||||
const tokens = enc.encode(text);
|
||||
token_count -= tokens.length;
|
||||
}
|
||||
}
|
||||
|
||||
const max_tokens = 4096 - token_count;
|
||||
console.log('MAX TOKENS ???', max_tokens);
|
||||
|
||||
const svc_apiErrpr = this.services.get('api-error');
|
||||
if ( max_tokens <= 8 ) {
|
||||
throw svc_apiErrpr.create('max_tokens_exceeded', {
|
||||
input_tokens: token_count,
|
||||
max_tokens: 4096 - 8,
|
||||
});
|
||||
}
|
||||
messages = await OpenAIUtil.process_input_messages(messages);
|
||||
|
||||
const completion = await this.openai.chat.completions.create({
|
||||
user: user_private_uid,
|
||||
messages: messages,
|
||||
model: model,
|
||||
max_tokens,
|
||||
...(tools ? { tools } : {}),
|
||||
// max_tokens,
|
||||
stream,
|
||||
...(stream ? {
|
||||
stream_options: { include_usage: true },
|
||||
} : {}),
|
||||
});
|
||||
|
||||
if ( stream ) {
|
||||
let usage_promise = new TeePromise();
|
||||
|
||||
const entire = [];
|
||||
const stream = new PassThrough();
|
||||
const retval = new TypedValue({
|
||||
$: 'stream',
|
||||
content_type: 'application/x-ndjson',
|
||||
chunked: true,
|
||||
}, stream);
|
||||
(async () => {
|
||||
for await ( const chunk of completion ) {
|
||||
entire.push(chunk);
|
||||
if ( chunk.usage ) {
|
||||
usage_promise.resolve({
|
||||
input_tokens: chunk.usage.prompt_tokens,
|
||||
output_tokens: chunk.usage.completion_tokens,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if ( chunk.choices.length < 1 ) continue;
|
||||
if ( nou(chunk.choices[0].delta.content) ) continue;
|
||||
const str = JSON.stringify({
|
||||
text: chunk.choices[0].delta.content
|
||||
});
|
||||
stream.write(str + '\n');
|
||||
}
|
||||
stream.end();
|
||||
})();
|
||||
|
||||
return new TypedValue({ $: 'ai-chat-intermediate' }, {
|
||||
stream: true,
|
||||
response: retval,
|
||||
usage_promise: usage_promise,
|
||||
});
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
||||
this.log.info('how many choices?: ' + completion.choices.length);
|
||||
|
||||
// Record spending information
|
||||
const spending_meta = {};
|
||||
spending_meta.timestamp = Date.now();
|
||||
spending_meta.count_tokens_input = token_count;
|
||||
/**
|
||||
* Records spending metadata for the chat completion request and performs token counting.
|
||||
* Initializes metadata object with timestamp and token counts for both input and output.
|
||||
* Uses tiktoken to count output tokens from the completion response.
|
||||
* Records spending data via spending service and increments usage counters.
|
||||
* @private
|
||||
*/
|
||||
spending_meta.count_tokens_output = (() => {
|
||||
// count output tokens (overestimate)
|
||||
const enc = this.modules.tiktoken.encoding_for_model(model);
|
||||
const text = JSON.stringify(completion.choices);
|
||||
const tokens = enc.encode(text);
|
||||
return tokens.length;
|
||||
})();
|
||||
|
||||
const svc_spending = Context.get('services').get('spending');
|
||||
svc_spending.record_spending('openai', 'chat-completion', spending_meta);
|
||||
|
||||
const svc_counting = Context.get('services').get('counting');
|
||||
svc_counting.increment({
|
||||
service_name: 'openai:chat-completion',
|
||||
service_type: 'gpt',
|
||||
values: {
|
||||
model,
|
||||
input_tokens: token_count,
|
||||
output_tokens: spending_meta.count_tokens_output,
|
||||
}
|
||||
return OpenAIUtil.handle_completion_output({
|
||||
usage_calculator: OpenAIUtil.create_usage_calculator({
|
||||
model_details: (await this.models_()).find(m => m.id === model),
|
||||
}),
|
||||
stream, completion,
|
||||
moderate: moderation && this.check_moderation.bind(this),
|
||||
});
|
||||
|
||||
const is_empty = completion.choices?.[0]?.message?.content?.trim() === '';
|
||||
if ( is_empty ) {
|
||||
// GPT refuses to generate an empty response if you ask it to,
|
||||
// so this will probably only happen on an error condition.
|
||||
throw new Error('an empty response was generated');
|
||||
}
|
||||
|
||||
// We need to moderate the completion too
|
||||
if ( moderation ) {
|
||||
const text = completion.choices[0].message.content;
|
||||
const moderation_result = await this.check_moderation(text);
|
||||
if ( moderation_result.flagged ) {
|
||||
throw new Error('message is not allowed');
|
||||
}
|
||||
}
|
||||
|
||||
const ret = completion.choices[0];
|
||||
ret.usage = {
|
||||
input_tokens: completion.usage.prompt_tokens,
|
||||
output_tokens: completion.usage.completion_tokens,
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,18 @@ class PuterAIModule extends AdvancedBase {
|
||||
// services.registerService('claude', ClaudeEnoughService);
|
||||
}
|
||||
|
||||
if ( !! config?.services?.['deepseek'] ) {
|
||||
const { DeepSeekService } = require('./DeepSeekService');
|
||||
services.registerService('deepseek', DeepSeekService);
|
||||
|
||||
// const { ClaudeEnoughService } = require('./ClaudeEnoughService');
|
||||
// services.registerService('claude', ClaudeEnoughService);
|
||||
}
|
||||
if ( !! config?.services?.['gemini'] ) {
|
||||
const { GeminiService } = require('./GeminiService');
|
||||
services.registerService('gemini', GeminiService);
|
||||
}
|
||||
|
||||
const { AIChatService } = require('./AIChatService');
|
||||
services.registerService('ai-chat', AIChatService);
|
||||
|
||||
|
||||
@@ -23,15 +23,13 @@ const BaseService = require("../../services/BaseService");
|
||||
const { whatis, nou } = require("../../util/langutil");
|
||||
const { PassThrough } = require("stream");
|
||||
const { TypedValue } = require("../../services/drivers/meta/Runtime");
|
||||
const OpenAIUtil = require("./lib/OpenAIUtil");
|
||||
const { TeePromise } = require('@heyputer/putility').libs.promise;
|
||||
|
||||
const PUTER_PROMPT = `
|
||||
You are running on an open-source platform called Puter,
|
||||
as the xAI implementation for a driver interface
|
||||
called puter-chat-completion.
|
||||
|
||||
The following JSON contains system messages from the
|
||||
user of the driver interface (typically an app on Puter):
|
||||
`.replace('\n', ' ').trim();
|
||||
|
||||
|
||||
@@ -121,104 +119,33 @@ class XAIService extends BaseService {
|
||||
* AI Chat completion method.
|
||||
* See AIChatService for more details.
|
||||
*/
|
||||
async complete ({ messages, stream, model }) {
|
||||
async complete ({ messages, stream, model, tools }) {
|
||||
model = this.adapt_model(model);
|
||||
const adapted_messages = [];
|
||||
|
||||
const system_prompts = [];
|
||||
let previous_was_user = false;
|
||||
for ( const message of messages ) {
|
||||
if ( typeof message.content === 'string' ) {
|
||||
message.content = {
|
||||
type: 'text',
|
||||
text: message.content,
|
||||
};
|
||||
}
|
||||
if ( whatis(message.content) !== 'array' ) {
|
||||
message.content = [message.content];
|
||||
}
|
||||
if ( ! message.role ) message.role = 'user';
|
||||
if ( message.role === 'user' && previous_was_user ) {
|
||||
const last_msg = adapted_messages[adapted_messages.length-1];
|
||||
last_msg.content.push(
|
||||
...(Array.isArray ? message.content : [message.content])
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ( message.role === 'system' ) {
|
||||
system_prompts.push(...message.content);
|
||||
continue;
|
||||
}
|
||||
adapted_messages.push(message);
|
||||
if ( message.role === 'user' ) {
|
||||
previous_was_user = true;
|
||||
}
|
||||
}
|
||||
|
||||
adapted_messages.unshift({
|
||||
messages = await OpenAIUtil.process_input_messages(messages);
|
||||
|
||||
messages.unshift({
|
||||
role: 'system',
|
||||
content: this.get_system_prompt() +
|
||||
JSON.stringify(system_prompts),
|
||||
content: this.get_system_prompt()
|
||||
})
|
||||
|
||||
const completion = await this.openai.chat.completions.create({
|
||||
messages: adapted_messages,
|
||||
messages,
|
||||
model: model ?? this.get_default_model(),
|
||||
...(tools ? { tools } : {}),
|
||||
max_tokens: 1000,
|
||||
stream,
|
||||
...(stream ? {
|
||||
stream_options: { include_usage: true },
|
||||
} : {}),
|
||||
});
|
||||
|
||||
if ( stream ) {
|
||||
let usage_promise = new TeePromise();
|
||||
|
||||
const stream = new PassThrough();
|
||||
const retval = new TypedValue({
|
||||
$: 'stream',
|
||||
content_type: 'application/x-ndjson',
|
||||
chunked: true,
|
||||
}, stream);
|
||||
(async () => {
|
||||
let last_usage = null;
|
||||
for await ( const chunk of completion ) {
|
||||
if ( chunk.usage ) last_usage = chunk.usage;
|
||||
// if (
|
||||
// event.type !== 'content_block_delta' ||
|
||||
// event.delta.type !== 'text_delta'
|
||||
// ) continue;
|
||||
// const str = JSON.stringify({
|
||||
// text: event.delta.text,
|
||||
// });
|
||||
// stream.write(str + '\n');
|
||||
if ( chunk.choices.length < 1 ) continue;
|
||||
if ( nou(chunk.choices[0].delta.content) ) continue;
|
||||
const str = JSON.stringify({
|
||||
text: chunk.choices[0].delta.content
|
||||
});
|
||||
stream.write(str + '\n');
|
||||
}
|
||||
usage_promise.resolve({
|
||||
input_tokens: last_usage.prompt_tokens,
|
||||
output_tokens: last_usage.completion_tokens,
|
||||
});
|
||||
stream.end();
|
||||
})();
|
||||
|
||||
return new TypedValue({ $: 'ai-chat-intermediate' }, {
|
||||
stream: true,
|
||||
response: retval,
|
||||
usage_promise: usage_promise,
|
||||
});
|
||||
}
|
||||
|
||||
const ret = completion.choices[0];
|
||||
ret.usage = {
|
||||
input_tokens: completion.usage.prompt_tokens,
|
||||
output_tokens: completion.usage.completion_tokens,
|
||||
};
|
||||
return ret;
|
||||
return OpenAIUtil.handle_completion_output({
|
||||
usage_calculator: OpenAIUtil.create_usage_calculator({
|
||||
model_details: (await this.models_()).find(m => m.id === model),
|
||||
}),
|
||||
stream, completion,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,4 +59,174 @@ URL.createObjectURL(await (await fetch("http://api.puter.localhost:4100/drivers/
|
||||
}),
|
||||
"method": "POST",
|
||||
})).blob());
|
||||
```
|
||||
```
|
||||
|
||||
### Tool Use
|
||||
|
||||
```javascript
|
||||
await puter.ai.chat('What\'s the weather like in Vancouver?', {
|
||||
tools: [
|
||||
{
|
||||
type: 'function',
|
||||
'function': {
|
||||
name: 'get_weather',
|
||||
description: 'A string describing the weather',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
type: 'string',
|
||||
description: 'city',
|
||||
},
|
||||
},
|
||||
required: ['location'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
strict: true
|
||||
},
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
await puter.ai.chat([
|
||||
{ content: `What's the weather like in Vancouver?` },
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": null,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_vcfEOmDczXq7KGMirPGGiNEe",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": "{\"location\":\"Vancouver\"}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"refusal": null
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
tool_call_id: 'call_vcfEOmDczXq7KGMirPGGiNEe',
|
||||
content: 'Sunny with a chance of rain'
|
||||
},
|
||||
], {
|
||||
tools: [
|
||||
{
|
||||
type: 'function',
|
||||
'function': {
|
||||
name: 'get_weather',
|
||||
description: 'A string describing the weather',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
type: 'string',
|
||||
description: 'city',
|
||||
},
|
||||
},
|
||||
required: ['location'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
strict: true
|
||||
},
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### Claude Tool Use with Streaming
|
||||
|
||||
```javascript
|
||||
gen = await puter.ai.chat('What\'s the weather like in Vancouver?', {
|
||||
model: 'claude',
|
||||
stream: true,
|
||||
tools: [
|
||||
{
|
||||
type: 'function',
|
||||
'function': {
|
||||
name: 'get_weather',
|
||||
description: 'A string describing the weather',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
type: 'string',
|
||||
description: 'city',
|
||||
},
|
||||
},
|
||||
required: ['location'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
strict: true
|
||||
},
|
||||
}
|
||||
]
|
||||
})
|
||||
for await ( const thing of gen ) { console.log('thing', thing) }
|
||||
```
|
||||
|
||||
Last item in the stream looks like this:
|
||||
```json
|
||||
{
|
||||
"tool_use": {
|
||||
"type": "tool_use",
|
||||
"id": "toolu_01Y4naZhXygjUVRjGBvrL9z8",
|
||||
"name": "get_weather",
|
||||
"input": {
|
||||
"location": "Vancouver"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Respond like this:
|
||||
```javascript
|
||||
gen = await puter.ai.chat([
|
||||
{ role: 'user', content: `What's the weather like in Vancouver?` },
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{ type: 'text', text: "I'll check the weather in Vancouver for you." },
|
||||
{ type: 'tool_use', name: 'get_weather', id: 'toolu_01Y4naZhXygjUVRjGBvrL9z8', input: { location: 'Vancouver' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: 'toolu_01Y4naZhXygjUVRjGBvrL9z8',
|
||||
content: 'Sunny with a chance of rain'
|
||||
}
|
||||
]
|
||||
},
|
||||
], {
|
||||
model: 'claude',
|
||||
stream: true,
|
||||
tools: [
|
||||
{
|
||||
type: 'function',
|
||||
'function': {
|
||||
name: 'get_weather',
|
||||
description: 'A string describing the weather',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
type: 'string',
|
||||
description: 'city',
|
||||
},
|
||||
},
|
||||
required: ['location'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
strict: true
|
||||
},
|
||||
}
|
||||
]
|
||||
})
|
||||
for await ( const item of gen ) { console.log(item) }
|
||||
```
|
||||
@@ -0,0 +1,58 @@
|
||||
const { nou } = require('../../../util/langutil');
|
||||
const Streaming = require('../lib/Streaming');
|
||||
// const claude_sample = require('../samples/claude-1');
|
||||
const claude_sample = require('../samples/claude-tools-1');
|
||||
|
||||
const echo_stream = {
|
||||
write: data => {
|
||||
console.log(data);
|
||||
}
|
||||
};
|
||||
|
||||
const chatStream = new Streaming.AIChatStream({ stream: echo_stream });
|
||||
|
||||
let message;
|
||||
let contentBlock;
|
||||
for (const event of claude_sample) {
|
||||
if ( event.type === 'message_start' ) {
|
||||
message = chatStream.message();
|
||||
continue;
|
||||
}
|
||||
if ( event.type === 'message_stop' ) {
|
||||
message.end();
|
||||
message = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( event.type === 'content_block_start' ) {
|
||||
if ( event.content_block.type === 'tool_use' ) {
|
||||
contentBlock = message.contentBlock({
|
||||
type: event.content_block.type,
|
||||
id: event.content_block.id,
|
||||
name: event.content_block.name,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
contentBlock = message.contentBlock({
|
||||
type: event.content_block.type,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( event.type === 'content_block_stop' ) {
|
||||
contentBlock.end();
|
||||
contentBlock = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( event.type === 'content_block_delta' ) {
|
||||
if ( event.delta.type === 'input_json_delta' ) {
|
||||
contentBlock.addPartialJSON(event.delta.partial_json);
|
||||
continue;
|
||||
}
|
||||
if ( event.delta.type === 'text_delta' ) {
|
||||
contentBlock.addText(event.delta.text);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
const { nou } = require('../../../util/langutil');
|
||||
const FunctionCalling = require('../lib/FunctionCalling');
|
||||
const Streaming = require('../lib/Streaming');
|
||||
const openai_fish = require('../samples/openai-tools-1');
|
||||
|
||||
const echo_stream = {
|
||||
write: data => {
|
||||
console.log(data);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const chatStream = new Streaming.AIChatStream({
|
||||
stream: echo_stream,
|
||||
});
|
||||
|
||||
const message = chatStream.message();
|
||||
let textblock = message.contentBlock({ type: 'text' });
|
||||
let toolblock = null;
|
||||
let mode = 'text';
|
||||
|
||||
const tool_call_blocks = [];
|
||||
|
||||
for ( const chunk of openai_fish ) {
|
||||
if ( chunk.usage ) continue;
|
||||
if ( chunk.choices.length < 1 ) continue;
|
||||
|
||||
const choice = chunk.choices[0];
|
||||
|
||||
if ( ! nou(choice.delta.content) ) {
|
||||
if ( mode === 'tool' ) {
|
||||
toolblock.end();
|
||||
mode = 'text';
|
||||
textblock = message.contentBlock({ type: 'text' });
|
||||
}
|
||||
textblock.addText(choice.delta.content);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! nou(choice.delta.tool_calls) ) {
|
||||
if ( mode === 'text' ) {
|
||||
mode = 'tool';
|
||||
textblock.end();
|
||||
}
|
||||
for ( const tool_call of choice.delta.tool_calls ) {
|
||||
if ( ! tool_call_blocks[tool_call.index] ) {
|
||||
toolblock = message.contentBlock({
|
||||
type: 'tool_use',
|
||||
id: tool_call.function.name,
|
||||
});
|
||||
tool_call_blocks[tool_call.index] = toolblock;
|
||||
} else {
|
||||
toolblock = tool_call_blocks[tool_call.index];
|
||||
}
|
||||
toolblock.addPartialJSON(tool_call.function.arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( mode === 'text' ) textblock.end();
|
||||
if ( mode === 'tool' ) toolblock.end();
|
||||
message.end();
|
||||
@@ -0,0 +1,93 @@
|
||||
module.exports = class FunctionCalling {
|
||||
/**
|
||||
* Normalizes the 'tools' object in-place.
|
||||
*
|
||||
* This function will accept an array of tools provided by the
|
||||
* user, and produce a normalized object that can then be
|
||||
* converted to the apprpriate representation for another
|
||||
* service.
|
||||
*
|
||||
* We will accept conventions from either service that a user
|
||||
* might expect to work, prioritizing the OpenAI convention
|
||||
* when conflicting conventions are present.
|
||||
*
|
||||
* @param {*} tools
|
||||
*/
|
||||
static normalize_tools_object (tools) {
|
||||
for ( let i=0 ; i < tools.length ; i++ ) {
|
||||
const tool = tools[i];
|
||||
let normalized_tool = {};
|
||||
|
||||
const normalize_function = fn => {
|
||||
const normal_fn = {};
|
||||
const parameters =
|
||||
fn.parameters ||
|
||||
fn.input_schema;
|
||||
|
||||
normal_fn.parameters = parameters ?? {
|
||||
type: 'object',
|
||||
};
|
||||
|
||||
if ( fn.name ) {
|
||||
normal_fn.name = fn.name;
|
||||
}
|
||||
|
||||
if ( fn.description ) {
|
||||
normal_fn.description = fn.description;
|
||||
}
|
||||
|
||||
return normal_fn;
|
||||
}
|
||||
|
||||
if ( tool.input_schema ) {
|
||||
normalized_tool = {
|
||||
type: 'function',
|
||||
function: normalize_function(tool),
|
||||
};
|
||||
} else if ( tool.type === 'function' ) {
|
||||
normalized_tool = {
|
||||
type: 'function',
|
||||
function: normalize_function(tool.function),
|
||||
}
|
||||
} else {
|
||||
normalized_tool = {
|
||||
type: 'function',
|
||||
function: normalize_function(tool),
|
||||
};
|
||||
}
|
||||
|
||||
tools[i] = normalized_tool;
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will convert a normalized tools object to the
|
||||
* format expected by OpenAI.
|
||||
*
|
||||
* @param {*} tools
|
||||
* @returns
|
||||
*/
|
||||
static make_openai_tools (tools) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will convert a normalized tools object to the
|
||||
* format expected by Claude.
|
||||
*
|
||||
* @param {*} tools
|
||||
* @returns
|
||||
*/
|
||||
static make_claude_tools (tools) {
|
||||
if ( ! tools ) return undefined;
|
||||
return tools.map(tool => {
|
||||
const { name, description, parameters } = tool.function;
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
input_schema: parameters,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Technically this should be called "GeminiUtil",
|
||||
* but Google's AI API defies all the established conventions
|
||||
* so it made sense to defy them here as well.
|
||||
*/
|
||||
module.exports = class GeminiSquareHole {
|
||||
static process_input_messages = async (messages) => {
|
||||
messages = messages.slice();
|
||||
|
||||
for ( const msg of messages ) {
|
||||
msg.parts = msg.content;
|
||||
delete msg.content;
|
||||
|
||||
if ( msg.role === 'assistant' ) {
|
||||
msg.role = 'model';
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
static create_usage_calculator = ({ model_details }) => {
|
||||
return ({ usageMetadata }) => {
|
||||
const tokens = [];
|
||||
|
||||
tokens.push({
|
||||
type: 'prompt',
|
||||
model: model_details.id,
|
||||
amount: usageMetadata.promptTokenCount,
|
||||
cost: model_details.cost.input * usageMetadata.promptTokenCount,
|
||||
});
|
||||
|
||||
tokens.push({
|
||||
type: 'completion',
|
||||
model: model_details.id,
|
||||
amount: usageMetadata.candidatesTokenCount,
|
||||
cost: model_details.cost.output * usageMetadata.candidatesTokenCount,
|
||||
});
|
||||
|
||||
return tokens;
|
||||
};
|
||||
};
|
||||
|
||||
static create_chat_stream_handler = ({
|
||||
stream, // GenerateContentStreamResult:stream
|
||||
usage_promise,
|
||||
}) => async ({ chatStream }) => {
|
||||
const message = chatStream.message();
|
||||
let textblock = message.contentBlock({ type: 'text' });
|
||||
let last_usage = null;
|
||||
for await ( const chunk of stream ) {
|
||||
// This is spread across several lines so that the stack trace
|
||||
// is more helpful if we get an exception because of an
|
||||
// inconsistent response from the model.
|
||||
const candidate = chunk.candidates[0];
|
||||
const content = candidate.content;
|
||||
const parts = content.parts;
|
||||
for ( const part of parts ) {
|
||||
const text = part.text;
|
||||
textblock.addText(text);
|
||||
}
|
||||
|
||||
last_usage = chunk.usageMetadata;
|
||||
}
|
||||
|
||||
usage_promise.resolve(last_usage);
|
||||
|
||||
textblock.end();
|
||||
message.end();
|
||||
chatStream.end();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
const { whatis } = require("../../../util/langutil");
|
||||
|
||||
module.exports = class Messages {
|
||||
static normalize_single_message (message, params = {}) {
|
||||
params = Object.assign({
|
||||
role: 'user',
|
||||
}, params);
|
||||
|
||||
if ( typeof message === 'string' ) {
|
||||
message = {
|
||||
content: [message],
|
||||
};
|
||||
}
|
||||
if ( whatis(message) !== 'object' ) {
|
||||
throw new Error('each message must be a string or object');
|
||||
}
|
||||
if ( ! message.role ) {
|
||||
message.role = params.role;
|
||||
}
|
||||
if ( ! message.content ) {
|
||||
if ( message.tool_calls ) {
|
||||
message.content = [];
|
||||
for ( let i=0 ; i < message.tool_calls.length ; i++ ) {
|
||||
const tool_call = message.tool_calls[i];
|
||||
message.content.push({
|
||||
type: 'tool_use',
|
||||
id: tool_call.id,
|
||||
name: tool_call.function.name,
|
||||
input: tool_call.function.arguments,
|
||||
});
|
||||
}
|
||||
delete message.tool_calls;
|
||||
} else {
|
||||
throw new Error(`each message must have a 'content' property`);
|
||||
}
|
||||
}
|
||||
if ( whatis(message.content) !== 'array' ) {
|
||||
message.content = [message.content];
|
||||
}
|
||||
// Coerce each content block into an object
|
||||
for ( let i=0 ; i < message.content.length ; i++ ) {
|
||||
if ( whatis(message.content[i]) === 'string' ) {
|
||||
message.content[i] = {
|
||||
type: 'text',
|
||||
text: message.content[i],
|
||||
};
|
||||
}
|
||||
if ( whatis(message.content[i]) !== 'object' ) {
|
||||
throw new Error('each message content item must be a string or object');
|
||||
}
|
||||
if ( ! message.content[i].type ) {
|
||||
message.content[i].type = 'text';
|
||||
}
|
||||
}
|
||||
|
||||
// Remove "text" properties from content blocks with type=tool_result
|
||||
for ( let i=0 ; i < message.content.length ; i++ ) {
|
||||
if ( message.content[i].type !== 'tool_use' ) {
|
||||
continue;
|
||||
}
|
||||
if ( message.content[i].hasOwnProperty('text') ) {
|
||||
delete message.content[i].text;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('???', message)
|
||||
return message;
|
||||
}
|
||||
static normalize_messages (messages, params = {}) {
|
||||
for ( let i=0 ; i < messages.length ; i++ ) {
|
||||
messages[i] = this.normalize_single_message(messages[i], params);
|
||||
}
|
||||
|
||||
// Split messages with tool_use content into separate messages
|
||||
// TODO: unit test this
|
||||
messages = [...messages];
|
||||
for ( let i=0 ; i < messages.length ; i++ ) {
|
||||
let message = messages[i];
|
||||
let separated_messages = [];
|
||||
for ( let j=0 ; j < message.content.length ; j++ ) {
|
||||
if ( message.content[j].type === 'tool_result' ) {
|
||||
separated_messages.push({
|
||||
...message,
|
||||
content: [message.content[j]],
|
||||
});
|
||||
} else {
|
||||
separated_messages.push({
|
||||
...message,
|
||||
content: [message.content[j]],
|
||||
});
|
||||
}
|
||||
}
|
||||
messages.splice(i, 1, ...separated_messages);
|
||||
}
|
||||
|
||||
// If multiple messages are from the same role, merge them
|
||||
let merged_messages = [];
|
||||
let current_role = null;
|
||||
for ( let i=0 ; i < messages.length ; i++ ) {
|
||||
if ( current_role === messages[i].role ) {
|
||||
merged_messages[merged_messages.length - 1].content.push(...messages[i].content);
|
||||
} else {
|
||||
merged_messages.push(messages[i]);
|
||||
current_role = messages[i].role;
|
||||
}
|
||||
}
|
||||
|
||||
return merged_messages;
|
||||
}
|
||||
|
||||
static extract_and_remove_system_messages (messages) {
|
||||
let system_messages = [];
|
||||
let new_messages = [];
|
||||
for ( let i=0 ; i < messages.length ; i++ ) {
|
||||
if ( messages[i].role === 'system' ) {
|
||||
system_messages.push(messages[i]);
|
||||
} else {
|
||||
new_messages.push(messages[i]);
|
||||
}
|
||||
}
|
||||
return [system_messages, new_messages];
|
||||
}
|
||||
|
||||
static extract_text (messages) {
|
||||
return messages.map(m => {
|
||||
if ( whatis(m) === 'string' ) {
|
||||
return m;
|
||||
}
|
||||
if ( whatis(m) !== 'object' ) {
|
||||
return '';
|
||||
}
|
||||
if ( whatis(m.content) === 'array' ) {
|
||||
return m.content.map(c => c.text).join(' ');
|
||||
}
|
||||
if ( whatis(m.content) === 'string' ) {
|
||||
return m.content;
|
||||
} else {
|
||||
const is_text_type = m.content.type === 'text' ||
|
||||
! m.content.hasOwnProperty('type');
|
||||
if ( is_text_type ) {
|
||||
if ( whatis(m.content.text) !== 'string' ) {
|
||||
throw new Error('text content must be a string');
|
||||
}
|
||||
return m.content.text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}).join(' ');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
const putility = require("@heyputer/putility");
|
||||
const { TypedValue } = require("../../../services/drivers/meta/Runtime");
|
||||
const { nou } = require("../../../util/langutil");
|
||||
|
||||
module.exports = class OpenAIUtil {
|
||||
/**
|
||||
* Process input messages from Puter's normalized format to OpenAI's format
|
||||
* May make changes in-place.
|
||||
*
|
||||
* @param {Array<Message>} messages - array of normalized messages
|
||||
* @returns {Array<Message>} - array of messages in OpenAI format
|
||||
*/
|
||||
static process_input_messages = async (messages) => {
|
||||
for ( const msg of messages ) {
|
||||
if ( ! msg.content ) continue;
|
||||
if ( typeof msg.content !== 'object' ) continue;
|
||||
|
||||
const content = msg.content;
|
||||
|
||||
for ( const o of content ) {
|
||||
if ( ! o.hasOwnProperty('image_url') ) continue;
|
||||
if ( o.type ) continue;
|
||||
o.type = 'image_url';
|
||||
}
|
||||
|
||||
// coerce tool calls
|
||||
for ( let i = content.length - 1 ; i >= 0 ; i-- ) {
|
||||
const content_block = content[i];
|
||||
|
||||
if ( content_block.type === 'tool_use' ) {
|
||||
if ( ! msg.hasOwnProperty('tool_calls') ) {
|
||||
msg.tool_calls = [];
|
||||
}
|
||||
msg.tool_calls.push({
|
||||
id: content_block.id,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: content_block.name,
|
||||
arguments: JSON.stringify(content_block.input),
|
||||
}
|
||||
});
|
||||
content.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// coerce tool results
|
||||
// (we assume multiple tool results were already split into separate messages)
|
||||
for ( let i = content.length - 1 ; i >= 0 ; i-- ) {
|
||||
const content_block = content[i];
|
||||
if ( content_block.type !== 'tool_result' ) continue;
|
||||
msg.role = 'tool';
|
||||
msg.tool_call_id = content_block.tool_use_id;
|
||||
msg.content = content_block.content;
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
static create_usage_calculator = ({ model_details }) => {
|
||||
return ({ usage }) => {
|
||||
const tokens = [];
|
||||
|
||||
tokens.push({
|
||||
type: 'prompt',
|
||||
model: model_details.id,
|
||||
amount: usage.prompt_tokens,
|
||||
cost: model_details.cost.input * usage.prompt_tokens,
|
||||
});
|
||||
|
||||
tokens.push({
|
||||
type: 'completion',
|
||||
model: model_details.id,
|
||||
amount: usage.completion_tokens,
|
||||
cost: model_details.cost.output * usage.completion_tokens,
|
||||
});
|
||||
|
||||
return tokens;
|
||||
};
|
||||
};
|
||||
|
||||
static create_chat_stream_handler = ({
|
||||
deviations,
|
||||
completion, usage_promise,
|
||||
}) => async ({ chatStream }) => {
|
||||
deviations = Object.assign({
|
||||
// affected by: Groq
|
||||
index_usage_from_stream_chunk: chunk => chunk.usage,
|
||||
// affected by: Mistral
|
||||
chunk_but_like_actually: chunk => chunk,
|
||||
index_tool_calls_from_stream_choice: choice => choice.delta.tool_calls,
|
||||
}, deviations);
|
||||
|
||||
const message = chatStream.message();
|
||||
let textblock = message.contentBlock({ type: 'text' });
|
||||
let toolblock = null;
|
||||
let mode = 'text';
|
||||
const tool_call_blocks = [];
|
||||
|
||||
let last_usage = null;
|
||||
for await ( let chunk of completion ) {
|
||||
chunk = deviations.chunk_but_like_actually(chunk);
|
||||
if ( process.env.DEBUG ) {
|
||||
const delta = chunk?.choices?.[0]?.delta;
|
||||
console.log(
|
||||
`AI CHUNK`,
|
||||
chunk,
|
||||
delta && JSON.stringify(delta)
|
||||
);
|
||||
}
|
||||
const chunk_usage = deviations.index_usage_from_stream_chunk(chunk);
|
||||
if ( chunk_usage ) last_usage = chunk_usage;
|
||||
if ( chunk.choices.length < 1 ) continue;
|
||||
|
||||
const choice = chunk.choices[0];
|
||||
|
||||
if ( ! nou(choice.delta.content) ) {
|
||||
if ( mode === 'tool' ) {
|
||||
toolblock.end();
|
||||
mode = 'text';
|
||||
textblock = message.contentBlock({ type: 'text' });
|
||||
}
|
||||
textblock.addText(choice.delta.content);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tool_calls = deviations.index_tool_calls_from_stream_choice(choice);
|
||||
if ( ! nou(tool_calls) ) {
|
||||
if ( mode === 'text' ) {
|
||||
mode = 'tool';
|
||||
textblock.end();
|
||||
}
|
||||
for ( const tool_call of tool_calls ) {
|
||||
if ( ! tool_call_blocks[tool_call.index] ) {
|
||||
toolblock = message.contentBlock({
|
||||
type: 'tool_use',
|
||||
id: tool_call.id,
|
||||
name: tool_call.function.name,
|
||||
});
|
||||
tool_call_blocks[tool_call.index] = toolblock;
|
||||
} else {
|
||||
toolblock = tool_call_blocks[tool_call.index];
|
||||
}
|
||||
toolblock.addPartialJSON(tool_call.function.arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
usage_promise.resolve(last_usage);
|
||||
|
||||
if ( mode === 'text' ) textblock.end();
|
||||
if ( mode === 'tool' ) toolblock.end();
|
||||
message.end();
|
||||
chatStream.end();
|
||||
};
|
||||
|
||||
static async handle_completion_output ({
|
||||
deviations,
|
||||
stream, completion, moderate,
|
||||
usage_calculator,
|
||||
}) {
|
||||
if ( stream ) {
|
||||
let usage_promise = new putility.libs.promise.TeePromise();
|
||||
|
||||
const init_chat_stream =
|
||||
OpenAIUtil.create_chat_stream_handler({
|
||||
deviations,
|
||||
completion,
|
||||
usage_promise,
|
||||
usage_calculator,
|
||||
});
|
||||
|
||||
return new TypedValue({ $: 'ai-chat-intermediate' }, {
|
||||
stream: true,
|
||||
init_chat_stream,
|
||||
usage_promise: usage_promise.then(usage => {
|
||||
return usage_calculator ? usage_calculator({ usage }) : {
|
||||
input_tokens: usage.prompt_tokens,
|
||||
output_tokens: usage.completion_tokens,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const is_empty = completion.choices?.[0]?.message?.content?.trim() === '';
|
||||
if ( is_empty && ! completion.choices?.[0]?.message?.tool_calls ) {
|
||||
// GPT refuses to generate an empty response if you ask it to,
|
||||
// so this will probably only happen on an error condition.
|
||||
throw new Error('an empty response was generated');
|
||||
}
|
||||
|
||||
// We need to moderate the completion too
|
||||
const mod_text = completion.choices[0].message.content;
|
||||
if ( moderate && mod_text !== null ) {
|
||||
const moderation_result = await moderate(mod_text);
|
||||
if ( moderation_result.flagged ) {
|
||||
throw new Error('message is not allowed');
|
||||
}
|
||||
}
|
||||
|
||||
const ret = completion.choices[0];
|
||||
ret.usage = usage_calculator ? usage_calculator(completion) : {
|
||||
input_tokens: completion.usage.prompt_tokens,
|
||||
output_tokens: completion.usage.completion_tokens,
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Assign the properties of the override object to the original object,
|
||||
* like Object.assign, except properties are ordered so override properties
|
||||
* are enumerated first.
|
||||
*
|
||||
* @param {*} original
|
||||
* @param {*} override
|
||||
*/
|
||||
const objectAssignTop = (original, override) => {
|
||||
let o = {
|
||||
...original,
|
||||
...override,
|
||||
};
|
||||
o = {
|
||||
...override,
|
||||
...original,
|
||||
};
|
||||
return o;
|
||||
}
|
||||
|
||||
class AIChatConstructStream {
|
||||
constructor (chatStream, params) {
|
||||
this.chatStream = chatStream;
|
||||
if ( this._start ) this._start(params);
|
||||
}
|
||||
end () {
|
||||
if ( this._end ) this._end();
|
||||
}
|
||||
}
|
||||
|
||||
class AIChatTextStream extends AIChatConstructStream {
|
||||
addText (text) {
|
||||
const json = JSON.stringify({
|
||||
type: 'text', text,
|
||||
});
|
||||
this.chatStream.stream.write(json + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
class AIChatToolUseStream extends AIChatConstructStream {
|
||||
_start (params) {
|
||||
this.contentBlock = params;
|
||||
this.buffer = '';
|
||||
}
|
||||
addPartialJSON (partial_json) {
|
||||
this.buffer += partial_json;
|
||||
}
|
||||
_end () {
|
||||
if ( this.buffer.trim() === '' ) {
|
||||
this.buffer = '{}';
|
||||
}
|
||||
const str = JSON.stringify(objectAssignTop({
|
||||
...this.contentBlock,
|
||||
input: JSON.parse(this.buffer),
|
||||
...( ! this.contentBlock.text ? { text: "" } : {}),
|
||||
}, {
|
||||
type: 'tool_use',
|
||||
}));
|
||||
this.chatStream.stream.write(str + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
class AIChatMessageStream extends AIChatConstructStream {
|
||||
contentBlock ({ type, ...params }) {
|
||||
if ( type === 'tool_use' ) {
|
||||
return new AIChatToolUseStream(this.chatStream, params);
|
||||
}
|
||||
if ( type === 'text' ) {
|
||||
return new AIChatTextStream(this.chatStream, params);
|
||||
}
|
||||
throw new Error(`Unknown content block type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
class AIChatStream {
|
||||
constructor ({ stream }) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
end () {
|
||||
this.stream.end();
|
||||
}
|
||||
|
||||
message () {
|
||||
return new AIChatMessageStream(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = class Streaming {
|
||||
static AIChatStream = AIChatStream;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
const { expect } = require('chai');
|
||||
const Messages = require('./Messages.js');
|
||||
|
||||
describe('Messages', () => {
|
||||
describe('normalize_single_message', () => {
|
||||
const cases = [
|
||||
{
|
||||
name: 'string message',
|
||||
input: 'Hello, world!',
|
||||
output: {
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hello, world!',
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
for ( const tc of cases ) {
|
||||
it(`should normalize ${tc.name}`, () => {
|
||||
const output = Messages.normalize_single_message(tc.input);
|
||||
expect(output).to.deep.equal(tc.output);
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('extract_text', () => {
|
||||
const cases = [
|
||||
{
|
||||
name: 'string message',
|
||||
input: ['Hello, world!'],
|
||||
output: 'Hello, world!',
|
||||
},
|
||||
{
|
||||
name: 'object message',
|
||||
input: [{
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hello, world!',
|
||||
}
|
||||
]
|
||||
}],
|
||||
output: 'Hello, world!',
|
||||
},
|
||||
{
|
||||
name: 'irregular messages',
|
||||
input: [
|
||||
'First Part',
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Second Part',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
content: 'Third Part',
|
||||
}
|
||||
],
|
||||
output: 'First Part Second Part Third Part',
|
||||
}
|
||||
];
|
||||
for ( const tc of cases ) {
|
||||
it(`should extract text from ${tc.name}`, () => {
|
||||
const output = Messages.extract_text(tc.input);
|
||||
expect(output).to.equal(tc.output);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
module.exports = [
|
||||
{
|
||||
type: 'message_start',
|
||||
message: {
|
||||
id: 'msg_01KKQeaUDpMzNovH9utP5qJc',
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
content: [],
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: 82,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
output_tokens: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'content_block_start',
|
||||
index: 0,
|
||||
content_block: { type: 'text', text: '' }
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: { type: 'text_delta', text: 'Some' }
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: { type: 'text_delta', text: ' species of fish, like the electric' }
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: {
|
||||
type: 'text_delta',
|
||||
text: ' eel, can generate powerful electrical'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: { type: 'text_delta', text: ' charges of up to 860 ' }
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: { type: 'text_delta', text: 'volts to stun prey an' }
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: { type: 'text_delta', text: 'd defend themselves.' }
|
||||
},
|
||||
{ type: 'content_block_stop', index: 0 },
|
||||
{
|
||||
type: 'message_delta',
|
||||
delta: { stop_reason: 'end_turn', stop_sequence: null },
|
||||
usage: { output_tokens: 35 }
|
||||
},
|
||||
{ type: 'message_stop' },
|
||||
]
|
||||
@@ -0,0 +1,76 @@
|
||||
module.exports = [
|
||||
{
|
||||
type: 'message_start',
|
||||
message: {
|
||||
id: 'msg_01GAy4THpFyFJcpxqWXBMrvx',
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
content: [],
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: 458,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
output_tokens: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'content_block_start',
|
||||
index: 0,
|
||||
content_block: { type: 'text', text: '' }
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: { type: 'text_delta', text: 'I' }
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: {
|
||||
type: 'text_delta',
|
||||
text: "'ll check the weather in Vancouver for you."
|
||||
}
|
||||
},
|
||||
{ type: 'content_block_stop', index: 0 },
|
||||
{
|
||||
type: 'content_block_start',
|
||||
index: 1,
|
||||
content_block: {
|
||||
type: 'tool_use',
|
||||
id: 'toolu_01E12jeyCenTtntPBk1j7rgc',
|
||||
name: 'get_weather',
|
||||
input: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 1,
|
||||
delta: { type: 'input_json_delta', partial_json: '' }
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 1,
|
||||
delta: { type: 'input_json_delta', partial_json: '{"location"' }
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 1,
|
||||
delta: { type: 'input_json_delta', partial_json: ': "Van' }
|
||||
},
|
||||
{
|
||||
type: 'content_block_delta',
|
||||
index: 1,
|
||||
delta: { type: 'input_json_delta', partial_json: 'couver"}' }
|
||||
},
|
||||
{ type: 'content_block_stop', index: 1 },
|
||||
{
|
||||
type: 'message_delta',
|
||||
delta: { stop_reason: 'tool_use', stop_sequence: null },
|
||||
usage: { output_tokens: 64 }
|
||||
},
|
||||
{ type: 'message_stop' },
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
module.exports = [
|
||||
{
|
||||
id: 'chatcmpl-AvspmQTvFBBjKsFhHYhyiphFmKMY8',
|
||||
object: 'chat.completion.chunk',
|
||||
created: 1738358842,
|
||||
model: 'gpt-4o-mini-2024-07-18',
|
||||
service_tier: 'default',
|
||||
system_fingerprint: 'fp_bd83329f63',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
role: "assistant",
|
||||
content: "",
|
||||
refusal: null
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: null
|
||||
}
|
||||
],
|
||||
usage: null
|
||||
},
|
||||
...[
|
||||
`Fish`, ` are`, ` diverse`, ` aquatic`, ` creatures`, ` that`, ` play`,
|
||||
` a`, ` crucial`, ` role`, ` in`, ` marine`, ` ecosystems`, ` and`,
|
||||
` human`, ` diets`, `.`
|
||||
].map(str => ({
|
||||
id: 'chatcmpl-AvspmQTvFBBjKsFhHYhyiphFmKMY8',
|
||||
object: 'chat.completion.chunk',
|
||||
created: 1738358842,
|
||||
model: 'gpt-4o-mini-2024-07-18',
|
||||
service_tier: 'default',
|
||||
system_fingerprint: 'fp_bd83329f63',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
content: str
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: null
|
||||
}
|
||||
],
|
||||
usage: null
|
||||
})),
|
||||
];
|
||||
@@ -0,0 +1,102 @@
|
||||
module.exports = [
|
||||
{
|
||||
id: 'chatcmpl-Avqr6AwmQoEFLXuwf1llkKknIR4Ry',
|
||||
object: 'chat.completion.chunk',
|
||||
created: 1738351236,
|
||||
model: 'gpt-4o-mini-2024-07-18',
|
||||
service_tier: 'default',
|
||||
system_fingerprint: 'fp_72ed7ab54c',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: "call_ULl8cRKFQbYeJSIZ3giLAg6r",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_weather",
|
||||
arguments: ""
|
||||
}
|
||||
}
|
||||
],
|
||||
refusal: null
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: null
|
||||
}
|
||||
],
|
||||
usage: null
|
||||
},
|
||||
...[
|
||||
`{"`, `location`, `":"`,
|
||||
`V`, `ancouver`,
|
||||
`"}`
|
||||
].map(str => ({
|
||||
id: 'chatcmpl-Avqr6AwmQoEFLXuwf1llkKknIR4Ry',
|
||||
object: 'chat.completion.chunk',
|
||||
created: 1738351236,
|
||||
model: 'gpt-4o-mini-2024-07-18',
|
||||
service_tier: 'default',
|
||||
system_fingerprint: 'fp_72ed7ab54c',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
function: {
|
||||
arguments: str,
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: null
|
||||
}
|
||||
],
|
||||
usage: null
|
||||
})),
|
||||
{
|
||||
id: 'chatcmpl-Avqr6AwmQoEFLXuwf1llkKknIR4Ry',
|
||||
object: 'chat.completion.chunk',
|
||||
created: 1738351236,
|
||||
model: 'gpt-4o-mini-2024-07-18',
|
||||
service_tier: 'default',
|
||||
system_fingerprint: 'fp_72ed7ab54c',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {},
|
||||
logprobs: null,
|
||||
finish_reason: 'tool_calls'
|
||||
}
|
||||
],
|
||||
usage: null
|
||||
},
|
||||
{
|
||||
id: 'chatcmpl-Avqr6AwmQoEFLXuwf1llkKknIR4Ry',
|
||||
object: 'chat.completion.chunk',
|
||||
created: 1738351236,
|
||||
model: 'gpt-4o-mini-2024-07-18',
|
||||
service_tier: 'default',
|
||||
system_fingerprint: 'fp_72ed7ab54c',
|
||||
choices: [],
|
||||
usage: {
|
||||
prompt_tokens: 62,
|
||||
completion_tokens: 16,
|
||||
total_tokens: 78,
|
||||
prompt_tokens_details: { cached_tokens: 0, audio_tokens: 0 },
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: 0,
|
||||
audio_tokens: 0,
|
||||
accepted_prediction_tokens: 0,
|
||||
rejected_prediction_tokens: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,44 @@
|
||||
const BaseService = require("../../services/BaseService");
|
||||
|
||||
class ExecInterfaceService extends BaseService {
|
||||
async ['__on_driver.register.interfaces'] () {
|
||||
const svc_registry = this.services.get('registry');
|
||||
const col_interfaces = svc_registry.get('interfaces');
|
||||
|
||||
col_interfaces.set('puter-exec', {
|
||||
description: 'Execute code with various languages.',
|
||||
methods: {
|
||||
about: {
|
||||
description: 'Get information about the execution service.',
|
||||
parameters: {},
|
||||
result: { type: 'json' },
|
||||
},
|
||||
supported: {
|
||||
description: 'List supported languages and their details.',
|
||||
parameters: {},
|
||||
result: { type: 'json' },
|
||||
},
|
||||
exec: {
|
||||
description: 'Execute code with a specific language.',
|
||||
parameters: {
|
||||
runtime: {
|
||||
type: 'string',
|
||||
description: 'Name of programming language or ID of runtime.',
|
||||
},
|
||||
code: {
|
||||
type: 'string',
|
||||
description: 'Code to execute.',
|
||||
},
|
||||
stdin: {
|
||||
type: 'string',
|
||||
description: 'Input to provide to the code.',
|
||||
}
|
||||
},
|
||||
result: {},
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ExecInterfaceService;
|
||||
@@ -0,0 +1,66 @@
|
||||
const axios = require('axios');
|
||||
|
||||
class Judge0Client {
|
||||
constructor (options = {}) {
|
||||
Object.assign(this, {
|
||||
baseURL: 'https://judge0-ce.p.sulu.sh',
|
||||
token: '',
|
||||
}, options);
|
||||
}
|
||||
|
||||
async about () {
|
||||
return await this.get_('/about');
|
||||
}
|
||||
|
||||
async create_submission ({ lang_id, code }) {
|
||||
return await this.post_('/submissions', {
|
||||
language_id: lang_id,
|
||||
source_code: code,
|
||||
});
|
||||
}
|
||||
|
||||
async get_submission (id) {
|
||||
return await this.get_(`/submissions/${id}`);
|
||||
}
|
||||
|
||||
async get_ (path) {
|
||||
if ( ! path.startsWith('/') ) {
|
||||
path = `/${path}`;
|
||||
}
|
||||
console.log('how is this url invalid??', `${this.baseURL}${path}`);
|
||||
const resp = await axios.request({
|
||||
method: 'GET',
|
||||
url: `${this.baseURL}${path}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
async post_ (path, data) {
|
||||
if ( ! path.startsWith('/') ) {
|
||||
path = `/${path}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await axios.request({
|
||||
method: 'POST',
|
||||
url: `${this.baseURL}${path}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
return resp.data;
|
||||
} catch (e) {
|
||||
console.error('error response', e.response.data);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Judge0Client };
|
||||
@@ -0,0 +1,121 @@
|
||||
const putility = require("@heyputer/putility");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const { Judge0Client } = require("./Judge0Client");
|
||||
const { Context } = require("../../util/context");
|
||||
const { find_highest_version } = require("../../util/versionutil");
|
||||
|
||||
class Judge0Service extends BaseService {
|
||||
_construct () {
|
||||
this.about_ = {};
|
||||
}
|
||||
|
||||
async _init () {
|
||||
this.languages = require('./languages/languages');
|
||||
this.client = new Judge0Client({
|
||||
token: this.config.token,
|
||||
});
|
||||
|
||||
// this.submissions_ = {};
|
||||
}
|
||||
|
||||
static IMPLEMENTS = {
|
||||
['puter-exec']: {
|
||||
async about () {
|
||||
return this.about ?? (this.about = await this.client.about());
|
||||
},
|
||||
async supported () {
|
||||
},
|
||||
async exec ({ runtime, code }) {
|
||||
return await this.exec_({ runtime, code });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async exec_ ({ runtime, code }) {
|
||||
const lang_id = (() => {
|
||||
if ( runtime.startsWith('j0-') ) {
|
||||
return runtime.slice(3);
|
||||
}
|
||||
if ( runtime.includes('-') ) {
|
||||
const versionIndex = runtime.lastIndexOf('-');
|
||||
const langPart = runtime.slice(0, versionIndex);
|
||||
const versionPart = runtime.slice(versionIndex + 1);
|
||||
const lang = this.languages.find((lang) =>
|
||||
lang.language === langPart && lang.version === versionPart
|
||||
);
|
||||
if ( lang ) {
|
||||
return lang.judge0_id;
|
||||
}
|
||||
}
|
||||
const matchingLangs = this.languages.filter((lang) => lang.language === runtime);
|
||||
const lang = find_highest_version(matchingLangs);
|
||||
|
||||
if ( lang ) {
|
||||
return lang.judge0_id;
|
||||
}
|
||||
})();
|
||||
|
||||
if ( !lang_id ) {
|
||||
throw new Error(`Language or runtime not found: ${runtime}`);
|
||||
}
|
||||
|
||||
const result = await this.client.create_submission({
|
||||
lang_id,
|
||||
code,
|
||||
});
|
||||
|
||||
const submission_done = new putility.libs.promise.TeePromise();
|
||||
(async () => {
|
||||
// Need to poll the submission until it's done
|
||||
let i = 0;
|
||||
let poll_running = false;
|
||||
let poll = setInterval(async () => {
|
||||
// Prevent overlapping polls
|
||||
if ( poll_running ) return;
|
||||
|
||||
// Custom backoff strategy
|
||||
let will_skip = false;
|
||||
if ( i > 5 && i % 2 === 0 ) will_skip = true;
|
||||
if ( i > 10 && i % 3 === 0 ) will_skip = true;
|
||||
if ( i > 50 && i % 5 === 0 ) will_skip = true;
|
||||
i++;
|
||||
if ( will_skip ) return;
|
||||
|
||||
// Poll the submission
|
||||
poll_running = true;
|
||||
const submission = await this.client.get_submission(result.token);
|
||||
if ( submission.status.id >= 3 ) {
|
||||
clearInterval(poll);
|
||||
// this.submissions_[result.id] = submission;
|
||||
submission_done.resolve(submission);
|
||||
}
|
||||
poll_running = false;
|
||||
});
|
||||
|
||||
// Wait for the submission to be done
|
||||
const submission = await submission_done;
|
||||
|
||||
this.log.noticeme('Submission done', submission);
|
||||
|
||||
// Send event
|
||||
const svc_event = this.services.get('event');
|
||||
svc_event.emit('puter-exec.submission.done', {
|
||||
id: submission.token,
|
||||
actor: Context.get('actor'),
|
||||
output: submission.stdout,
|
||||
summary: submission.message,
|
||||
measures: {
|
||||
time: submission.time,
|
||||
memory: submission.memory,
|
||||
},
|
||||
aux_outputs: {
|
||||
compile: submission.compile_output,
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Judge0Service;
|
||||
@@ -0,0 +1,21 @@
|
||||
const config = require("../../config");
|
||||
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
|
||||
class PuterExecModule extends AdvancedBase {
|
||||
async install (context) {
|
||||
const services = context.get('services');
|
||||
|
||||
const ExecInterfaceService = require('./ExecInterfaceService');
|
||||
services.registerService('__exec-interfaces', ExecInterfaceService);
|
||||
|
||||
if ( !! config?.services?.['judge0'] ) {
|
||||
const Judge0Service = require('./Judge0Service');
|
||||
services.registerService('judge0', Judge0Service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PuterExecModule
|
||||
};
|
||||
@@ -0,0 +1,561 @@
|
||||
module.exports = [
|
||||
{
|
||||
"judge0_id": 45,
|
||||
"id": "j0-45",
|
||||
"language": "asm",
|
||||
"version": "2.14.02",
|
||||
"versions": {
|
||||
"nasm": "2.14.02"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 46,
|
||||
"id": "j0-46",
|
||||
"language": "bash",
|
||||
"version": "5.0.0",
|
||||
"versions": {
|
||||
"bash": "5.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 47,
|
||||
"id": "j0-47",
|
||||
"language": "basic",
|
||||
"version": "1.07.1",
|
||||
"versions": {
|
||||
"fbc": "1.07.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 104,
|
||||
"id": "j0-104",
|
||||
"language": "c",
|
||||
"version": "18.1.8",
|
||||
"versions": {
|
||||
"clang": "18.1.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 75,
|
||||
"id": "j0-75",
|
||||
"language": "c",
|
||||
"version": "7.0.1",
|
||||
"versions": {
|
||||
"clang": "7.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 76,
|
||||
"id": "j0-76",
|
||||
"language": "c++",
|
||||
"version": "7.0.1",
|
||||
"versions": {
|
||||
"clang": "7.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 103,
|
||||
"id": "j0-103",
|
||||
"language": "c",
|
||||
"version": "14.1.0",
|
||||
"versions": {
|
||||
"gcc": "14.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 105,
|
||||
"id": "j0-105",
|
||||
"language": "c++",
|
||||
"version": "14.1.0",
|
||||
"versions": {
|
||||
"gcc": "14.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 48,
|
||||
"id": "j0-48",
|
||||
"language": "c",
|
||||
"version": "7.4.0",
|
||||
"versions": {
|
||||
"gcc": "7.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 52,
|
||||
"id": "j0-52",
|
||||
"language": "c++",
|
||||
"version": "7.4.0",
|
||||
"versions": {
|
||||
"gcc": "7.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 49,
|
||||
"id": "j0-49",
|
||||
"language": "c",
|
||||
"version": "8.3.0",
|
||||
"versions": {
|
||||
"gcc": "8.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 53,
|
||||
"id": "j0-53",
|
||||
"language": "c++",
|
||||
"version": "8.3.0",
|
||||
"versions": {
|
||||
"gcc": "8.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 50,
|
||||
"id": "j0-50",
|
||||
"language": "c",
|
||||
"version": "9.2.0",
|
||||
"versions": {
|
||||
"gcc": "9.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 54,
|
||||
"id": "j0-54",
|
||||
"language": "c++",
|
||||
"version": "9.2.0",
|
||||
"versions": {
|
||||
"gcc": "9.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 86,
|
||||
"id": "j0-86",
|
||||
"language": "clojure",
|
||||
"version": "1.10.1",
|
||||
"versions": {
|
||||
"clojure": "1.10.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 51,
|
||||
"id": "j0-51",
|
||||
"language": "c#",
|
||||
"version": "6.6.0.161",
|
||||
"versions": {
|
||||
"mono": "6.6.0.161"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 77,
|
||||
"id": "j0-77",
|
||||
"language": "cobol",
|
||||
"version": "2.2",
|
||||
"versions": {
|
||||
"gnucobol": "2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 55,
|
||||
"id": "j0-55",
|
||||
"language": "common lisp",
|
||||
"version": "2.0.0",
|
||||
"versions": {
|
||||
"sbcl": "2.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 90,
|
||||
"id": "j0-90",
|
||||
"language": "dart",
|
||||
"version": "2.19.2",
|
||||
"versions": {
|
||||
"dart": "2.19.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 56,
|
||||
"id": "j0-56",
|
||||
"language": "d",
|
||||
"version": "2.089.1",
|
||||
"versions": {
|
||||
"dmd": "2.089.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 57,
|
||||
"id": "j0-57",
|
||||
"language": "elixir",
|
||||
"version": "1.9.4",
|
||||
"versions": {
|
||||
"elixir": "1.9.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 58,
|
||||
"id": "j0-58",
|
||||
"language": "erlang",
|
||||
"version": "22.2",
|
||||
"versions": {
|
||||
"otp": "22.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 44,
|
||||
"id": "j0-44",
|
||||
"language": "executable",
|
||||
"versions": {}
|
||||
},
|
||||
{
|
||||
"judge0_id": 87,
|
||||
"id": "j0-87",
|
||||
"language": "f#",
|
||||
"version": "Core",
|
||||
"versions": {
|
||||
".net": "Core"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 59,
|
||||
"id": "j0-59",
|
||||
"language": "fortran",
|
||||
"version": "9.2.0",
|
||||
"versions": {
|
||||
"gfortran": "9.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 60,
|
||||
"id": "j0-60",
|
||||
"language": "go",
|
||||
"version": "1.13.5",
|
||||
"versions": {
|
||||
"go": "1.13.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 95,
|
||||
"id": "j0-95",
|
||||
"language": "go",
|
||||
"version": "1.18.5",
|
||||
"versions": {
|
||||
"go": "1.18.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 88,
|
||||
"id": "j0-88",
|
||||
"language": "groovy",
|
||||
"version": "3.0.3",
|
||||
"versions": {
|
||||
"groovy": "3.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 61,
|
||||
"id": "j0-61",
|
||||
"language": "haskell",
|
||||
"version": "8.8.1",
|
||||
"versions": {
|
||||
"ghc": "8.8.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 96,
|
||||
"id": "j0-96",
|
||||
"language": "javafx",
|
||||
"version": "17.0.6",
|
||||
"versions": {
|
||||
"jdk": "17.0.6",
|
||||
"": "OpenJFX"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 91,
|
||||
"id": "j0-91",
|
||||
"language": "java",
|
||||
"version": "17.0.6",
|
||||
"versions": {
|
||||
"jdk": "17.0.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 62,
|
||||
"id": "j0-62",
|
||||
"language": "java",
|
||||
"version": "13.0.1",
|
||||
"versions": {
|
||||
"openjdk": "13.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 63,
|
||||
"id": "j0-63",
|
||||
"language": "javascript",
|
||||
"version": "12.14.0",
|
||||
"versions": {
|
||||
"node": "12.14.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 93,
|
||||
"id": "j0-93",
|
||||
"language": "javascript",
|
||||
"version": "18.15.0",
|
||||
"versions": {
|
||||
"node": "18.15.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 97,
|
||||
"id": "j0-97",
|
||||
"language": "javascript",
|
||||
"version": "20.17.0",
|
||||
"versions": {
|
||||
"node": "20.17.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 102,
|
||||
"id": "j0-102",
|
||||
"language": "javascript",
|
||||
"version": "22.08.0",
|
||||
"versions": {
|
||||
"node": "22.08.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 78,
|
||||
"id": "j0-78",
|
||||
"language": "kotlin",
|
||||
"version": "1.3.70",
|
||||
"versions": {
|
||||
"kotlin": "1.3.70"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 64,
|
||||
"id": "j0-64",
|
||||
"language": "lua",
|
||||
"version": "5.3.5",
|
||||
"versions": {
|
||||
"lua": "5.3.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 89,
|
||||
"id": "j0-89",
|
||||
"language": "multi-file program",
|
||||
"versions": {}
|
||||
},
|
||||
{
|
||||
"judge0_id": 79,
|
||||
"id": "j0-79",
|
||||
"language": "objective-c",
|
||||
"version": "7.0.1",
|
||||
"versions": {
|
||||
"clang": "7.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 65,
|
||||
"id": "j0-65",
|
||||
"language": "ocaml",
|
||||
"version": "4.09.0",
|
||||
"versions": {
|
||||
"ocaml": "4.09.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 66,
|
||||
"id": "j0-66",
|
||||
"language": "octave",
|
||||
"version": "5.1.0",
|
||||
"versions": {
|
||||
"octave": "5.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 67,
|
||||
"id": "j0-67",
|
||||
"language": "pascal",
|
||||
"version": "3.0.4",
|
||||
"versions": {
|
||||
"fpc": "3.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 85,
|
||||
"id": "j0-85",
|
||||
"language": "perl",
|
||||
"version": "5.28.1",
|
||||
"versions": {
|
||||
"perl": "5.28.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 68,
|
||||
"id": "j0-68",
|
||||
"language": "php",
|
||||
"version": "7.4.1",
|
||||
"versions": {
|
||||
"php": "7.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 98,
|
||||
"id": "j0-98",
|
||||
"language": "php",
|
||||
"version": "8.3.11",
|
||||
"versions": {
|
||||
"php": "8.3.11"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 43,
|
||||
"id": "j0-43",
|
||||
"language": "plain text",
|
||||
"versions": {}
|
||||
},
|
||||
{
|
||||
"judge0_id": 69,
|
||||
"id": "j0-69",
|
||||
"language": "prolog",
|
||||
"version": "Prolog",
|
||||
"versions": {
|
||||
"gnu": "Prolog"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 70,
|
||||
"id": "j0-70",
|
||||
"language": "python",
|
||||
"version": "2.7.17",
|
||||
"versions": {
|
||||
"python": "2.7.17"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 92,
|
||||
"id": "j0-92",
|
||||
"language": "python",
|
||||
"version": "3.11.2",
|
||||
"versions": {
|
||||
"python": "3.11.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 100,
|
||||
"id": "j0-100",
|
||||
"language": "python",
|
||||
"version": "3.12.5",
|
||||
"versions": {
|
||||
"python": "3.12.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 71,
|
||||
"id": "j0-71",
|
||||
"language": "python",
|
||||
"version": "3.8.1",
|
||||
"versions": {
|
||||
"python": "3.8.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 80,
|
||||
"id": "j0-80",
|
||||
"language": "r",
|
||||
"version": "4.0.0",
|
||||
"versions": {
|
||||
"r": "4.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 99,
|
||||
"id": "j0-99",
|
||||
"language": "r",
|
||||
"version": "4.4.1",
|
||||
"versions": {
|
||||
"r": "4.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 72,
|
||||
"id": "j0-72",
|
||||
"language": "ruby",
|
||||
"version": "2.7.0",
|
||||
"versions": {
|
||||
"ruby": "2.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 73,
|
||||
"id": "j0-73",
|
||||
"language": "rust",
|
||||
"version": "1.40.0",
|
||||
"versions": {
|
||||
"rust": "1.40.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 81,
|
||||
"id": "j0-81",
|
||||
"language": "scala",
|
||||
"version": "2.13.2",
|
||||
"versions": {
|
||||
"scala": "2.13.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 82,
|
||||
"id": "j0-82",
|
||||
"language": "sql",
|
||||
"version": "3.27.2",
|
||||
"versions": {
|
||||
"sqlite": "3.27.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 83,
|
||||
"id": "j0-83",
|
||||
"language": "swift",
|
||||
"version": "5.2.3",
|
||||
"versions": {
|
||||
"swift": "5.2.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 74,
|
||||
"id": "j0-74",
|
||||
"language": "typescript",
|
||||
"version": "3.7.4",
|
||||
"versions": {
|
||||
"typescript": "3.7.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 94,
|
||||
"id": "j0-94",
|
||||
"language": "typescript",
|
||||
"version": "5.0.3",
|
||||
"versions": {
|
||||
"typescript": "5.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 101,
|
||||
"id": "j0-101",
|
||||
"language": "typescript",
|
||||
"version": "5.6.2",
|
||||
"versions": {
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"judge0_id": 84,
|
||||
"id": "j0-84",
|
||||
"language": "vb.net",
|
||||
"version": "0.0.0.5943",
|
||||
"versions": {
|
||||
"vbnc": "0.0.0.5943"
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,63 @@
|
||||
const fs = require('fs');
|
||||
const sample = JSON.parse(fs.readFileSync('sample.json', 'utf8'));
|
||||
|
||||
const parse_language_name = (name) => {
|
||||
// Possible formats:
|
||||
// "Name Of Language"
|
||||
// "Name Of Language (Version)"
|
||||
// "Name Of Language (Runtime Version)"
|
||||
// "Name of Language (Runtime Version, Runtime Version)"
|
||||
|
||||
if ( name.indexOf('(') === -1 ) {
|
||||
return { language: name, versions: {} };
|
||||
}
|
||||
const language = name.slice(0, name.indexOf('(')).trim();
|
||||
const parens = name.slice(name.indexOf('(') + 1, name.indexOf(')'))
|
||||
const versions = parens.split(',').map(versionLine => {
|
||||
const parts = versionLine.split(' ');
|
||||
if ( parts.length === 1 ) {
|
||||
return { [language]: parts[0].trim() };
|
||||
}
|
||||
const [key, value] = parts;
|
||||
return { [key.trim().toLowerCase()]: value.trim() };
|
||||
}).reduce((acc, cur) => ({ ...acc, ...cur }), {});
|
||||
|
||||
return { language, versions };
|
||||
};
|
||||
|
||||
const normalizeName = name => {
|
||||
name = name.toLowerCase();
|
||||
if ( name === 'node.js' ) name = 'node';
|
||||
if ( name === 'assembly' ) name = 'asm';
|
||||
if ( name === 'visual basic.net' ) name = 'vb.net';
|
||||
|
||||
// TODO: what kind of executable? ELF binary? Windows executable?
|
||||
// if ( name === 'executable' ) name = 'exe';
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
const output = [];
|
||||
for ( const item of sample ) {
|
||||
const { id, name } = item;
|
||||
let { language, versions } = parse_language_name(name);
|
||||
|
||||
language = normalizeName(language);
|
||||
{
|
||||
const newVersions = {};
|
||||
for ( const key in versions ) {
|
||||
newVersions[normalizeName(key)] = versions[key];
|
||||
}
|
||||
versions = newVersions;
|
||||
}
|
||||
|
||||
output.push({
|
||||
judge0_id: id,
|
||||
id: `j0-${id}`,
|
||||
language,
|
||||
version: versions[Object.keys(versions)[0]],
|
||||
versions,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
@@ -0,0 +1,254 @@
|
||||
[
|
||||
{
|
||||
"id": 45,
|
||||
"name": "Assembly (NASM 2.14.02)"
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"name": "Bash (5.0.0)"
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"name": "Basic (FBC 1.07.1)"
|
||||
},
|
||||
{
|
||||
"id": 104,
|
||||
"name": "C (Clang 18.1.8)"
|
||||
},
|
||||
{
|
||||
"id": 75,
|
||||
"name": "C (Clang 7.0.1)"
|
||||
},
|
||||
{
|
||||
"id": 76,
|
||||
"name": "C++ (Clang 7.0.1)"
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"name": "C (GCC 14.1.0)"
|
||||
},
|
||||
{
|
||||
"id": 105,
|
||||
"name": "C++ (GCC 14.1.0)"
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"name": "C (GCC 7.4.0)"
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"name": "C++ (GCC 7.4.0)"
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"name": "C (GCC 8.3.0)"
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"name": "C++ (GCC 8.3.0)"
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"name": "C (GCC 9.2.0)"
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"name": "C++ (GCC 9.2.0)"
|
||||
},
|
||||
{
|
||||
"id": 86,
|
||||
"name": "Clojure (1.10.1)"
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"name": "C# (Mono 6.6.0.161)"
|
||||
},
|
||||
{
|
||||
"id": 77,
|
||||
"name": "COBOL (GnuCOBOL 2.2)"
|
||||
},
|
||||
{
|
||||
"id": 55,
|
||||
"name": "Common Lisp (SBCL 2.0.0)"
|
||||
},
|
||||
{
|
||||
"id": 90,
|
||||
"name": "Dart (2.19.2)"
|
||||
},
|
||||
{
|
||||
"id": 56,
|
||||
"name": "D (DMD 2.089.1)"
|
||||
},
|
||||
{
|
||||
"id": 57,
|
||||
"name": "Elixir (1.9.4)"
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"name": "Erlang (OTP 22.2)"
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"name": "Executable"
|
||||
},
|
||||
{
|
||||
"id": 87,
|
||||
"name": "F# (.NET Core SDK 3.1.202)"
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"name": "Fortran (GFortran 9.2.0)"
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"name": "Go (1.13.5)"
|
||||
},
|
||||
{
|
||||
"id": 95,
|
||||
"name": "Go (1.18.5)"
|
||||
},
|
||||
{
|
||||
"id": 88,
|
||||
"name": "Groovy (3.0.3)"
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"name": "Haskell (GHC 8.8.1)"
|
||||
},
|
||||
{
|
||||
"id": 96,
|
||||
"name": "JavaFX (JDK 17.0.6, OpenJFX 22.0.2)"
|
||||
},
|
||||
{
|
||||
"id": 91,
|
||||
"name": "Java (JDK 17.0.6)"
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"name": "Java (OpenJDK 13.0.1)"
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"name": "JavaScript (Node.js 12.14.0)"
|
||||
},
|
||||
{
|
||||
"id": 93,
|
||||
"name": "JavaScript (Node.js 18.15.0)"
|
||||
},
|
||||
{
|
||||
"id": 97,
|
||||
"name": "JavaScript (Node.js 20.17.0)"
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"name": "JavaScript (Node.js 22.08.0)"
|
||||
},
|
||||
{
|
||||
"id": 78,
|
||||
"name": "Kotlin (1.3.70)"
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"name": "Lua (5.3.5)"
|
||||
},
|
||||
{
|
||||
"id": 89,
|
||||
"name": "Multi-file program"
|
||||
},
|
||||
{
|
||||
"id": 79,
|
||||
"name": "Objective-C (Clang 7.0.1)"
|
||||
},
|
||||
{
|
||||
"id": 65,
|
||||
"name": "OCaml (4.09.0)"
|
||||
},
|
||||
{
|
||||
"id": 66,
|
||||
"name": "Octave (5.1.0)"
|
||||
},
|
||||
{
|
||||
"id": 67,
|
||||
"name": "Pascal (FPC 3.0.4)"
|
||||
},
|
||||
{
|
||||
"id": 85,
|
||||
"name": "Perl (5.28.1)"
|
||||
},
|
||||
{
|
||||
"id": 68,
|
||||
"name": "PHP (7.4.1)"
|
||||
},
|
||||
{
|
||||
"id": 98,
|
||||
"name": "PHP (8.3.11)"
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"name": "Plain Text"
|
||||
},
|
||||
{
|
||||
"id": 69,
|
||||
"name": "Prolog (GNU Prolog 1.4.5)"
|
||||
},
|
||||
{
|
||||
"id": 70,
|
||||
"name": "Python (2.7.17)"
|
||||
},
|
||||
{
|
||||
"id": 92,
|
||||
"name": "Python (3.11.2)"
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"name": "Python (3.12.5)"
|
||||
},
|
||||
{
|
||||
"id": 71,
|
||||
"name": "Python (3.8.1)"
|
||||
},
|
||||
{
|
||||
"id": 80,
|
||||
"name": "R (4.0.0)"
|
||||
},
|
||||
{
|
||||
"id": 99,
|
||||
"name": "R (4.4.1)"
|
||||
},
|
||||
{
|
||||
"id": 72,
|
||||
"name": "Ruby (2.7.0)"
|
||||
},
|
||||
{
|
||||
"id": 73,
|
||||
"name": "Rust (1.40.0)"
|
||||
},
|
||||
{
|
||||
"id": 81,
|
||||
"name": "Scala (2.13.2)"
|
||||
},
|
||||
{
|
||||
"id": 82,
|
||||
"name": "SQL (SQLite 3.27.2)"
|
||||
},
|
||||
{
|
||||
"id": 83,
|
||||
"name": "Swift (5.2.3)"
|
||||
},
|
||||
{
|
||||
"id": 74,
|
||||
"name": "TypeScript (3.7.4)"
|
||||
},
|
||||
{
|
||||
"id": 94,
|
||||
"name": "TypeScript (5.0.3)"
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"name": "TypeScript (5.6.2)"
|
||||
},
|
||||
{
|
||||
"id": 84,
|
||||
"name": "Visual Basic.Net (vbnc 0.0.0.5943)"
|
||||
}
|
||||
]
|
||||
@@ -100,7 +100,7 @@ module.exports = class DatabaseFSEntryFetcher extends BaseService {
|
||||
async findByUID(uuid, fetch_entry_options = {}) {
|
||||
const { thumbnail } = fetch_entry_options;
|
||||
|
||||
let fsentry = await this.db.requireRead(
|
||||
let fsentry = await this.db.tryHardRead(
|
||||
`SELECT ` +
|
||||
this.defaultProperties.join(', ') +
|
||||
(thumbnail ? `, thumbnail` : '') +
|
||||
@@ -114,7 +114,7 @@ module.exports = class DatabaseFSEntryFetcher extends BaseService {
|
||||
async findByID(id, fetch_entry_options = {}) {
|
||||
const { thumbnail } = fetch_entry_options;
|
||||
|
||||
let fsentry = await this.db.requireRead(
|
||||
let fsentry = await this.db.tryHardRead(
|
||||
`SELECT ` +
|
||||
this.defaultProperties.join(', ') +
|
||||
(thumbnail ? `, thumbnail` : '') +
|
||||
|
||||
@@ -29,6 +29,9 @@ const FSNodeContext = require('../../../filesystem/FSNodeContext');
|
||||
const { RESOURCE_STATUS_PENDING_CREATE } = require('../ResourceService');
|
||||
const { ParallelTasks } = require('../../../util/otelutil');
|
||||
|
||||
const { TYPE_DIRECTORY } = require('../../../filesystem/FSNodeContext');
|
||||
const APIError = require('../../../api/APIError');
|
||||
|
||||
class PuterFSProvider extends putility.AdvancedBase {
|
||||
static MODULES = {
|
||||
_path: require('path'),
|
||||
@@ -152,6 +155,49 @@ class PuterFSProvider extends putility.AdvancedBase {
|
||||
return child_uuids;
|
||||
}
|
||||
|
||||
async move ({ context, node, new_parent, new_name, metadata }) {
|
||||
const { _path } = this.modules;
|
||||
|
||||
const services = context.get('services');
|
||||
const old_path = await node.get('path');
|
||||
const new_path = _path.join(await new_parent.get('path'), new_name);
|
||||
|
||||
const svc_fsEntry = services.get('fsEntryService');
|
||||
const op_update = await svc_fsEntry.update(node.uid, {
|
||||
...(
|
||||
await node.get('parent_uid') !== await new_parent.get('uid')
|
||||
? { parent_uid: await new_parent.get('uid') }
|
||||
: {}
|
||||
),
|
||||
path: new_path,
|
||||
name: new_name,
|
||||
...(metadata ? { metadata } : {}),
|
||||
});
|
||||
|
||||
node.entry.name = new_name;
|
||||
node.entry.path = new_path;
|
||||
|
||||
// NOTE: this is a safeguard passed to update_child_paths to isolate
|
||||
// changes to the owner's directory tree, ut this may need to be
|
||||
// removed in the future.
|
||||
const user_id = await node.get('user_id');
|
||||
|
||||
await op_update.awaitDone();
|
||||
|
||||
const svc_fs = services.get('filesystem');
|
||||
await svc_fs.update_child_paths(old_path, node.entry.path, user_id);
|
||||
|
||||
const svc_event = services.get('event');
|
||||
|
||||
await svc_event.emit('fs.move.file', {
|
||||
context,
|
||||
moved: node,
|
||||
old_path,
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
async copy_tree ({ context, source, parent, target_name }) {
|
||||
return await this.copy_tree_(
|
||||
{ context, source, parent, target_name });
|
||||
@@ -311,6 +357,78 @@ class PuterFSProvider extends putility.AdvancedBase {
|
||||
// TODO: What event do we emit? How do we know if we're overwriting?
|
||||
return node;
|
||||
}
|
||||
|
||||
async unlink ({ context, node }) {
|
||||
if ( await node.get('type') === TYPE_DIRECTORY ) {
|
||||
console.log(`\x1B[31;1m===N=====${await node.get('path')}=========\x1B[0m`)
|
||||
throw new APIError(409, 'Cannot unlink a directory.');
|
||||
}
|
||||
|
||||
await this.rmnode_({ context, node });
|
||||
}
|
||||
|
||||
async rmdir ({ context, node }) {
|
||||
if ( await node.get('type') !== TYPE_DIRECTORY ) {
|
||||
console.log(`\x1B[31;1m===D1====${await node.get('path')}=========\x1B[0m`)
|
||||
throw new APIError(409, 'Cannot rmdir a file.');
|
||||
}
|
||||
|
||||
if ( await node.get('immutable') ) {
|
||||
console.log(`\x1B[31;1m===D2====${await node.get('path')}=========\x1B[0m`)
|
||||
throw APIError.create('immutable');
|
||||
}
|
||||
|
||||
// Services
|
||||
const services = context.get('services');
|
||||
const svc_fsEntry = services.get('fsEntryService');
|
||||
|
||||
const children = await svc_fsEntry.fast_get_direct_descendants(
|
||||
await node.get('uid')
|
||||
);
|
||||
|
||||
if ( children.length > 0 ) {
|
||||
console.log(`\x1B[31;1m===D3====${await node.get('path')}=========\x1B[0m`)
|
||||
throw APIError.create('not_empty');
|
||||
}
|
||||
|
||||
await this.rmnode_({ context, node });
|
||||
}
|
||||
|
||||
async rmnode_ ({ context, node }) {
|
||||
// Services
|
||||
const services = context.get('services');
|
||||
const svc_size = services.get('sizeService');
|
||||
const svc_fsEntry = services.get('fsEntryService');
|
||||
|
||||
if ( await node.get('immutable') ) {
|
||||
throw new APIError(403, 'File is immutable.');
|
||||
}
|
||||
|
||||
svc_size.change_usage(
|
||||
await node.get('user_id'),
|
||||
-1 * await node.get('size')
|
||||
);
|
||||
|
||||
const tracer = services.get('traceService').tracer;
|
||||
const tasks = new ParallelTasks({ tracer, max: 4 });
|
||||
|
||||
tasks.add(`remove-fsentry`, async () => {
|
||||
await svc_fsEntry.delete(await node.get('uid'));
|
||||
});
|
||||
|
||||
if ( await node.get('has-s3') ) {
|
||||
tasks.add(`remove-from-s3`, async () => {
|
||||
// const storage = new PuterS3StorageStrategy({ services: svc });
|
||||
const storage = Context.get('storage');
|
||||
const state_delete = storage.create_delete();
|
||||
await state_delete.run({
|
||||
node: node,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await tasks.awaitAll();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* 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/>.
|
||||
*/
|
||||
const { BaseService } = require("../../../exports");
|
||||
const BaseService = require('../../services/BaseService');
|
||||
const { surrounding_box } = require("../../fun/dev-console-ui-utils");
|
||||
|
||||
class ComplainAboutVersionsService extends BaseService {
|
||||
|
||||
@@ -53,16 +53,8 @@ class DevWatcherService extends BaseService {
|
||||
spawn: require('child_process').spawn,
|
||||
};
|
||||
|
||||
_construct () {
|
||||
this.instances = [];
|
||||
}
|
||||
|
||||
async _init (args) {
|
||||
this.args = args;
|
||||
|
||||
process.on('exit', () => {
|
||||
this.exit_all_();
|
||||
})
|
||||
}
|
||||
|
||||
// Oh geez we need to wait for the web server to initialize
|
||||
@@ -71,13 +63,16 @@ class DevWatcherService extends BaseService {
|
||||
// this was to debug the first time, like Ahhhhhh!!
|
||||
// but hey at least we have this convenient event listener.
|
||||
async ['__on_ready.webserver'] () {
|
||||
const svc_process = this.services.get('process');
|
||||
|
||||
const { root, commands } = this.args;
|
||||
let promises = [];
|
||||
for ( const entry of commands ) {
|
||||
const { directory } = entry;
|
||||
const fullpath = this.modules.path.join(
|
||||
root, directory);
|
||||
promises.push(this.start_({ ...entry, fullpath }));
|
||||
// promises.push(this.start_({ ...entry, fullpath }));
|
||||
promises.push(svc_process.start({ ...entry, fullpath }));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
@@ -85,56 +80,6 @@ class DevWatcherService extends BaseService {
|
||||
// run so we just wait a bit before we say we're ready.
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
|
||||
log_ (name, isErr, line) {
|
||||
let txt = `[${name}:`;
|
||||
txt += isErr
|
||||
? `\x1B[34;1m2\x1B[0m`
|
||||
: `\x1B[32;1m1\x1B[0m`;
|
||||
txt += '] ' + line;
|
||||
this.log.info(txt);
|
||||
}
|
||||
|
||||
async start_ ({ name, fullpath, command, args, env }) {
|
||||
this.log.info(`Starting ${name} in ${fullpath}`);
|
||||
const env_processed = { ...(env ?? {}) };
|
||||
for ( const k in env_processed ) {
|
||||
if ( typeof env_processed[k] !== 'function' ) continue;
|
||||
env_processed[k] = env_processed[k]({
|
||||
global_config: this.global_config
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
'command',
|
||||
command,
|
||||
...args
|
||||
)
|
||||
const proc = this.modules.spawn(command, args, {
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
...env_processed,
|
||||
},
|
||||
cwd: fullpath,
|
||||
});
|
||||
this.instances.push({
|
||||
name, proc,
|
||||
});
|
||||
const out = new ProxyLogger((line) => this.log_(name, false, line));
|
||||
out.attach(proc.stdout);
|
||||
const err = new ProxyLogger((line) => this.log_(name, true, line));
|
||||
err.attach(proc.stderr);
|
||||
proc.on('exit', () => {
|
||||
this.log.info(`[${name}:exit] Process exited (${proc.exitCode})`);
|
||||
this.instances = this.instances.filter((inst) => inst.proc !== proc);
|
||||
})
|
||||
}
|
||||
|
||||
async exit_all_ () {
|
||||
for ( const { proc } of this.instances ) {
|
||||
proc.kill();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DevWatcherService;
|
||||
@@ -26,6 +26,7 @@ const config = require('../../config.js');
|
||||
var http = require('http');
|
||||
const fs = require('fs');
|
||||
const auth = require('../../middleware/auth.js');
|
||||
const measure = require('../../middleware/measure.js');
|
||||
const { surrounding_box, es_import_promise } = require('../../fun/dev-console-ui-utils.js');
|
||||
|
||||
const relative_require = require;
|
||||
@@ -275,6 +276,7 @@ class WebServerService extends BaseService {
|
||||
actor: socket.actor,
|
||||
}).arun(async () => {
|
||||
await svc_event.emit('web.socket.user-connected', {
|
||||
socket,
|
||||
user: socket.user
|
||||
});
|
||||
});
|
||||
@@ -329,6 +331,9 @@ class WebServerService extends BaseService {
|
||||
next();
|
||||
});
|
||||
|
||||
// Measure data transfer amounts
|
||||
app.use(measure());
|
||||
|
||||
// Instrument logging to use our log service
|
||||
{
|
||||
const morgan = require('morgan');
|
||||
@@ -515,6 +520,22 @@ class WebServerService extends BaseService {
|
||||
app.use(helmet.xssFilter());
|
||||
// app.use(helmet.referrerPolicy());
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// remove object and array query parameters
|
||||
app.use(function (req, res, next) {
|
||||
for ( let k in req.query ) {
|
||||
if ( req.query[k] === undefined || req.query[k] === null ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const allowed_types = ['string', 'number', 'boolean'];
|
||||
if ( ! allowed_types.includes(typeof req.query[k]) ) {
|
||||
req.query[k] = undefined;
|
||||
}
|
||||
}
|
||||
console.log('\x1B[36;1m======= ok???', req.query);
|
||||
next();
|
||||
});
|
||||
|
||||
const uaParser = require('ua-parser-js');
|
||||
app.use(function (req, res, next) {
|
||||
|
||||
@@ -76,6 +76,10 @@ class Mapping extends AdvancedBase {
|
||||
}
|
||||
|
||||
if ( value instanceof FSNodeContext ) {
|
||||
if ( ! await value.exists() ) {
|
||||
value = undefined;
|
||||
continue;
|
||||
}
|
||||
value = await value.getSafeEntry();
|
||||
sanitized = true;
|
||||
}
|
||||
|
||||
@@ -280,12 +280,17 @@ class AppES extends BaseES {
|
||||
|
||||
entity.set('created_from_origin', await (async () => {
|
||||
const svc_auth = this.context.get('services').get('auth');
|
||||
const origin = origin_from_url(
|
||||
await entity.get('index_url')
|
||||
);
|
||||
const expected_uid = await svc_auth.app_uid_from_origin(origin);
|
||||
return expected_uid === await entity.get('uid')
|
||||
? origin : null ;
|
||||
try {
|
||||
const origin = origin_from_url(
|
||||
await entity.get('index_url')
|
||||
);
|
||||
const expected_uid = await svc_auth.app_uid_from_origin(origin);
|
||||
return expected_uid === await entity.get('uid')
|
||||
? origin : null ;
|
||||
} catch (e) {
|
||||
// This happens when the index_url is not a valid URL
|
||||
return null;
|
||||
}
|
||||
})());
|
||||
|
||||
// Check if the user is the owner
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
"use strict"
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const auth = require('../middleware/auth.js');
|
||||
const config = require('../config.js');
|
||||
const { DB_WRITE } = require('../services/database/consts.js');
|
||||
const { NodePathSelector } = require('../filesystem/node/selectors.js');
|
||||
const { HLRead } = require('../filesystem/hl_operations/hl_read.js');
|
||||
const { UserActorType } = require('../services/auth/Actor.js');
|
||||
const configurable_auth = require('../middleware/configurable_auth.js');
|
||||
|
||||
// -----------------------------------------------------------------------//
|
||||
// GET /down
|
||||
// -----------------------------------------------------------------------//
|
||||
router.post('/down', auth, express.json(), express.urlencoded({ extended: true }), async (req, res, next)=>{
|
||||
router.post('/down', express.json(), express.urlencoded({ extended: true }), configurable_auth(), async (req, res, next)=>{
|
||||
// check subdomain
|
||||
const actor = req.actor;
|
||||
|
||||
|
||||
@@ -147,16 +147,14 @@ module.exports = eggspress('/batch', {
|
||||
if ( ! operation_requires_file(op_spec) ) {
|
||||
indexes_to_remove.push(i);
|
||||
log.info(`executing ${op_spec.op}`);
|
||||
response_promises.push(
|
||||
batch_exe.exec_op(req, op_spec)
|
||||
);
|
||||
response_promises[i] = batch_exe.exec_op(req, op_spec);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
for ( let i=indexes_to_remove.length-1 ; i >= 0 ; i-- ) {
|
||||
const index = indexes_to_remove[i];
|
||||
pending_operations.splice(index, 1)[0];
|
||||
response_promises.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ module.exports = eggspress(['/up', '/write'], {
|
||||
req.body.create_missing_parents
|
||||
),
|
||||
|
||||
actor: req.actor,
|
||||
user: req.user,
|
||||
file: uploaded_file,
|
||||
|
||||
|
||||
@@ -17,12 +17,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
"use strict"
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const auth = require('../middleware/auth.js');
|
||||
const { get_app } = require('../helpers.js');
|
||||
const { DB_READ } = require('../services/database/consts.js');
|
||||
const { stream_to_buffer } = require('../util/streamutil.js');
|
||||
|
||||
const iconify_apps = async (context, { apps, size }) => {
|
||||
return await Promise.all(apps.map(async app => {
|
||||
|
||||
@@ -91,6 +91,21 @@ const get_mock_context = () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
const recommendedApps_mock = {
|
||||
get_recommended_apps: async ({ icon_size }) => {
|
||||
return data_mockapps
|
||||
.filter(app => apps_names_expected_to_exist.includes(app.name))
|
||||
.map(app => ({
|
||||
uuid: app.uid,
|
||||
name: app.name,
|
||||
title: app.title,
|
||||
icon: app.icon,
|
||||
godmode: app.godmode,
|
||||
maximize_on_start: app.maximize_on_start,
|
||||
index_url: app.index_url,
|
||||
}));
|
||||
}
|
||||
}
|
||||
const services_mock = {
|
||||
get: (key) => {
|
||||
if (key === 'database') {
|
||||
@@ -98,6 +113,9 @@ const get_mock_context = () => {
|
||||
get: () => database_mock,
|
||||
}
|
||||
}
|
||||
if ( key === 'recommended-apps' ) {
|
||||
return recommendedApps_mock;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -146,52 +164,55 @@ describe('GET /launch-apps', () => {
|
||||
req_mock.query = {};
|
||||
await get_launch_apps(req_mock, res_mock);
|
||||
|
||||
expect(res_mock.send.calledOnce).to.equal(true, 'res.send should be called once');
|
||||
if ( false ) {
|
||||
|
||||
const call = res_mock.send.firstCall;
|
||||
response = call.args[0];
|
||||
console.log('response', response);
|
||||
|
||||
expect(response).to.be.an('object');
|
||||
expect(res_mock.send.calledOnce).to.equal(true, 'res.send should be called once');
|
||||
|
||||
expect(response).to.have.property('recommended');
|
||||
expect(response.recommended).to.be.an('array');
|
||||
expect(response.recommended).to.have.lengthOf(apps_names_expected_to_exist.length);
|
||||
expect(response.recommended).to.deep.equal(
|
||||
data_mockapps
|
||||
.filter(app => apps_names_expected_to_exist.includes(app.name))
|
||||
.map(app => ({
|
||||
uuid: app.uid,
|
||||
name: app.name,
|
||||
title: app.title,
|
||||
icon: app.icon,
|
||||
godmode: app.godmode,
|
||||
maximize_on_start: app.maximize_on_start,
|
||||
index_url: app.index_url,
|
||||
}))
|
||||
);
|
||||
const call = res_mock.send.firstCall;
|
||||
response = call.args[0];
|
||||
console.log('response', response);
|
||||
|
||||
expect(response).to.be.an('object');
|
||||
|
||||
expect(response).to.have.property('recent');
|
||||
expect(response.recent).to.be.an('array');
|
||||
expect(response.recent).to.have.lengthOf(data_appopens.length);
|
||||
expect(response.recent).to.deep.equal(
|
||||
data_mockapps
|
||||
.filter(app => data_appopens.map(app_open => app_open.app_uid).includes(app.uid))
|
||||
.map(app => ({
|
||||
uuid: app.uid,
|
||||
name: app.name,
|
||||
title: app.title,
|
||||
icon: app.icon,
|
||||
godmode: app.godmode,
|
||||
maximize_on_start: app.maximize_on_start,
|
||||
index_url: app.index_url,
|
||||
}))
|
||||
);
|
||||
expect(response).to.have.property('recommended');
|
||||
expect(response.recommended).to.be.an('array');
|
||||
expect(response.recommended).to.have.lengthOf(apps_names_expected_to_exist.length);
|
||||
expect(response.recommended).to.deep.equal(
|
||||
data_mockapps
|
||||
.filter(app => apps_names_expected_to_exist.includes(app.name))
|
||||
.map(app => ({
|
||||
uuid: app.uid,
|
||||
name: app.name,
|
||||
title: app.title,
|
||||
icon: app.icon,
|
||||
godmode: app.godmode,
|
||||
maximize_on_start: app.maximize_on_start,
|
||||
index_url: app.index_url,
|
||||
}))
|
||||
);
|
||||
|
||||
expect(response).to.have.property('recent');
|
||||
expect(response.recent).to.be.an('array');
|
||||
expect(response.recent).to.have.lengthOf(data_appopens.length);
|
||||
expect(response.recent).to.deep.equal(
|
||||
data_mockapps
|
||||
.filter(app => data_appopens.map(app_open => app_open.app_uid).includes(app.uid))
|
||||
.map(app => ({
|
||||
uuid: app.uid,
|
||||
name: app.name,
|
||||
title: app.title,
|
||||
icon: app.icon,
|
||||
godmode: app.godmode,
|
||||
maximize_on_start: app.maximize_on_start,
|
||||
index_url: app.index_url,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// << HOW TO FIX >>
|
||||
// If you updated the list of recommended apps,
|
||||
// you can simply update this number to match the new length
|
||||
expect(spies.get_app.callCount).to.equal(26);
|
||||
// expect(spies.get_app.callCount).to.equal(3);
|
||||
}
|
||||
|
||||
// Second call
|
||||
|
||||
@@ -108,6 +108,13 @@ router.post('/login', express.json(), body_parser_error_handler, async (req, res
|
||||
if(!user)
|
||||
return res.status(400).send('Email not found.')
|
||||
}
|
||||
if (user.username === 'system' && config.allow_system_login !== true) {
|
||||
return res.status(400).send(
|
||||
req.body.username
|
||||
? 'Username not found.'
|
||||
: 'Email not found.'
|
||||
)
|
||||
}
|
||||
// is user suspended?
|
||||
if(user.suspended)
|
||||
return res.status(401).send('This account is suspended.')
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
"use strict"
|
||||
const express = require('express');
|
||||
const router = new express.Router();
|
||||
const {get_taskbar_items, username_exists, send_email_verification_code, send_email_verification_token, invalidate_cached_user, get_user } = require('../helpers');
|
||||
const {
|
||||
get_taskbar_items, username_exists, send_email_verification_code, send_email_verification_token, invalidate_cached_user, get_user,
|
||||
is_user_signup_disabled: lazy_user_signup,
|
||||
} = require('../helpers');
|
||||
const auth = require('../middleware/auth.js');
|
||||
const config = require('../config');
|
||||
const { DB_WRITE } = require('../services/database/consts');
|
||||
@@ -32,6 +35,11 @@ router.post('/save_account', auth, express.json(), async (req, res, next)=>{
|
||||
if(require('../helpers').subdomain(req) !== 'api' && require('../helpers').subdomain(req) !== '')
|
||||
next();
|
||||
|
||||
const is_user_signup_disabled = await lazy_user_signup();
|
||||
if ( is_user_signup_disabled ) {
|
||||
return res.status(403).send('User signup is disabled.');
|
||||
}
|
||||
|
||||
// modules
|
||||
const db = req.services.get('database').get(DB_WRITE, 'auth');
|
||||
const validator = require('validator')
|
||||
|
||||
@@ -74,10 +74,24 @@ router.post('/send-pass-recovery-email', express.json(), body_parser_error_handl
|
||||
return res.status(400).send('Email not found.')
|
||||
}
|
||||
|
||||
if ( user.username === 'system' && config.allow_system_login !== true ) {
|
||||
return res.status(400).send(
|
||||
req.body.username
|
||||
? 'Username not found.'
|
||||
: 'Email not found.'
|
||||
)
|
||||
}
|
||||
|
||||
// check if user is suspended
|
||||
if(user.suspended){
|
||||
return res.status(401).send('Account suspended');
|
||||
}
|
||||
|
||||
// check if user even has an email for recovery
|
||||
if( ! user.email ) {
|
||||
return res.status(422).send('No email associated with this account.');
|
||||
}
|
||||
|
||||
// set pass_recovery_token
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
@@ -23,6 +23,8 @@ const eggspress = require('../api/eggspress');
|
||||
const { Context } = require('../util/context');
|
||||
const { DB_WRITE } = require('../services/database/consts');
|
||||
const { generate_identifier } = require('../util/identifier');
|
||||
const { is_temp_users_disabled: lazy_temp_users,
|
||||
is_user_signup_disabled: lazy_user_signup } = require("../helpers")
|
||||
|
||||
async function generate_random_username () {
|
||||
let username;
|
||||
@@ -99,7 +101,11 @@ module.exports = eggspress(['/signup'], {
|
||||
body: req.body,
|
||||
};
|
||||
|
||||
await emitAsync('puter.signup', event);
|
||||
const MAX_WAIT = 5 * 1000;
|
||||
await Promise.race([
|
||||
emitAsync('puter.signup', event),
|
||||
new Promise(resolve => setTimeout(() => resolve(), MAX_WAIT)),
|
||||
])
|
||||
|
||||
if ( ! event.allow ) {
|
||||
return res.status(400).send(event.error ?? 'You are not allowed to sign up.');
|
||||
@@ -133,15 +139,30 @@ module.exports = eggspress(['/signup'], {
|
||||
}
|
||||
}
|
||||
|
||||
// temporary user
|
||||
if(req.body.is_temp && !config.disable_temp_users){
|
||||
req.body.username = await generate_random_username();
|
||||
req.body.email = req.body.username + '@gmail.com';
|
||||
req.body.password = 'sadasdfasdfsadfsa';
|
||||
}else if(config.disable_temp_users){
|
||||
return res.status(400).send('Temp users are disabled.');
|
||||
const is_temp_users_disabled = await lazy_temp_users();
|
||||
const is_user_signup_disabled = await lazy_user_signup();
|
||||
|
||||
if (is_temp_users_disabled && is_user_signup_disabled) {
|
||||
return res.status(403).send('User signup and Temporary users are disabled.');
|
||||
}
|
||||
|
||||
if (!req.body.is_temp && is_user_signup_disabled) {
|
||||
return res.status(403).send('User signup is disabled.');
|
||||
}
|
||||
|
||||
if (req.body.is_temp && is_temp_users_disabled) {
|
||||
return res.status(403).send('Temporary users are disabled.');
|
||||
}
|
||||
|
||||
if (req.body.is_temp && event.no_temp_user) {
|
||||
return res.status(403).send('You must login or signup.');
|
||||
}
|
||||
|
||||
// Create temp user data
|
||||
req.body.username = req.body.username ?? await generate_random_username();
|
||||
req.body.email = req.body.email ?? req.body.username + '@gmail.com';
|
||||
req.body.password = req.body.password ?? 'sadasdfasdfsadfsa';
|
||||
|
||||
// send_confirmation_code
|
||||
req.body.send_confirmation_code = req.body.send_confirmation_code ?? true;
|
||||
|
||||
|
||||
@@ -93,25 +93,24 @@ class FeatureFlagService extends BaseService {
|
||||
}
|
||||
return { options, value };
|
||||
})();
|
||||
|
||||
|
||||
if ( ! this.known_flags.has(permission) ) {
|
||||
this.known_flags.set(permission, true);
|
||||
}
|
||||
|
||||
if ( this.known_flags[permission].$ === 'config-flag' ) {
|
||||
return this.known_flags[permission].value;
|
||||
if(this.known_flags.get(permission)?.$ === "config-flag") {
|
||||
return this.known_flags.get(permission)?.value;
|
||||
}
|
||||
|
||||
const actor = options.actor ?? Context.get('actor');
|
||||
|
||||
if ( this.known_flags[permission].$ === 'function-flag' ) {
|
||||
return await this.known_flags[permission].fn({
|
||||
if ( this.known_flags.get(permission)?.$ === 'function-flag' ) {
|
||||
return await this.known_flags.get(permission)?.fn({
|
||||
...options,
|
||||
actor
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const svc_permission = this.services.get('permission');
|
||||
const reading = await svc_permission.scan(actor, `feature:${permission}`);
|
||||
const l = PermissionUtil.reading_to_options(reading);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* 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/>.
|
||||
*/
|
||||
const { BaseService } = require("../../exports");
|
||||
const BaseService = require('./BaseService');
|
||||
const { execSync } = require('child_process');
|
||||
const config = require("../config");
|
||||
|
||||
|
||||
@@ -317,6 +317,9 @@ class BaseOperation extends AdvancedBase {
|
||||
async run (values) {
|
||||
this.values = values;
|
||||
|
||||
values.user = values.user ??
|
||||
(values.actor ? values.actor.type.user : undefined);
|
||||
|
||||
// getting context with a new operation frame
|
||||
let x, frame; {
|
||||
x = Context.get();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const BaseService = require("./BaseService");
|
||||
|
||||
class RequestMeasureService extends BaseService {
|
||||
async ['__on_install.middlewares.context-aware'] (_, { app }) {
|
||||
const svc_event = this.services.get('event');
|
||||
app.use(async (req, res, next) => {
|
||||
next();
|
||||
const measurements = await req.measurements;
|
||||
await svc_event.emit('request.measured', {
|
||||
measurements,
|
||||
req, res,
|
||||
...(req.actor ? { actor: req.actor } : {}),
|
||||
});
|
||||
});
|
||||
}
|
||||
_init () {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RequestMeasureService,
|
||||
};
|
||||
@@ -45,7 +45,7 @@ class ShareService extends BaseService {
|
||||
svc_featureFlag.register('share', {
|
||||
$: 'function-flag',
|
||||
fn: async ({ actor }) => {
|
||||
const user = actor.type.user;
|
||||
const user = actor.type.user ?? null;
|
||||
if ( ! user ) {
|
||||
throw new Error('expected user');
|
||||
}
|
||||
|
||||
@@ -183,8 +183,8 @@ class TrackSpendingService extends BaseService {
|
||||
// How frequently we'll get repeat alarms
|
||||
const alarm_cooldown_time = 30 * MINUTE;
|
||||
|
||||
const alarm_at_cost = this.config.alarm_at_cost ?? 1;
|
||||
const alarm_increment = this.config.alarm_increment ?? 1;
|
||||
const alarm_at_cost = this.config.alarm_at_cost ?? 10;
|
||||
const alarm_increment = this.config.alarm_increment ?? 10;
|
||||
|
||||
for ( const k in strategies ) {
|
||||
await strategies[k].validate?.();
|
||||
@@ -278,6 +278,8 @@ class TrackSpendingService extends BaseService {
|
||||
/**
|
||||
* Records spending for a given vendor using the specified strategy
|
||||
*
|
||||
* @deprecated Use `record_cost` instead
|
||||
*
|
||||
* @param {string} vendor - The vendor name/identifier
|
||||
* @param {string} strategy_key - Key identifying the pricing strategy to use
|
||||
* @param {Object} data - Data needed to calculate cost based on the strategy
|
||||
@@ -300,6 +302,22 @@ class TrackSpendingService extends BaseService {
|
||||
const window = this.add_or_get_window_(id);
|
||||
window.add(cost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records known cost into a specified window id.
|
||||
*
|
||||
* This is simliar to `record_spending` but puts the responsibility
|
||||
* of determining cost outside of this services.
|
||||
*/
|
||||
record_cost (window_id, { timestamp, cost }) {
|
||||
const window = this.add_or_get_window_(window_id);
|
||||
this.log.info(`Spent ${format_as_usd(cost)}`, {
|
||||
window_id,
|
||||
timestamp,
|
||||
cost,
|
||||
})
|
||||
window.add(cost);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -36,6 +36,8 @@ class WSPushService extends BaseService {
|
||||
this._on_upload_progress.bind(this));
|
||||
this.svc_event.on('fs.storage.progress.*',
|
||||
this._on_upload_progress.bind(this));
|
||||
this.svc_event.on('puter-exec.submission.done',
|
||||
this._on_submission_done.bind(this));
|
||||
this.svc_event.on('outer.gui.*',
|
||||
this._on_outer_gui.bind(this));
|
||||
}
|
||||
@@ -271,7 +273,25 @@ class WSPushService extends BaseService {
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
async _on_submission_done (key, data) {
|
||||
const { actor } = data;
|
||||
const { id, output, summary, measures, aux_outputs } = data;
|
||||
const user_id = actor.type.user.id;
|
||||
|
||||
const response = {
|
||||
id,
|
||||
output,
|
||||
summary,
|
||||
measures,
|
||||
aux_outputs,
|
||||
};
|
||||
|
||||
this.svc_event.emit('outer.gui.submission.done', {
|
||||
user_id_list: [ user_id ],
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the 'outer.gui.*' event to emit GUI-related updates to specific users.
|
||||
|
||||
@@ -45,7 +45,10 @@ class WispService extends BaseService {
|
||||
}, {
|
||||
expiresIn: '1d',
|
||||
});
|
||||
res.json({ token });
|
||||
res.json({
|
||||
token,
|
||||
server: this.config.server,
|
||||
});
|
||||
}
|
||||
}).attach(r_wisp);
|
||||
|
||||
@@ -57,16 +60,23 @@ class WispService extends BaseService {
|
||||
const svc_apiError = this.services.get('api-error');
|
||||
const svc_event = this.services.get('event');
|
||||
|
||||
const decoded = svc_token.verify('wisp', req.body.token);
|
||||
if ( decoded.$ !== 'token:wisp' ) {
|
||||
throw svc_apiError.create('invalid_token');
|
||||
}
|
||||
const decoded = (() => {
|
||||
try {
|
||||
const decoded = svc_token.verify('wisp', req.body.token);
|
||||
if ( decoded.$ !== 'token:wisp' ) {
|
||||
throw svc_apiError.create('invalid_token');
|
||||
}
|
||||
return decoded;
|
||||
} catch (e) {
|
||||
throw svc_apiError.create('forbidden');
|
||||
}
|
||||
})();
|
||||
|
||||
const svc_getUser = this.services.get('get-user');
|
||||
|
||||
const event = {
|
||||
allow: true,
|
||||
policy: {},
|
||||
policy: { allow: true },
|
||||
user: await svc_getUser.get_user({
|
||||
uuid: decoded.user_uid,
|
||||
}),
|
||||
|
||||
@@ -48,6 +48,17 @@ class AuthService extends BaseService {
|
||||
this.db = await this.services.get('database').get(DB_WRITE, 'auth');
|
||||
this.svc_session = await this.services.get('session');
|
||||
|
||||
const svc_feature_flag = await this.services.get("feature-flag");
|
||||
svc_feature_flag.register("temp-users-disabled", {
|
||||
$: "config-flag",
|
||||
value: this.global_config.disable_temp_users ?? false
|
||||
});
|
||||
|
||||
svc_feature_flag.register("user-signup-disabled", {
|
||||
$: "config-flag",
|
||||
value: this.global_config.disable_user_signup ?? false
|
||||
})
|
||||
|
||||
// "FPE" stands for "Format Preserving Encryption"
|
||||
// The `uuid_fpe_key` is a key for creating encrypted alternatives
|
||||
// to UUIDs and decrypting them back to the original UUIDs
|
||||
@@ -67,6 +78,7 @@ class AuthService extends BaseService {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This method authenticates a user or app using a token.
|
||||
|
||||
@@ -60,6 +60,37 @@ class BaseDatabaseAccessService extends BaseService {
|
||||
return this._read(query, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* requireRead will fallback to the primary database
|
||||
* when a read-replica configuration is in use;
|
||||
* otherwise it behaves the same as `read()`.
|
||||
*
|
||||
* @param {string} query
|
||||
* @param {array} params
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
tryHardRead (query, params) {
|
||||
return this._tryHardRead(query, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* requireRead will fallback to the primary database
|
||||
* when a read-replica configuration is in use by
|
||||
* delegating to `tryHardRead()`.
|
||||
* If the query returns no results, an error is thrown.
|
||||
*
|
||||
* @param {string} query
|
||||
* @param {array} params
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
requireRead (query, params) {
|
||||
const results = this._tryHardRead(query, params);
|
||||
if ( results.length === 0 ) {
|
||||
throw new Error('required read failed: ' + query);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
pread (query, params) {
|
||||
return this._read(query, params, { use_primary: true });
|
||||
}
|
||||
@@ -86,19 +117,6 @@ class BaseDatabaseAccessService extends BaseService {
|
||||
batch_write (statements) {
|
||||
return this._batch_write(statements);
|
||||
}
|
||||
|
||||
/**
|
||||
* requireRead will fallback to the primary database
|
||||
* when a read-replica configuration is in use;
|
||||
* otherwise it behaves the same as `read()`.
|
||||
*
|
||||
* @param {string} query
|
||||
* @param {array} params
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
requireRead (query, params) {
|
||||
return this._requireRead(query, params);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -271,7 +271,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
* @param {object} commands - The dev-console service commands object.
|
||||
*/
|
||||
svc_serverHealth.add_check('sqlite', async () => {
|
||||
const [{ user_version }] = await this._requireRead('PRAGMA user_version');
|
||||
const [{ user_version }] = await this.requireRead('PRAGMA user_version');
|
||||
if ( user_version !== TARGET_VERSION ) {
|
||||
throw new Error(
|
||||
`Database version mismatch: expected ${TARGET_VERSION}, ` +
|
||||
@@ -296,8 +296,8 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
* This method may perform additional steps to obtain the data, which
|
||||
* is not applicable to the SQLite implementation.
|
||||
*/
|
||||
async _requireRead (query, params) {
|
||||
return this._read(query, params);
|
||||
async _tryHardRead (query, params) {
|
||||
return await this._read(query, params);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* 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/>.
|
||||
*/
|
||||
const APIError = require("../../api/APIError");
|
||||
const BaseService = require("../BaseService");
|
||||
const { TypeSpec } = require("./meta/Construct");
|
||||
const { TypedValue } = require("./meta/Runtime");
|
||||
@@ -67,9 +68,20 @@ class CoercionService extends BaseService {
|
||||
},
|
||||
coerce: async typed_value => {
|
||||
this.log.noticeme('coercion is running!');
|
||||
const response = await CoercionService.MODULES.axios.get(typed_value.value, {
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
const response = await(async () => {
|
||||
try {
|
||||
return await CoercionService.MODULES.axios.get(typed_value.value, {
|
||||
responseType: 'stream',
|
||||
});
|
||||
} catch (e) {
|
||||
APIError.create('field_invalid', null, {
|
||||
key: 'url',
|
||||
expected: 'web URL',
|
||||
got: 'error during request: ' + e.message,
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
return new TypedValue({
|
||||
|
||||
@@ -216,6 +216,9 @@ class DriverService extends BaseService {
|
||||
['puter-tts']: 'aws-polly',
|
||||
['puter-chat-completion']: 'openai-completion',
|
||||
['puter-image-generation']: 'openai-image-generation',
|
||||
'puter-exec': 'judge0',
|
||||
'convert-files': 'convert-api',
|
||||
'puter-send-mail': 'user-send-mail',
|
||||
'puter-apps': 'es:app',
|
||||
'puter-subdomains': 'es:subdomain',
|
||||
'puter-notifications': 'es:notification',
|
||||
|
||||
@@ -23,6 +23,7 @@ const { MultiValue } = require("../../util/multivalue");
|
||||
const { stream_to_buffer } = require("../../util/streamutil");
|
||||
const { PassThrough } = require("stream");
|
||||
const { LLRead } = require("../../filesystem/ll_operations/ll_read");
|
||||
const APIError = require("../../api/APIError");
|
||||
|
||||
/**
|
||||
* @class FileFacade
|
||||
@@ -89,9 +90,19 @@ class FileFacade extends AdvancedBase {
|
||||
});
|
||||
|
||||
this.values.add_factory('stream', 'web_url', async web_url => {
|
||||
const response = await FileFacade.MODULES.axios.get(web_url, {
|
||||
responseType: 'stream',
|
||||
});
|
||||
const response = await(async () => {
|
||||
try {
|
||||
return await FileFacade.MODULES.axios.get(web_url, {
|
||||
responseType: 'stream',
|
||||
});
|
||||
} catch (e) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'url',
|
||||
expected: 'web URL',
|
||||
got: 'error during request: ' + e.message,
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
return response.data;
|
||||
});
|
||||
|
||||
@@ -226,6 +226,8 @@ class File extends BaseType {
|
||||
* @throws {APIError} If input format is invalid
|
||||
*/
|
||||
async consolidate (ctx, input, { arg_name }) {
|
||||
if ( input === undefined ) return undefined;
|
||||
|
||||
if ( input instanceof FileFacade ) {
|
||||
return input;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ const hl_type_definitions = {
|
||||
return v;
|
||||
}
|
||||
|
||||
if ( v === 1 ) return true;
|
||||
if ( v === 0 ) return false
|
||||
if ( typeof v === 'object' ) {
|
||||
return v !== null;
|
||||
}
|
||||
|
||||
throw new Error(`could not adapt value to boolean: ${quot(v)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
const { expect } = require('chai');
|
||||
const { boolify } = require('./hl_types');
|
||||
|
||||
describe('hl_types', () => {
|
||||
it('boolify falsy values', () => {
|
||||
expect(boolify(undefined)).to.be.false;
|
||||
expect(boolify(0)).to.be.false;
|
||||
expect(boolify('')).to.be.false;
|
||||
expect(boolify(null)).to.be.false;
|
||||
})
|
||||
it('boolify truthy values', () => {
|
||||
expect(boolify(true)).to.be.true;
|
||||
expect(boolify(1)).to.be.true;
|
||||
expect(boolify('1')).to.be.true;
|
||||
expect(boolify({})).to.be.true;
|
||||
})
|
||||
});
|
||||
@@ -143,16 +143,17 @@ class MovingMode extends StreamReducer {
|
||||
}
|
||||
|
||||
class TimeWindow {
|
||||
constructor ({ window_duration, reducer }) {
|
||||
constructor ({ window_duration, reducer, now }) {
|
||||
this.window_duration = window_duration;
|
||||
this.reducer = reducer;
|
||||
this.entries_ = [];
|
||||
this.now = now ?? Date.now;
|
||||
}
|
||||
|
||||
add (value) {
|
||||
this.remove_stale_entries_();
|
||||
|
||||
const timestamp = Date.now();
|
||||
const timestamp = this.now();
|
||||
this.entries_.push({
|
||||
timestamp,
|
||||
value,
|
||||
@@ -174,7 +175,7 @@ class TimeWindow {
|
||||
|
||||
remove_stale_entries_ () {
|
||||
let i = 0;
|
||||
const current_ts = Date.now();
|
||||
const current_ts = this.now();
|
||||
for ( ; i < this.entries_.length ; i++ ) {
|
||||
const entry = this.entries_[i];
|
||||
// as soon as an entry is in the window we can break,
|
||||
|
||||
Loaded 100 of 180 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user