A local-first private AI assistant for everyday use. Runs on-device models with encrypted P2P sync, and supports sharing chats publicly on ATProto.
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

Added chat log persistence

Implemented chatdb for chat persistence

authored by

Anandu Pavanan and committed by
GitHub
ef9f7168 4be75326

+1425 -520
+133 -32
Cargo.lock
··· 1433 1433 ] 1434 1434 1435 1435 [[package]] 1436 + name = "fallible-iterator" 1437 + version = "0.3.0" 1438 + source = "registry+https://github.com/rust-lang/crates.io-index" 1439 + checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 1440 + 1441 + [[package]] 1442 + name = "fallible-streaming-iterator" 1443 + version = "0.1.9" 1444 + source = "registry+https://github.com/rust-lang/crates.io-index" 1445 + checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 1446 + 1447 + [[package]] 1436 1448 name = "fastant" 1437 1449 version = "0.1.11" 1438 1450 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1525 1537 version = "0.1.5" 1526 1538 source = "registry+https://github.com/rust-lang/crates.io-index" 1527 1539 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 1540 + 1541 + [[package]] 1542 + name = "foldhash" 1543 + version = "0.2.0" 1544 + source = "registry+https://github.com/rust-lang/crates.io-index" 1545 + checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" 1528 1546 1529 1547 [[package]] 1530 1548 name = "foreign-types" ··· 1892 1910 dependencies = [ 1893 1911 "allocator-api2", 1894 1912 "equivalent", 1895 - "foldhash", 1913 + "foldhash 0.1.5", 1896 1914 ] 1897 1915 1898 1916 [[package]] ··· 1900 1918 version = "0.16.1" 1901 1919 source = "registry+https://github.com/rust-lang/crates.io-index" 1902 1920 checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 1921 + dependencies = [ 1922 + "foldhash 0.2.0", 1923 + ] 1924 + 1925 + [[package]] 1926 + name = "hashlink" 1927 + version = "0.11.0" 1928 + source = "registry+https://github.com/rust-lang/crates.io-index" 1929 + checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" 1930 + dependencies = [ 1931 + "hashbrown 0.16.1", 1932 + ] 1903 1933 1904 1934 [[package]] 1905 1935 name = "heck" ··· 2274 2304 2275 2305 [[package]] 2276 2306 name = "ipnet" 2277 - version = "2.11.0" 2307 + version = "2.12.0" 2278 2308 source = "registry+https://github.com/rust-lang/crates.io-index" 2279 - checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 2309 + checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" 2280 2310 2281 2311 [[package]] 2282 2312 name = "iri-string" ··· 2321 2351 2322 2352 [[package]] 2323 2353 name = "js-sys" 2324 - version = "0.3.90" 2354 + version = "0.3.91" 2325 2355 source = "registry+https://github.com/rust-lang/crates.io-index" 2326 - checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" 2356 + checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" 2327 2357 dependencies = [ 2328 2358 "once_cell", 2329 2359 "wasm-bindgen", ··· 2463 2493 2464 2494 [[package]] 2465 2495 name = "libredox" 2466 - version = "0.1.12" 2496 + version = "0.1.14" 2467 2497 source = "registry+https://github.com/rust-lang/crates.io-index" 2468 - checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" 2498 + checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" 2469 2499 dependencies = [ 2470 - "bitflags", 2471 2500 "libc", 2501 + ] 2502 + 2503 + [[package]] 2504 + name = "libsqlite3-sys" 2505 + version = "0.36.0" 2506 + source = "registry+https://github.com/rust-lang/crates.io-index" 2507 + checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" 2508 + dependencies = [ 2509 + "cc", 2510 + "pkg-config", 2511 + "vcpkg", 2472 2512 ] 2473 2513 2474 2514 [[package]] ··· 3067 3107 3068 3108 [[package]] 3069 3109 name = "pin-project" 3070 - version = "1.1.10" 3110 + version = "1.1.11" 3071 3111 source = "registry+https://github.com/rust-lang/crates.io-index" 3072 - checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" 3112 + checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" 3073 3113 dependencies = [ 3074 3114 "pin-project-internal", 3075 3115 ] 3076 3116 3077 3117 [[package]] 3078 3118 name = "pin-project-internal" 3079 - version = "1.1.10" 3119 + version = "1.1.11" 3080 3120 source = "registry+https://github.com/rust-lang/crates.io-index" 3081 - checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" 3121 + checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" 3082 3122 dependencies = [ 3083 3123 "proc-macro2", 3084 3124 "quote", ··· 3087 3127 3088 3128 [[package]] 3089 3129 name = "pin-project-lite" 3090 - version = "0.2.16" 3130 + version = "0.2.17" 3091 3131 source = "registry+https://github.com/rust-lang/crates.io-index" 3092 - checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 3132 + checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" 3093 3133 3094 3134 [[package]] 3095 3135 name = "pin-utils" ··· 3099 3139 3100 3140 [[package]] 3101 3141 name = "piper" 3102 - version = "0.2.4" 3142 + version = "0.2.5" 3103 3143 source = "registry+https://github.com/rust-lang/crates.io-index" 3104 - checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" 3144 + checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" 3105 3145 dependencies = [ 3106 3146 "atomic-waker", 3107 3147 "fastrand", ··· 3527 3567 ] 3528 3568 3529 3569 [[package]] 3570 + name = "rsqlite-vfs" 3571 + version = "0.1.0" 3572 + source = "registry+https://github.com/rust-lang/crates.io-index" 3573 + checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" 3574 + dependencies = [ 3575 + "hashbrown 0.16.1", 3576 + "thiserror 2.0.18", 3577 + ] 3578 + 3579 + [[package]] 3530 3580 name = "rstest" 3531 3581 version = "0.25.0" 3532 3582 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3557 3607 ] 3558 3608 3559 3609 [[package]] 3610 + name = "rusqlite" 3611 + version = "0.38.0" 3612 + source = "registry+https://github.com/rust-lang/crates.io-index" 3613 + checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" 3614 + dependencies = [ 3615 + "bitflags", 3616 + "fallible-iterator", 3617 + "fallible-streaming-iterator", 3618 + "hashlink", 3619 + "libsqlite3-sys", 3620 + "smallvec", 3621 + "sqlite-wasm-rs", 3622 + ] 3623 + 3624 + [[package]] 3625 + name = "rusqlite_migration" 3626 + version = "2.4.1" 3627 + source = "registry+https://github.com/rust-lang/crates.io-index" 3628 + checksum = "67ffe0efe8568c769d1e39c67ea2a093134afaac9bb4559246a2ee7a4d686044" 3629 + dependencies = [ 3630 + "log", 3631 + "rusqlite", 3632 + ] 3633 + 3634 + [[package]] 3560 3635 name = "rustc_version" 3561 3636 version = "0.4.1" 3562 3637 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3980 4055 ] 3981 4056 3982 4057 [[package]] 4058 + name = "sqlite-wasm-rs" 4059 + version = "0.5.2" 4060 + source = "registry+https://github.com/rust-lang/crates.io-index" 4061 + checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" 4062 + dependencies = [ 4063 + "cc", 4064 + "js-sys", 4065 + "rsqlite-vfs", 4066 + "wasm-bindgen", 4067 + ] 4068 + 4069 + [[package]] 3983 4070 name = "stable_deref_trait" 3984 4071 version = "1.2.1" 3985 4072 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4178 4265 "keyring", 4179 4266 "owo-colors", 4180 4267 "reqwest", 4268 + "rusqlite", 4269 + "rusqlite_migration", 4181 4270 "rustyline", 4182 4271 "semver", 4183 4272 "serde", ··· 4186 4275 "tilekit", 4187 4276 "tokio", 4188 4277 "toml 1.0.3+spec-1.1.0", 4278 + "uuid", 4189 4279 "wiremock", 4190 4280 ] 4191 4281 ··· 4227 4317 4228 4318 [[package]] 4229 4319 name = "tokio-macros" 4230 - version = "2.6.0" 4320 + version = "2.6.1" 4231 4321 source = "registry+https://github.com/rust-lang/crates.io-index" 4232 - checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 4322 + checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" 4233 4323 dependencies = [ 4234 4324 "proc-macro2", 4235 4325 "quote", ··· 4591 4681 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 4592 4682 4593 4683 [[package]] 4684 + name = "uuid" 4685 + version = "1.21.0" 4686 + source = "registry+https://github.com/rust-lang/crates.io-index" 4687 + checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" 4688 + dependencies = [ 4689 + "getrandom 0.4.1", 4690 + "js-sys", 4691 + "wasm-bindgen", 4692 + ] 4693 + 4694 + [[package]] 4594 4695 name = "valuable" 4595 4696 version = "0.1.1" 4596 4697 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4668 4769 4669 4770 [[package]] 4670 4771 name = "wasm-bindgen" 4671 - version = "0.2.113" 4772 + version = "0.2.114" 4672 4773 source = "registry+https://github.com/rust-lang/crates.io-index" 4673 - checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" 4774 + checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" 4674 4775 dependencies = [ 4675 4776 "cfg-if", 4676 4777 "once_cell", ··· 4681 4782 4682 4783 [[package]] 4683 4784 name = "wasm-bindgen-futures" 4684 - version = "0.4.63" 4785 + version = "0.4.64" 4685 4786 source = "registry+https://github.com/rust-lang/crates.io-index" 4686 - checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a" 4787 + checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" 4687 4788 dependencies = [ 4688 4789 "cfg-if", 4689 4790 "futures-util", ··· 4695 4796 4696 4797 [[package]] 4697 4798 name = "wasm-bindgen-macro" 4698 - version = "0.2.113" 4799 + version = "0.2.114" 4699 4800 source = "registry+https://github.com/rust-lang/crates.io-index" 4700 - checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" 4801 + checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" 4701 4802 dependencies = [ 4702 4803 "quote", 4703 4804 "wasm-bindgen-macro-support", ··· 4705 4806 4706 4807 [[package]] 4707 4808 name = "wasm-bindgen-macro-support" 4708 - version = "0.2.113" 4809 + version = "0.2.114" 4709 4810 source = "registry+https://github.com/rust-lang/crates.io-index" 4710 - checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" 4811 + checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" 4711 4812 dependencies = [ 4712 4813 "bumpalo", 4713 4814 "proc-macro2", ··· 4718 4819 4719 4820 [[package]] 4720 4821 name = "wasm-bindgen-shared" 4721 - version = "0.2.113" 4822 + version = "0.2.114" 4722 4823 source = "registry+https://github.com/rust-lang/crates.io-index" 4723 - checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" 4824 + checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" 4724 4825 dependencies = [ 4725 4826 "unicode-ident", 4726 4827 ] ··· 4774 4875 4775 4876 [[package]] 4776 4877 name = "web-sys" 4777 - version = "0.3.90" 4878 + version = "0.3.91" 4778 4879 source = "registry+https://github.com/rust-lang/crates.io-index" 4779 - checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97" 4880 + checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" 4780 4881 dependencies = [ 4781 4882 "js-sys", 4782 4883 "wasm-bindgen", ··· 5298 5399 5299 5400 [[package]] 5300 5401 name = "zlib-rs" 5301 - version = "0.6.2" 5402 + version = "0.6.3" 5302 5403 source = "registry+https://github.com/rust-lang/crates.io-index" 5303 - checksum = "c745c48e1007337ed136dc99df34128b9faa6ed542d80a1c673cf55a6d7236c8" 5404 + checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" 5304 5405 5305 5406 [[package]] 5306 5407 name = "zmij"
+1
modelfiles/qwen
··· 1 + FROM mlx-community/Qwen3.5-4B-MLX-4bit
+1 -1
server/api.py
··· 86 86 @app.post("/v1/responses") 87 87 async def create_chat_response(request: ResponsesRequest): 88 88 """ 89 - Create a response with openResponse format 89 + Create a response with openResponses format 90 90 """ 91 91 92 92 if request.stream:
+96 -69
server/backend/mlx.py
··· 13 13 Role, 14 14 SystemContent, 15 15 ) 16 - from openresponses_types import ReasoningEffortEnum 16 + from openresponses_types import AssistantMessageItemParam, ReasoningEffortEnum 17 17 from openresponses_types.types import ( 18 18 DeveloperMessageItemParam, 19 19 Error, 20 20 IncompleteDetails, 21 21 UserMessageItemParam, 22 22 ) 23 + 24 + from ..reasoning_utils import ReasoningExtractor 23 25 24 26 from ..cache_utils import get_model_path 25 27 from ..hf_downloader import pull_model ··· 224 226 if not prev_id: 225 227 return user_input 226 228 227 - prev_id = json.loads(prev_id) 228 - 229 229 prev = _responses.get(prev_id) # pyright: ignore 230 230 231 231 if not prev or not getattr(prev, "output", None): ··· 295 295 296 296 297 297 def handle_response_input(request: ResponsesRequest): 298 - dev_msg_item = None 299 298 user_msg_item = None 300 299 user_input_content = "" 300 + 301 301 if isinstance(request.input, str): 302 302 user_input_content = request.input 303 303 else: 304 - for item in request.input: 305 - match item: 306 - case UserMessageItemParam(): 307 - user_msg_item = item 308 - user_input_content = item.content.root # pyright: ignore 309 - case DeveloperMessageItemParam(): 310 - dev_msg_item = item 311 - case _: 312 - raise TypeError("unknown type") 313 - return [user_input_content, user_msg_item, dev_msg_item] 304 + user_msg_item = request.input[-1] 305 + user_input_content = user_msg_item.content.root 306 + return user_input_content 314 307 315 308 316 309 async def generate_response_chat_stream( 317 310 request: ResponsesRequest, 318 311 ) -> AsyncGenerator[str, None]: 319 - """Generate streaming chat responses for Responses API.""" 312 + """Generate streaming chat responses for OpenResponses API.""" 320 313 model = request.model 321 314 created = int(time.time()) 322 315 runner = get_or_load_model(model) 323 316 metrics = None 324 317 325 318 user_input_content = "" 326 - 327 - dev_msg_item = None 328 - user_msg_item = None 329 - [user_input_content, user_msg_item, dev_msg_item] = handle_response_input(request) 330 - user_input_content = _prepend_previous_response( 331 - user_input_content, request.previous_response_id 332 - ) 319 + convo: Conversation | None = None 320 + user_input_content = handle_response_input(request) 321 + if is_harmony_family(model): 333 322 334 - reasoning_effort = get_reasoning_effort(request.reasoning.effort) 323 + reasoning_effort = get_reasoning_effort(request.reasoning.effort) 335 324 336 - convo = build_harmony_conversation( 337 - reasoning_effort, dev_msg_item, user_input_content 338 - ) 325 + convo = build_harmony_conversation( 326 + reasoning_effort, request.input # pyright: ignore 327 + ) 339 328 340 329 input_tokens = len(runner.tokenizer.encode(user_input_content)) # pyright: ignore 341 330 ··· 365 354 error = None 366 355 incomplete_details = None 367 356 has_answer_started: bool = False 368 - # TODO: we need to inject the context prepending, else model is losing it. 369 357 try: 370 - for token in runner.generate_streaming_gpt( 371 - conversation=convo, 372 - max_tokens=runner.get_effective_max_tokens(request.max_output_tokens), 373 - temperature=request.temperature or 1, 374 - top_p=request.top_p or 1, 375 - ): 358 + 359 + # TODO: Add the turn convo context for non-harmony models too 360 + iterator: Iterator | None = None 361 + if is_harmony_family(model): 362 + iterator = runner.generate_streaming_gpt( 363 + conversation=convo, # pyright: ignore 364 + max_tokens=runner.get_effective_max_tokens(request.max_output_tokens), 365 + temperature=request.temperature or 1, 366 + top_p=request.top_p or 1, 367 + ) 368 + else: 369 + iterator = runner.generate_streaming( 370 + prompt=user_input_content, # pyright: ignore 371 + max_tokens=runner.get_effective_max_tokens(request.max_output_tokens), 372 + temperature=request.temperature or 1, 373 + top_p=request.top_p or 1, 374 + ) 375 + for token in iterator: # pyright: ignore 376 376 if isinstance(token, GenerationMetrics): 377 377 metrics = token 378 378 continue ··· 495 495 496 496 user_input_content = "" 497 497 498 - dev_msg_item = None 499 - user_msg_item = None 500 - [user_input_content, user_msg_item, dev_msg_item] = handle_response_input(request) 501 - user_input_content = _prepend_previous_response( 502 - user_input_content, request.previous_response_id 503 - ) 498 + user_input_content = handle_response_input(request) 504 499 505 500 reasoning_effort = get_reasoning_effort(request.reasoning.effort) 506 - 507 - convo = build_harmony_conversation( 508 - reasoning_effort, dev_msg_item, user_input_content 509 - ) 501 + convo: Conversation | None = None 502 + if is_harmony_family(model): 503 + convo = build_harmony_conversation( 504 + reasoning_effort, request.input # pyright: ignore 505 + ) 510 506 511 507 metrics_obj = None 512 508 error = None 513 509 incomplete_details = None 514 510 515 511 try: 512 + generated_text = "" 516 513 start_time = time.time() 517 - generated_text = runner.generate_batch_gpt( 518 - conversation=convo, 519 - max_tokens=runner.get_effective_max_tokens(request.max_output_tokens), 520 - temperature=request.temperature or 1, 521 - top_p=request.top_p or 1, 522 - use_chat_template=True, 523 - ) 524 - 514 + if is_harmony_family(model): 515 + runner.generate_batch_gpt( 516 + conversation=convo, # pyright: ignore 517 + max_tokens=runner.get_effective_max_tokens(request.max_output_tokens), 518 + temperature=request.temperature or 1, 519 + top_p=request.top_p or 1, 520 + use_chat_template=True, 521 + ) 522 + else: 523 + runner.generate_batch( 524 + prompt=user_input_content, # pyright: ignore 525 + max_tokens=runner.get_effective_max_tokens(request.max_output_tokens), 526 + temperature=request.temperature or 1, 527 + top_p=request.top_p or 1, 528 + use_chat_template=True, 529 + ) 525 530 # Metrics for batch generation (approximate) 526 531 generation_time = time.time() - start_time 527 532 ··· 535 540 metrics_obj = { 536 541 "ttft_ms": generation_time * 1000.0, 537 542 "total_tokens": output_tokens, 538 - "tokens_per_second": (output_tokens / generation_time) 539 - if generation_time > 0 540 - else 0.0, 543 + "tokens_per_second": ( 544 + (output_tokens / generation_time) if generation_time > 0 else 0.0 545 + ), 541 546 "total_latency_s": generation_time, 542 547 } 543 548 ··· 599 604 600 605 def build_harmony_conversation( 601 606 reasoning_effort: ReasoningEffort, 602 - dev_msg_item: DeveloperMessageItemParam | None, 603 - user_input: str, 607 + convos: list, 604 608 ): 605 - system_message = SystemContent.new().with_reasoning_effort(reasoning_effort) 606 - dev_message: DeveloperContent = DeveloperContent.new() 607 - if isinstance(dev_msg_item, DeveloperMessageItemParam): 608 - dev_message = DeveloperContent.new().with_instructions( 609 - dev_msg_item.content.root 610 - ) # pyright: ignore 611 609 612 - convo = Conversation.from_messages( 613 - [ 614 - Message.from_role_and_content(Role.SYSTEM, system_message), 615 - Message.from_role_and_content(Role.DEVELOPER, dev_message), 616 - Message.from_role_and_content(Role.USER, user_input), 617 - ] 618 - ) 610 + convo_list = [ 611 + Message.from_role_and_content( 612 + Role.SYSTEM, SystemContent.new().with_reasoning_effort(reasoning_effort) 613 + ) 614 + ] 615 + for item in convos: 616 + match item: 617 + case UserMessageItemParam(): 618 + convo_list.append( 619 + Message.from_role_and_content( 620 + Role.USER, item.content.root 621 + ) # pyright: ignore 622 + ) 623 + case DeveloperMessageItemParam(): 624 + convo_list.append( 625 + Message.from_role_and_content( 626 + Role.DEVELOPER, 627 + DeveloperContent.new().with_instructions( 628 + item.content.root 629 + ), # pyright: ignore ) 630 + ) 631 + ) 632 + case AssistantMessageItemParam(): 633 + convo_list.append( 634 + Message.from_role_and_content( 635 + Role.ASSISTANT, item.content.root 636 + ) # pyright: ignore 637 + ) 638 + case _: 639 + raise TypeError("unknown type") 640 + 641 + convo = Conversation.from_messages(convo_list) 619 642 return convo 643 + 644 + 645 + def is_harmony_family(model_name: str): 646 + return ReasoningExtractor.detect_model_type(model_name) == "gpt-oss"
+2
server/reasoning_utils.py
··· 62 62 return "gpt-oss" 63 63 elif "deepseek" in model_lower and "r1" in model_lower: 64 64 return "deepseek" 65 + elif "qwen" in model_lower: 66 + return "deepseek" 65 67 elif "claude" in model_lower: 66 68 return "claude" 67 69 elif "qwq" in model_lower:
+16 -2
tilekit/src/modelfile.rs
··· 38 38 } 39 39 } 40 40 41 - #[derive(Debug, Clone)] 42 - enum Role { 41 + #[derive(Debug, Clone, Copy, serde::Serialize)] 42 + #[serde(rename_all = "lowercase")] 43 + pub enum Role { 43 44 System, 44 45 User, 45 46 Assistant, 47 + Developer, 46 48 } 47 49 48 50 #[derive(Clone, Debug)] ··· 70 72 } 71 73 } 72 74 } 75 + 76 + impl From<Role> for String { 77 + fn from(value: Role) -> Self { 78 + match value { 79 + Role::System => "system".to_owned(), 80 + Role::User => "user".to_owned(), 81 + Role::Assistant => "assistant".to_owned(), 82 + Role::Developer => "developer".to_owned(), 83 + } 84 + } 85 + } 86 + 73 87 #[allow(dead_code)] 74 88 #[derive(Debug, Clone)] 75 89 pub struct Parameter {
+3
tiles/Cargo.toml
··· 17 17 rustyline = "17.0" 18 18 toml = "1.0.3" 19 19 semver = "1.0" 20 + rusqlite = { version = "0.38.0", features = ["bundled"] } 21 + rusqlite_migration = "2.4.1" 22 + uuid = {version = "1.21.0", features = ["v7"]} 20 23 21 24 [dev-dependencies] 22 25 tempfile = "3"
+10 -9
tiles/src/commands/mod.rs
··· 4 4 5 5 use anyhow::{Result, anyhow}; 6 6 use owo_colors::OwoColorize; 7 - use tiles::runtime::Runtime; 8 - use tiles::utils::accounts::{ 7 + use tiles::core; 8 + use tiles::core::accounts::{ 9 9 RootUser, create_root_account, get_root_user_details, save_root_account, set_nickname, 10 10 }; 11 + use tiles::runtime::Runtime; 11 12 use tiles::utils::config::{ 12 13 ConfigProvider, DefaultProvider, get_or_create_config, set_user_data_path, 13 14 }; ··· 146 147 } 147 148 148 149 fn setup_default_user_data_dir<T: ConfigProvider>(config_provider: &T) -> Result<()> { 149 - // gets default data dir -> ~/.local/share/tiles/data 150 - // shows this is the data dir 151 - // asks if they want to change, if y, asks for new loc, else keep current one 152 - // writes the default/new path to in config.toml data->path 153 - // 154 150 let user_data_dir = config_provider.get_user_data_dir()?; 155 151 println!("{}", FTUE_DATA_DIR_PROMPT); 156 152 println!(" {}", user_data_dir.display()); ··· 228 224 } 229 225 230 226 pub async fn try_app_update() -> Result<()> { 227 + // no need to check updates in dev mode 228 + if cfg!(debug_assertions) { 229 + return Ok(()); 230 + } 231 231 let update_info: UpdateInfo = get_update_info().await?; 232 232 if update_info.can_update { 233 233 let update_str = format!( ··· 252 252 Ok(()) 253 253 } 254 254 255 - pub async fn run(runtime: &Runtime, run_args: RunArgs) { 256 - let _ = runtime.run(run_args).await; 255 + pub async fn run(runtime: &Runtime, run_args: RunArgs) -> Result<()> { 256 + core::init().inspect_err(|e| eprintln!("Tiles core init failed due to {:?}", e))?; 257 + runtime.run(run_args).await 257 258 } 258 259 259 260 pub fn set_data(path: &str) {
+621
tiles/src/core/accounts.rs
··· 1 + //! Accounts 2 + // Stuff related to account and identity system 3 + use anyhow::{Result, anyhow}; 4 + use rusqlite::{Connection, types::FromSqlError}; 5 + use std::{ 6 + fmt::Display, 7 + time::{SystemTime, UNIX_EPOCH}, 8 + }; 9 + use tilekit::accounts::create_identity; 10 + use toml::Table; 11 + use uuid::Uuid; 12 + 13 + use crate::{ 14 + core::storage::db::{DBTYPE, get_db_conn}, 15 + utils::config::{get_or_create_config, save_config}, 16 + }; 17 + const ROOT_USER_CONFIG_KEY: &str = "root-user"; 18 + 19 + const ROOT_PARSE_ERROR: &str = "Failed to parse root user config"; 20 + #[allow(dead_code)] 21 + pub struct RootUser { 22 + pub id: String, 23 + pub nickname: String, 24 + } 25 + 26 + #[derive(Debug)] 27 + pub enum ACCOUNT { 28 + LOCAL, 29 + } 30 + 31 + #[derive(Debug)] 32 + pub struct AccountError { 33 + pub error: String, 34 + } 35 + 36 + impl Display for AccountError { 37 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 + write!(f, "{}", self.error) 39 + } 40 + } 41 + impl std::error::Error for AccountError {} 42 + impl TryFrom<String> for ACCOUNT { 43 + type Error = AccountError; 44 + fn try_from(value: String) -> Result<Self, Self::Error> { 45 + let value_lower = value.to_lowercase(); 46 + match value_lower.as_str() { 47 + "local" => Ok(ACCOUNT::LOCAL), 48 + _ => Err(AccountError { 49 + error: "Invalid account type".to_owned(), 50 + }), 51 + } 52 + } 53 + } 54 + impl Display for ACCOUNT { 55 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 56 + match self { 57 + Self::LOCAL => write!(f, "{}", String::from("local")), 58 + } 59 + } 60 + } 61 + 62 + //TODO: add doc, mirrors user table schema 63 + #[allow(dead_code)] 64 + #[derive(Debug)] 65 + pub struct User { 66 + pub id: uuid::Uuid, 67 + pub user_id: String, 68 + pub username: String, 69 + pub active_profile: bool, 70 + pub account_type: ACCOUNT, 71 + pub root: bool, 72 + pub created_at: u64, 73 + pub updated_at: u64, 74 + } 75 + 76 + impl Display for RootUser { 77 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 78 + write!(f, "id: {}\nnickname: {}\n", self.id, self.nickname) 79 + } 80 + } 81 + 82 + impl RootUser { 83 + pub fn new(config: &Table) -> Result<Self> { 84 + let id = config 85 + .get("id") 86 + .ok_or_else(|| anyhow!("Missing ID"))? 87 + .as_str() 88 + .ok_or_else(|| anyhow!("ID not a string"))?; 89 + let nickname = config 90 + .get("nickname") 91 + .ok_or_else(|| anyhow!("Missing Nickname"))? 92 + .as_str() 93 + .ok_or_else(|| anyhow!("Nickname not a string"))?; 94 + Ok(RootUser { 95 + id: id.to_owned(), 96 + nickname: nickname.to_owned(), 97 + }) 98 + } 99 + 100 + pub fn to_table(&self) -> Table { 101 + let mut root_user_table = Table::new(); 102 + root_user_table.insert(String::from("id"), toml::Value::String(self.id.clone())); 103 + root_user_table.insert( 104 + String::from("nickname"), 105 + toml::Value::String(self.nickname.clone()), 106 + ); 107 + root_user_table 108 + } 109 + } 110 + 111 + /// Returns a `RootUser`, which represents a root user 112 + /// 113 + /// # Params 114 + /// 115 + /// - config: A `Table` type of entire config.toml file 116 + pub fn get_root_user_details(config: &Table) -> Result<RootUser> { 117 + let root_user = config 118 + .get(ROOT_USER_CONFIG_KEY) 119 + .ok_or_else(|| anyhow!(ROOT_PARSE_ERROR))?; 120 + let root_user_table = root_user 121 + .as_table() 122 + .ok_or_else(|| anyhow!("root user not a table"))?; 123 + RootUser::new(root_user_table) 124 + } 125 + 126 + /// Create a root account 127 + /// Stores the private credentials in OS secure password manager 128 + /// 129 + /// # Params 130 + /// 131 + /// - config: A `Table` type of entire config.toml file 132 + /// - nickname: Nickname for the identity (Optional) 133 + /// 134 + /// Returns the root_user_config as a `Table` type 135 + pub fn create_root_account(config: &Table, nickname: Option<String>) -> Result<Table> { 136 + let root_user = config 137 + .get(ROOT_USER_CONFIG_KEY) 138 + .ok_or_else(|| anyhow!("{} doesn't exist", ROOT_USER_CONFIG_KEY))?; 139 + let root_user_table = root_user 140 + .as_table() 141 + .ok_or_else(|| anyhow!(ROOT_PARSE_ERROR))?; 142 + let root_user_data = RootUser::new(root_user_table)?; 143 + let did = root_user_data.id; 144 + if did.is_empty() { 145 + Ok(create_root_user(root_user_table, nickname)?) 146 + } else { 147 + Ok(root_user_table.clone()) 148 + } 149 + } 150 + 151 + /// Save the root config in `Table` type to config.toml 152 + /// 153 + /// # Params 154 + /// 155 + /// - config: A `Table` type of entire config.toml file 156 + /// - root_user_config: A `Table` type of root user 157 + pub fn save_root_account(mut config: Table, root_user_config: &Table) -> Result<()> { 158 + config.insert( 159 + String::from(ROOT_USER_CONFIG_KEY), 160 + toml::Value::Table(root_user_config.clone()), 161 + ); 162 + save_config(&config) 163 + } 164 + 165 + /// Sets nickname for the root account 166 + /// 167 + /// # Params 168 + /// 169 + /// - config: A `Table` type of entire config.toml file 170 + /// - nickname: Nickname for the identity 171 + /// 172 + /// Returns the root_user_config as a `Table` type 173 + pub fn set_nickname(config: &Table, nickname: &str) -> Result<Table> { 174 + let root_user = config 175 + .get(ROOT_USER_CONFIG_KEY) 176 + .ok_or_else(|| anyhow!("{} doesn't exist", ROOT_USER_CONFIG_KEY))?; 177 + 178 + let mut root_user_table = root_user 179 + .as_table() 180 + .ok_or_else(|| anyhow!(ROOT_PARSE_ERROR))? 181 + .clone(); 182 + let did = root_user_table 183 + .get("id") 184 + .and_then(|v| v.as_str()) 185 + .ok_or(anyhow!("Failed to get id from config"))?; 186 + if did.is_empty() { 187 + Err(anyhow::anyhow!("No Root user available")) 188 + } else { 189 + root_user_table.insert("id".to_owned(), toml::Value::String(did.to_owned())); 190 + root_user_table.insert( 191 + "nickname".to_owned(), 192 + toml::Value::String(nickname.to_owned()), 193 + ); 194 + Ok(root_user_table) 195 + } 196 + } 197 + 198 + pub fn get_current_user(conn: &Connection) -> Result<User> { 199 + let mut fetch_current_user = conn.prepare("select id, user_id, username, account_type, active_profile, root, created_at, updated_at from users where active_profile= true")?; 200 + 201 + fetch_current_user 202 + .query_one([], |row| { 203 + let id: String = row.get(0)?; 204 + let account_type: String = row.get(3)?; 205 + let created_at: f64 = row.get(6)?; 206 + let updated_at: f64 = row.get(7)?; 207 + Ok(User { 208 + id: Uuid::try_parse(&id).map_err(FromSqlError::other)?, 209 + user_id: row.get(1)?, 210 + username: row.get(2)?, 211 + account_type: ACCOUNT::try_from(account_type).map_err(FromSqlError::other)?, 212 + active_profile: row.get(4)?, 213 + root: row.get(5)?, 214 + 215 + created_at: created_at as u64, 216 + updated_at: updated_at as u64, 217 + }) 218 + }) 219 + .map_err(<rusqlite::Error as Into<anyhow::Error>>::into) 220 + } 221 + // TODO: when we support multiple accounts 222 + // make sure that there can't be multiple rows with 223 + // root true. 224 + pub fn save_root_account_db() -> Result<()> { 225 + let conn = get_db_conn(DBTYPE::COMMON)?; 226 + let config = get_or_create_config()?; 227 + let root_user = get_root_user_details(&config)?; 228 + let user = User { 229 + id: Uuid::now_v7(), 230 + user_id: root_user.id, 231 + username: root_user.nickname, 232 + account_type: ACCOUNT::LOCAL, 233 + active_profile: true, 234 + root: true, 235 + created_at: SystemTime::now() 236 + .duration_since(UNIX_EPOCH) 237 + .expect("time went backwards") 238 + .as_secs(), 239 + updated_at: SystemTime::now() 240 + .duration_since(UNIX_EPOCH) 241 + .expect("time went backwards") 242 + .as_secs(), 243 + }; 244 + 245 + let mut fetch_root_user = conn.prepare("select id from users where root = true")?; 246 + 247 + match fetch_root_user.query_one([], |_row| Ok(())) { 248 + Err(rusqlite::Error::QueryReturnedNoRows) => { 249 + conn.execute("insert into users (id, user_id, username, active_profile, account_type, root) values 250 + (?1, ?2, ?3,?4, ?5, ?6)", (&user.id.to_string(), &user.user_id, &user.username, &user.active_profile, 251 + user.account_type.to_string(), &user.root))?; 252 + Ok(()) 253 + } 254 + Err(_err) => Err(anyhow!("Fetching user from db failed")), 255 + _ => Ok(()), 256 + } 257 + } 258 + 259 + fn create_root_user(root_user_config: &Table, nickname: Option<String>) -> Result<Table> { 260 + let mut root_user_table = root_user_config.clone(); 261 + match create_identity("tiles") { 262 + Ok(did) => { 263 + root_user_table.insert("id".to_owned(), toml::Value::String(did)); 264 + if let Some(nickname) = nickname { 265 + root_user_table.insert("nickname".to_owned(), toml::Value::String(nickname)); 266 + } 267 + Ok(root_user_table) 268 + } 269 + Err(err) => Err(err), 270 + } 271 + } 272 + 273 + #[cfg(test)] 274 + mod tests { 275 + use super::*; 276 + use crate::core::accounts::{ 277 + RootUser, create_root_account, get_current_user, get_root_user_details, set_nickname, 278 + }; 279 + use anyhow::Result; 280 + use keyring::{mock, set_default_credential_builder}; 281 + use rusqlite::Connection; 282 + use toml::Table; 283 + 284 + #[test] 285 + fn test_get_root_user_details_empty_id() -> Result<()> { 286 + let config: Table = toml::from_str( 287 + r#" 288 + [root-user] 289 + id = '' 290 + nickname = '' 291 + "#, 292 + ) 293 + .unwrap(); 294 + let acc_details = get_root_user_details(&config)?; 295 + assert!(acc_details.id.is_empty()); 296 + Ok(()) 297 + } 298 + 299 + #[test] 300 + fn test_get_root_user_details_valid_id() -> Result<()> { 301 + let config: Table = toml::from_str( 302 + r#" 303 + [root-user] 304 + id = 'did:key:xyz' 305 + nickname = '' 306 + "#, 307 + ) 308 + .unwrap(); 309 + let acc_details = get_root_user_details(&config)?; 310 + assert!(acc_details.id.contains("did:key")); 311 + Ok(()) 312 + } 313 + 314 + #[test] 315 + fn test_create_root_account_but_exists() { 316 + let config: Table = toml::from_str( 317 + r#" 318 + [root-user] 319 + id = 'did:key:xyz' 320 + nickname = '' 321 + "#, 322 + ) 323 + .unwrap(); 324 + let root_user = create_root_account(&config, None).unwrap(); 325 + 326 + assert_eq!( 327 + root_user.get("id").unwrap().as_str().unwrap(), 328 + "did:key:xyz" 329 + ); 330 + } 331 + 332 + #[test] 333 + fn test_create_root_account_new() { 334 + set_default_credential_builder(mock::default_credential_builder()); 335 + let config: Table = toml::from_str( 336 + r#" 337 + [root-user] 338 + id = '' 339 + nickname = '' 340 + "#, 341 + ) 342 + .unwrap(); 343 + let root_user = create_root_account(&config, None).unwrap(); 344 + 345 + assert_ne!( 346 + root_user.get("id").unwrap().as_str().unwrap(), 347 + "did:key:xyz" 348 + ); 349 + 350 + assert!( 351 + root_user 352 + .get("id") 353 + .unwrap() 354 + .as_str() 355 + .unwrap() 356 + .starts_with("did:key") 357 + ); 358 + } 359 + 360 + #[test] 361 + fn test_create_root_account_new_w_nickname() { 362 + set_default_credential_builder(mock::default_credential_builder()); 363 + let config: Table = toml::from_str( 364 + r#" 365 + [root-user] 366 + id = '' 367 + nickname = '' 368 + "#, 369 + ) 370 + .unwrap(); 371 + let root_user = create_root_account(&config, Some(String::from("madclaws"))).unwrap(); 372 + 373 + assert_ne!( 374 + root_user.get("id").unwrap().as_str().unwrap(), 375 + "did:key:xyz" 376 + ); 377 + 378 + assert!( 379 + root_user 380 + .get("id") 381 + .unwrap() 382 + .as_str() 383 + .unwrap() 384 + .starts_with("did:key") 385 + ); 386 + 387 + assert_eq!( 388 + root_user.get("nickname").unwrap().as_str().unwrap(), 389 + "madclaws" 390 + ); 391 + } 392 + 393 + #[test] 394 + fn test_get_root_user_details_missing_key() { 395 + let config: Table = toml::from_str( 396 + r#" 397 + # no root-user table 398 + [other] 399 + foo = "bar" 400 + "#, 401 + ) 402 + .unwrap(); 403 + 404 + let res = get_root_user_details(&config); 405 + assert!(res.is_err(), "Expected error when root-user key is missing"); 406 + } 407 + 408 + #[test] 409 + fn test_root_user_new_wrong_types() { 410 + // id is integer, nickname is table 411 + let config: Table = toml::from_str( 412 + r#" 413 + [root-user] 414 + id = 123 415 + nickname = { nested = "value" } 416 + "#, 417 + ) 418 + .unwrap(); 419 + 420 + let root_tbl = config.get("root-user").unwrap().as_table().unwrap().clone(); 421 + assert!( 422 + RootUser::new(&root_tbl).is_err(), 423 + "Expected error for wrong types" 424 + ); 425 + } 426 + 427 + #[test] 428 + fn test_root_user_roundtrip_table() -> Result<()> { 429 + let user = RootUser { 430 + id: "did:key:abc".into(), 431 + nickname: "nick".into(), 432 + }; 433 + let tbl = user.to_table(); 434 + let parsed = RootUser::new(&tbl)?; 435 + assert_eq!(parsed.id, user.id); 436 + assert_eq!(parsed.nickname, user.nickname); 437 + Ok(()) 438 + } 439 + 440 + #[test] 441 + fn test_set_nickname_but_invalid_config() { 442 + let config: Table = toml::from_str( 443 + r#" 444 + [ruser] 445 + id = '' 446 + "#, 447 + ) 448 + .unwrap(); 449 + 450 + assert!(set_nickname(&config, "madclaws").is_err()) 451 + } 452 + 453 + #[test] 454 + fn test_set_nickname_success() { 455 + let config: Table = toml::from_str( 456 + r#" 457 + [root-user] 458 + id = 'did:key:xyz' 459 + nickname = '' 460 + "#, 461 + ) 462 + .unwrap(); 463 + 464 + let updated = set_nickname(&config, "madclaws").expect("nickname update should succeed"); 465 + assert_eq!( 466 + updated.get("id").and_then(|v| v.as_str()), 467 + Some("did:key:xyz") 468 + ); 469 + assert_eq!( 470 + updated.get("nickname").and_then(|v| v.as_str()), 471 + Some("madclaws") 472 + ); 473 + } 474 + 475 + #[test] 476 + fn test_set_nickname_with_empty_id_fails() { 477 + let config: Table = toml::from_str( 478 + r#" 479 + [root-user] 480 + id = '' 481 + nickname = '' 482 + "#, 483 + ) 484 + .unwrap(); 485 + 486 + let err = set_nickname(&config, "madclaws").expect_err("empty id should fail"); 487 + assert!(err.to_string().contains("No Root user available")); 488 + } 489 + 490 + fn setup_db_schema() -> Connection { 491 + let conn = Connection::open_in_memory().unwrap(); 492 + conn.execute( 493 + " 494 + CREATE TABLE IF NOT EXISTS users ( 495 + id TEXT PRIMARY KEY, 496 + user_id TEXT NOT NULL, 497 + username TEXT NOT NULL, 498 + active_profile INTEGER NOT NULL DEFAULT 0 CHECK (active_profile IN (0,1)), 499 + account_type TEXT NOT NULL, 500 + root INTEGER NOT NULL DEFAULT 0 CHECK (root IN (0,1)), 501 + created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), 502 + updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), 503 + UNIQUE(account_type, user_id) 504 + ); 505 + ", 506 + [], 507 + ) 508 + .unwrap(); 509 + 510 + conn 511 + } 512 + 513 + #[test] 514 + fn test_get_user_when_no_user() { 515 + let conn = setup_db_schema(); 516 + assert!(get_current_user(&conn).is_err()) 517 + } 518 + 519 + #[test] 520 + fn test_get_current_user_valid() { 521 + let conn = setup_db_schema(); 522 + let user = User { 523 + id: Uuid::now_v7(), 524 + user_id: String::from("did"), 525 + username: String::from("nickname"), 526 + account_type: ACCOUNT::LOCAL, 527 + active_profile: true, 528 + root: true, 529 + created_at: SystemTime::now() 530 + .duration_since(UNIX_EPOCH) 531 + .expect("time went backwards") 532 + .as_secs(), 533 + updated_at: SystemTime::now() 534 + .duration_since(UNIX_EPOCH) 535 + .expect("time went backwards") 536 + .as_secs(), 537 + }; 538 + 539 + let mut fetch_root_user = conn 540 + .prepare("select id from users where root = true") 541 + .unwrap(); 542 + 543 + match fetch_root_user.query_one([], |_row| Ok(())) { 544 + Err(rusqlite::Error::QueryReturnedNoRows) => { 545 + conn.execute("insert into users (id, user_id, username, active_profile, account_type, root) values 546 + (?1, ?2, ?3,?4, ?5, ?6)", (&user.id.to_string(), &user.user_id, &user.username, &user.active_profile, 547 + user.account_type.to_string(), &user.root)).unwrap(); 548 + } 549 + Err(_err) => (), 550 + _ => (), 551 + } 552 + 553 + assert!(get_current_user(&conn).is_ok()) 554 + } 555 + 556 + #[test] 557 + fn test_get_current_user_invalid_uuid_fails() { 558 + let conn = setup_db_schema(); 559 + conn.execute( 560 + "insert into users (id, user_id, username, active_profile, account_type, root, created_at, updated_at) 561 + values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", 562 + ( 563 + "not-a-uuid", 564 + "did:key:test", 565 + "nickname", 566 + true, 567 + "local", 568 + true, 569 + 1_i64, 570 + 1_i64, 571 + ), 572 + ) 573 + .unwrap(); 574 + 575 + assert!(get_current_user(&conn).is_err()); 576 + } 577 + 578 + #[test] 579 + fn test_get_current_user_invalid_account_type_fails() { 580 + let conn = setup_db_schema(); 581 + conn.execute( 582 + "insert into users (id, user_id, username, active_profile, account_type, root, created_at, updated_at) 583 + values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", 584 + ( 585 + Uuid::now_v7().to_string(), 586 + "did:key:test", 587 + "nickname", 588 + true, 589 + "unknown", 590 + true, 591 + 1_i64, 592 + 1_i64, 593 + ), 594 + ) 595 + .unwrap(); 596 + 597 + assert!(get_current_user(&conn).is_err()); 598 + } 599 + 600 + #[test] 601 + fn test_get_current_user_inactive_only_rows_fails() { 602 + let conn = setup_db_schema(); 603 + conn.execute( 604 + "insert into users (id, user_id, username, active_profile, account_type, root, created_at, updated_at) 605 + values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", 606 + ( 607 + Uuid::now_v7().to_string(), 608 + "did:key:test", 609 + "nickname", 610 + false, 611 + "local", 612 + true, 613 + 1_i64, 614 + 1_i64, 615 + ), 616 + ) 617 + .unwrap(); 618 + 619 + assert!(get_current_user(&conn).is_err()); 620 + } 621 + }
+243
tiles/src/core/chats.rs
··· 1 + //! Chats.rs 2 + //! 3 + //! Stuff related to chats with the models 4 + //! 5 + 6 + use crate::core::accounts::User; 7 + use crate::runtime::mlx::ChatResponse; 8 + use crate::utils::get_unix_time_now; 9 + use anyhow::Result; 10 + use rusqlite::Connection; 11 + use tilekit::modelfile::Role; 12 + use uuid::Uuid; 13 + // model the chats table 14 + 15 + #[derive(serde::Serialize, Clone, Debug)] 16 + pub struct Message { 17 + pub r#type: String, 18 + pub role: Role, 19 + pub content: String, 20 + } 21 + 22 + pub struct Chats { 23 + pub id: Uuid, 24 + content: String, 25 + // The id of the responses api obj 26 + response_id: Option<String>, 27 + // The Model chat user role 28 + role: Role, 29 + user_id: String, 30 + // The parent Id of a model's reply 31 + context_id: Option<Uuid>, 32 + created_at: u64, 33 + updated_at: u64, 34 + } 35 + 36 + pub fn save_chat( 37 + conn: &Connection, 38 + user: &User, 39 + input: &str, 40 + chat_resp: Option<&ChatResponse>, 41 + ) -> Result<Chats> { 42 + if let Some(chat_response) = chat_resp { 43 + let chat_resp_cloned = chat_response.clone(); 44 + let chat = Chats { 45 + id: Uuid::now_v7(), 46 + user_id: user.user_id.clone(), 47 + content: input.to_owned(), 48 + response_id: Some(chat_resp_cloned.prev_response_id), 49 + role: Role::Assistant, 50 + context_id: chat_resp_cloned.parent_chat_id, 51 + created_at: get_unix_time_now(), 52 + updated_at: get_unix_time_now(), 53 + }; 54 + 55 + conn.execute("insert into chats(id, user_id, content, resp_id, role, context_id, created_at, updated_at) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", (&chat.id.to_string(), &chat.user_id, &chat.content, &chat.response_id, Into::<String>::into(chat.role), &chat.context_id.unwrap_or(Uuid::nil()).to_string(), &chat.created_at.to_string(), &chat.updated_at.to_string()))?; 56 + 57 + Ok(chat) 58 + } else { 59 + let chat = Chats { 60 + id: Uuid::now_v7(), 61 + user_id: user.user_id.clone(), 62 + content: input.to_owned(), 63 + response_id: None, 64 + role: Role::User, 65 + context_id: None, 66 + created_at: get_unix_time_now(), 67 + updated_at: get_unix_time_now(), 68 + }; 69 + 70 + conn.execute("insert into chats(id, user_id, content, resp_id, role, context_id, created_at, updated_at) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", (&chat.id.to_string(), &chat.user_id, &chat.content, &chat.response_id, Into::<String>::into(chat.role), &chat.context_id.unwrap_or(Uuid::nil()).to_string(), &chat.created_at.to_string(), &chat.updated_at.to_string()))?; 71 + 72 + Ok(chat) 73 + } 74 + } 75 + 76 + #[cfg(test)] 77 + mod tests { 78 + use std::time::{SystemTime, UNIX_EPOCH}; 79 + 80 + use rusqlite::Connection; 81 + use tilekit::modelfile::Role; 82 + use uuid::Uuid; 83 + 84 + use crate::{ 85 + core::{ 86 + accounts::{ACCOUNT, User}, 87 + chats::save_chat, 88 + }, 89 + runtime::mlx::ChatResponse, 90 + }; 91 + 92 + #[test] 93 + fn test_valid_input_save_chat() { 94 + let conn = setup_db_schema(); 95 + let user = create_user(); 96 + let input = "2+2"; 97 + let chat = save_chat(&conn, &user, input, None).expect("chat should be saved"); 98 + 99 + assert_eq!(chat.user_id, user.user_id); 100 + assert!(chat.response_id.is_none()); 101 + assert!(chat.context_id.is_none()); 102 + 103 + let saved = fetch_saved_chat_row(&conn, &chat.id); 104 + assert_eq!(saved.content, input); 105 + assert_eq!(saved.resp_id, None); 106 + assert_eq!(saved.role, Into::<String>::into(Role::User)); 107 + assert_eq!(saved.user_id, user.user_id); 108 + assert_eq!(saved.context_id, Uuid::nil().to_string()); 109 + } 110 + 111 + #[test] 112 + fn test_valid_response_save_chat() { 113 + let conn = setup_db_schema(); 114 + let user = create_user(); 115 + let parent_chat_id = Uuid::now_v7(); 116 + let chat_resp = ChatResponse { 117 + reply: "reply".to_owned(), 118 + code: "code".to_owned(), 119 + prev_response_id: String::from("resp_prev"), 120 + parent_chat_id: Some(parent_chat_id), 121 + metrics: None, 122 + }; 123 + let input = "2+2"; 124 + let chat = save_chat(&conn, &user, input, Some(&chat_resp)).expect("chat should be saved"); 125 + 126 + assert_eq!(chat.user_id, user.user_id); 127 + assert_eq!(chat.response_id.as_deref(), Some("resp_prev")); 128 + assert_eq!(chat.context_id, Some(parent_chat_id)); 129 + 130 + let saved = fetch_saved_chat_row(&conn, &chat.id); 131 + assert_eq!(saved.content, input); 132 + assert_eq!(saved.resp_id, Some(String::from("resp_prev"))); 133 + assert_eq!(saved.role, Into::<String>::into(Role::Assistant)); 134 + assert_eq!(saved.user_id, user.user_id); 135 + assert_eq!(saved.context_id, parent_chat_id.to_string()); 136 + } 137 + 138 + #[test] 139 + fn test_response_without_parent_chat_id_saves_nil_context() { 140 + let conn = setup_db_schema(); 141 + let user = create_user(); 142 + let chat_resp = ChatResponse { 143 + reply: "reply".to_owned(), 144 + code: "code".to_owned(), 145 + prev_response_id: String::from("resp_prev"), 146 + parent_chat_id: None, 147 + metrics: None, 148 + }; 149 + 150 + let chat = 151 + save_chat(&conn, &user, "hello", Some(&chat_resp)).expect("chat should be saved"); 152 + 153 + assert_eq!(chat.context_id, None); 154 + let saved = fetch_saved_chat_row(&conn, &chat.id); 155 + assert_eq!(saved.role, Into::<String>::into(Role::Assistant)); 156 + assert_eq!(saved.context_id, Uuid::nil().to_string()); 157 + } 158 + 159 + #[test] 160 + fn test_empty_input_is_saved() { 161 + let conn = setup_db_schema(); 162 + let user = create_user(); 163 + 164 + let chat = save_chat(&conn, &user, "", None).expect("empty content should still be saved"); 165 + 166 + let saved = fetch_saved_chat_row(&conn, &chat.id); 167 + assert_eq!(saved.content, ""); 168 + assert_eq!(saved.role, Into::<String>::into(Role::User)); 169 + } 170 + 171 + #[test] 172 + fn test_save_chat_errors_when_table_missing() { 173 + let conn = Connection::open_in_memory().expect("in-memory db should open"); 174 + let user = create_user(); 175 + 176 + let result = save_chat(&conn, &user, "2+2", None); 177 + 178 + assert!(result.is_err()); 179 + } 180 + 181 + struct SavedChatRow { 182 + content: String, 183 + resp_id: Option<String>, 184 + role: String, 185 + user_id: String, 186 + context_id: String, 187 + } 188 + 189 + fn fetch_saved_chat_row(conn: &Connection, chat_id: &Uuid) -> SavedChatRow { 190 + conn.query_row( 191 + "SELECT content, resp_id, role, user_id, context_id FROM chats WHERE id = ?1", 192 + [chat_id.to_string()], 193 + |row| { 194 + Ok(SavedChatRow { 195 + content: row.get(0)?, 196 + resp_id: row.get(1)?, 197 + role: row.get(2)?, 198 + user_id: row.get(3)?, 199 + context_id: row.get(4)?, 200 + }) 201 + }, 202 + ) 203 + .expect("saved chat row should exist") 204 + } 205 + 206 + fn create_user() -> User { 207 + User { 208 + id: Uuid::now_v7(), 209 + user_id: String::from("did"), 210 + username: String::from("nickname"), 211 + account_type: ACCOUNT::LOCAL, 212 + active_profile: true, 213 + root: true, 214 + created_at: SystemTime::now() 215 + .duration_since(UNIX_EPOCH) 216 + .expect("time went backwards") 217 + .as_secs(), 218 + updated_at: SystemTime::now() 219 + .duration_since(UNIX_EPOCH) 220 + .expect("time went backwards") 221 + .as_secs(), 222 + } 223 + } 224 + fn setup_db_schema() -> Connection { 225 + let conn = Connection::open_in_memory().unwrap(); 226 + conn.execute( 227 + "CREATE TABLE IF NOT EXISTS chats ( 228 + id TEXT PRIMARY KEY, 229 + content TEXT NOT NULL, 230 + resp_id TEXT, 231 + role TEXT NOT NULL, 232 + user_id TEXT NOT NULL, 233 + context_id TEXT , 234 + created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), 235 + updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')) 236 + );", 237 + [], 238 + ) 239 + .unwrap(); 240 + 241 + conn 242 + } 243 + }
+16 -1
tiles/src/core/mod.rs
··· 1 - // to be deprecated and removed, the core stuff will be moved to tilekit sdk 1 + //! tiles-core 2 + //! 3 + //! The core runtime which different UI apps can leverage 4 + //! Generally the core will be run as daemon and interact with other sub components 5 + 6 + use anyhow::Result; 7 + 8 + use crate::core::{accounts::save_root_account_db, storage::db::init_db}; 2 9 10 + pub mod accounts; 11 + pub mod chats; 3 12 pub mod health; 13 + pub mod storage; 14 + // Entrypoint of the core 15 + pub fn init() -> Result<()> { 16 + init_db()?; 17 + save_root_account_db() 18 + }
+95
tiles/src/core/storage/db.rs
··· 1 + //! Core Database Handling 2 + //! 3 + //! Uses sqlite as the underlying database 4 + //! 5 + 6 + use std::path::PathBuf; 7 + 8 + use anyhow::{Result, anyhow}; 9 + use rusqlite::Connection; 10 + 11 + use crate::utils::config::{ConfigProvider, DefaultProvider}; 12 + use rusqlite_migration::{M, Migrations}; 13 + pub enum DBTYPE { 14 + COMMON, 15 + CHAT, 16 + } 17 + 18 + // DEFINE MIGRATIONS 19 + 20 + // TODO: add the schema doc 21 + const COMMON_MIGRATION_ARRAY: &[M] = &[M::up( 22 + " 23 + CREATE TABLE IF NOT EXISTS users ( 24 + id TEXT PRIMARY KEY, 25 + user_id TEXT NOT NULL, 26 + username TEXT NOT NULL, 27 + active_profile INTEGER NOT NULL DEFAULT 0 CHECK (active_profile IN (0,1)), 28 + account_type TEXT NOT NULL, 29 + root INTEGER NOT NULL DEFAULT 0 CHECK (root IN (0,1)), 30 + created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), 31 + updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), 32 + UNIQUE(account_type, user_id) 33 + ); 34 + ", 35 + )]; 36 + 37 + const COMMON_MIGRATIONS: Migrations = Migrations::from_slice(COMMON_MIGRATION_ARRAY); 38 + 39 + // TODO: add the schema doc 40 + const CHATS_MIGRATION_ARRAY: &[M] = &[M::up( 41 + "CREATE TABLE IF NOT EXISTS chats ( 42 + id TEXT PRIMARY KEY, 43 + content TEXT NOT NULL, 44 + resp_id TEXT, 45 + role TEXT NOT NULL, 46 + user_id TEXT NOT NULL, 47 + context_id TEXT, 48 + created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), 49 + updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')) 50 + )", 51 + )]; 52 + 53 + const CHATS_MIGRATIONS: Migrations = Migrations::from_slice(CHATS_MIGRATION_ARRAY); 54 + 55 + pub fn init_db() -> Result<()> { 56 + let mut chat_conn = get_db_conn(DBTYPE::CHAT)?; 57 + let mut common_conn = get_db_conn(DBTYPE::COMMON)?; 58 + 59 + apply_migrations(&mut common_conn, &mut chat_conn) 60 + } 61 + 62 + pub fn get_db_conn(db_type: DBTYPE) -> Result<Connection> { 63 + let db_path = get_db_path(db_type)?; 64 + let conn = Connection::open(db_path) 65 + .map_err(|e| anyhow!("Failed to create db connection due to {:?}", e))?; 66 + 67 + conn.pragma_update(None, "journal_mode", "WAL")?; 68 + Ok(conn) 69 + } 70 + 71 + fn apply_migrations(common_conn: &mut Connection, chat_conn: &mut Connection) -> Result<()> { 72 + COMMON_MIGRATIONS 73 + .to_latest(common_conn) 74 + .map_err(<rusqlite_migration::Error as Into<anyhow::Error>>::into)?; 75 + CHATS_MIGRATIONS.to_latest(chat_conn).map_err(|e| e.into()) 76 + } 77 + fn get_db_path(db_type: DBTYPE) -> Result<PathBuf> { 78 + let user_data_dir = DefaultProvider.get_user_data_dir()?; 79 + match db_type { 80 + DBTYPE::COMMON => Ok(user_data_dir.join("common.db")), 81 + DBTYPE::CHAT => Ok(user_data_dir.join("chats.db")), 82 + } 83 + } 84 + 85 + #[cfg(test)] 86 + mod tests { 87 + 88 + use super::*; 89 + 90 + #[test] 91 + fn migrations_test() { 92 + assert!(COMMON_MIGRATIONS.validate().is_ok()); 93 + assert!(CHATS_MIGRATIONS.validate().is_ok()); 94 + } 95 + }
+1
tiles/src/core/storage/mod.rs
··· 1 + pub mod db;
+10 -4
tiles/src/main.rs
··· 118 118 SetNickname { nickname: String }, 119 119 } 120 120 121 - #[tokio::main(flavor = "current_thread")] 121 + #[tokio::main] 122 122 pub async fn main() -> Result<(), Box<dyn Error>> { 123 123 let cli = Cli::parse(); 124 124 let runtime = build_runtime(); ··· 133 133 commands::run_setup_for_ftue(&run_args) 134 134 .inspect_err(|e| eprintln!("Failed to setup Tiles due to {:?}", e))?; 135 135 let _ = commands::try_app_update().await; 136 - commands::run(&runtime, run_args).await; 136 + commands::run(&runtime, run_args) 137 + .await 138 + .inspect_err(|e| eprintln!("Tiles failed to run due to {:?}", e))?; 137 139 } 138 140 Some(Commands::Run { 139 141 modelfile_path, ··· 144 146 relay_count: flags.relay_count, 145 147 memory: flags.memory, 146 148 }; 147 - commands::run(&runtime, run_args).await; 149 + commands::run(&runtime, run_args) 150 + .await 151 + .inspect_err(|e| eprintln!("Tiles failed to run due to {:?}", e))?; 148 152 } 149 153 Some(Commands::Health) => { 150 154 commands::check_health().await; ··· 171 175 } 172 176 Some(Commands::Update) => { 173 177 println!("Checking for updates..."); 174 - let res = installer::try_update(None).await?; 178 + let res = installer::try_update(None) 179 + .await 180 + .inspect_err(|e| eprintln!("Failed in update process due to {:?}", e))?; 175 181 println!("{}", res); 176 182 } 177 183 }
+146 -68
tiles/src/runtime/mlx.rs
··· 1 + use crate::core::accounts::{User, get_current_user}; 2 + use crate::core::chats::{Message, save_chat}; 3 + use crate::core::storage::db::get_db_conn; 1 4 use crate::runtime::RunArgs; 2 5 use crate::utils::config::{ConfigProvider, DefaultProvider, get_memory_path}; 3 6 use crate::utils::hf_model_downloader::*; ··· 5 8 use futures_util::StreamExt; 6 9 use owo_colors::OwoColorize; 7 10 use reqwest::{Client, StatusCode}; 11 + use rusqlite::Connection; 8 12 use rustyline::completion::Completer; 9 13 use rustyline::highlight::Highlighter; 10 14 use rustyline::hint::Hinter; ··· 19 23 use std::process::Stdio; 20 24 use std::time::Duration; 21 25 use tilekit::modelfile::Modelfile; 26 + use tilekit::modelfile::Role; 22 27 use tokio::time::sleep; 28 + use uuid::Uuid; 23 29 24 - #[derive(Debug, Deserialize, Serialize)] 30 + #[derive(Debug, Deserialize, Serialize, Clone)] 25 31 pub struct BenchmarkMetrics { 26 32 ttft_ms: f64, 27 33 total_tokens: i32, ··· 43 49 pub struct MLXRuntime {} 44 50 45 51 impl MLXRuntime {} 52 + 53 + #[derive(Clone)] 46 54 pub struct ChatResponse { 47 55 // think: String, 48 - reply: String, 49 - code: String, 50 - prev_response_id: String, 51 - metrics: Option<BenchmarkMetrics>, 56 + pub reply: String, 57 + pub code: String, 58 + pub prev_response_id: String, 59 + pub parent_chat_id: Option<Uuid>, 60 + pub metrics: Option<BenchmarkMetrics>, 52 61 } 53 62 54 63 impl Default for MLXRuntime { ··· 231 240 } 232 241 // loading the model from mem-agent via daemon server 233 242 let memory_path = get_memory_path().context("Setting/Retrieving memory_path failed")?; 234 - let modelname = modelfile.from.as_ref().unwrap(); 235 243 match load_model(&modelfile, &default_modelfile, &memory_path).await { 236 - Ok(_) => start_repl(mlx_runtime, modelname, run_args).await, 244 + Ok(_) => start_repl(mlx_runtime, &modelfile, run_args).await?, 237 245 Err(err) => return Err(anyhow::anyhow!(err)), 238 246 } 239 247 Ok(()) 240 248 } 241 249 242 - async fn start_repl(mlx_runtime: &MLXRuntime, modelname: &str, run_args: &RunArgs) { 250 + async fn start_repl( 251 + mlx_runtime: &MLXRuntime, 252 + modelfile: &Modelfile, 253 + run_args: &RunArgs, 254 + ) -> Result<()> { 255 + let modelname = modelfile 256 + .from 257 + .clone() 258 + .ok_or_else(|| anyhow!("Error getting FROM from modelfile due to"))?; 259 + 243 260 println!("Running {} in interactive mode", modelname); 261 + let common_db_conn = get_db_conn(crate::core::storage::db::DBTYPE::COMMON)?; 262 + let chat_db_conn = get_db_conn(crate::core::storage::db::DBTYPE::CHAT)?; 263 + let current_user = get_current_user(&common_db_conn)?; 244 264 245 265 let config = Config::builder().auto_add_history(true).build(); 246 266 let mut editor = Editor::<TilesHinter, DefaultHistory>::with_config(config).unwrap(); ··· 248 268 let mut g_reply: String = "".to_owned(); 249 269 let mut prev_response_id: String = String::from(""); 250 270 271 + let mut conversations: Vec<Message> = vec![]; 251 272 loop { 252 273 let readline = editor.readline(">>> "); 253 274 let input = match readline { ··· 262 283 } 263 284 }; 264 285 265 - match handle_slash_command(&input, modelname) { 286 + match handle_slash_command(&input, modelname.as_str()) { 266 287 SlashCommand::Continue => continue, 267 288 SlashCommand::Exit => { 268 289 println!("Exiting interactive mode"); ··· 288 309 loop { 289 310 if remaining_count > 0 { 290 311 let chat_start = remaining_count == run_args.relay_count; 291 - if let Ok(response) = chat( 312 + 313 + match chat( 292 314 &input, 293 - modelname, 315 + modelfile, 294 316 chat_start, 295 317 &python_code, 296 318 &g_reply, 297 319 run_args, 298 320 &prev_response_id, 321 + &chat_db_conn, 322 + &current_user, 323 + &conversations, 299 324 ) 300 325 .await 301 326 { 302 - if response.reply.is_empty() { 303 - if !response.code.is_empty() { 304 - python_code = response.code; 305 - } 306 - if let Some(metrics) = response.metrics { 307 - bench_metrics.update(metrics); 308 - } 309 - remaining_count -= 1; 310 - } else { 311 - g_reply = response.reply.clone(); 312 - if run_args.memory { 313 - println!("\n{}", response.reply.trim()); 327 + Ok(response) => { 328 + if response.reply.is_empty() { 329 + if !response.code.is_empty() { 330 + python_code = response.code; 331 + } 332 + if let Some(metrics) = response.metrics { 333 + bench_metrics.update(metrics); 334 + } 335 + remaining_count -= 1; 314 336 } else { 315 - prev_response_id = response.prev_response_id; 316 - println!("\n"); 317 - } 318 - // Display benchmark metrics if available 319 - if let Some(metrics) = response.metrics { 320 - bench_metrics.update(metrics); 321 - println!( 322 - "{}", 323 - format!( 324 - "\n{} {:.1} tok/s | {} tokens | {:.0}s TTFT", 325 - "💡".yellow(), 326 - bench_metrics.total_tokens as f64 327 - / bench_metrics.total_latency_s, 328 - bench_metrics.total_tokens, 329 - bench_metrics.ttft_ms / 1000.0 330 - ) 331 - .dimmed() 332 - ); 333 - } 337 + g_reply = response.reply.clone(); 338 + if run_args.memory { 339 + println!("\n{}", response.reply.trim()); 340 + } else { 341 + prev_response_id = response.prev_response_id.clone(); 342 + println!("\n"); 343 + } 344 + conversations.push(Message { 345 + r#type: String::from("message"), 346 + role: Role::User, 347 + content: input, 348 + }); 349 + conversations.push(Message { 350 + r#type: String::from("message"), 351 + role: Role::Assistant, 352 + content: g_reply.clone(), 353 + }); 334 354 355 + save_chat(&chat_db_conn, &current_user, &g_reply, Some(&response))?; 356 + // Display benchmark metrics if available 357 + if let Some(metrics) = response.metrics { 358 + bench_metrics.update(metrics); 359 + println!( 360 + "{}", 361 + format!( 362 + "\n{} {:.1} tok/s | {} tokens | {:.0}s TTFT", 363 + "💡".yellow(), 364 + bench_metrics.total_tokens as f64 365 + / bench_metrics.total_latency_s, 366 + bench_metrics.total_tokens, 367 + bench_metrics.ttft_ms / 1000.0 368 + ) 369 + .dimmed() 370 + ); 371 + } 372 + 373 + break; 374 + } 375 + } 376 + Err(err) => { 377 + // if out of relay count, then clear the global_reply and ready for next fresh prompt 378 + println!("{:?}", err); 379 + g_reply.clear(); 335 380 break; 336 381 } 337 - } else { 338 - println!("\nFailed to respond"); 339 - break; 340 382 } 341 - } else { 342 - // if out of relay count, then clear the global_reply and ready for next fresh prompt 343 - g_reply.clear(); 344 - break; 345 383 } 346 384 } 347 385 if g_reply.is_empty() { 348 386 println!("\nNo reply, try another prompt"); 349 387 } 350 388 } 389 + Ok(()) 351 390 } 352 391 353 392 pub async fn ping() -> Result<()> { ··· 397 436 } 398 437 } 399 438 439 + //TODO: Have 2 separate chat functions for memory and non-memory 440 + #[allow(clippy::too_many_arguments)] 400 441 async fn chat( 401 442 input: &str, 402 - model_name: &str, 443 + modelfile: &Modelfile, 403 444 chat_start: bool, 404 445 python_code: &str, 405 446 g_reply: &str, 406 447 run_args: &RunArgs, 407 448 prev_response_id: &str, 449 + conn: &Connection, 450 + user: &User, 451 + conversations: &[Message], 408 452 ) -> Result<ChatResponse> { 409 453 let client = Client::new(); 454 + let modelname = modelfile 455 + .from 456 + .clone() 457 + .ok_or_else(|| anyhow!("Failed to get model name"))?; 458 + let prompt = modelfile 459 + .system 460 + .clone() 461 + .ok_or_else(|| anyhow!("Failed to get system prompt"))?; 462 + let convo_input = create_chat_input(input, prompt.as_str(), conversations); 410 463 let body = json!({ 411 - "model": model_name, 412 - "input": [{ 413 - "type": "message", 414 - "role": "user", 415 - "content": input 416 - }, 417 - { 418 - "type": "message", 419 - "role": "developer", 420 - "content": "" 421 - }], 464 + "model": modelname, 465 + "input": convo_input, 422 466 "reasoning": {"effort": "medium"}, 423 467 "chat_start": chat_start, 424 468 "stream": true, ··· 428 472 }); 429 473 430 474 let memory_body = json!({ 431 - "model": model_name, 475 + "model": modelname, 432 476 "input": input, 433 477 "chat_start": chat_start, 434 478 "stream": true, ··· 444 488 client.post(api_url).json(&body).send().await? 445 489 }; 446 490 491 + let chat = save_chat(conn, user, input, None)?; 447 492 let mut stream = res.bytes_stream(); 448 493 let mut accumulated = String::new(); 449 - println!(); 450 494 let mut metrics: Option<BenchmarkMetrics> = None; 451 495 let mut is_answer_start = false; 452 496 let mut prev_response_id: String = String::from(""); ··· 462 506 let data = line.trim_start_matches("data: "); 463 507 464 508 if data == "[DONE]" { 465 - return Ok(convert_to_chat_response( 509 + let mut chat_resp = convert_to_chat_response( 466 510 &accumulated, 467 511 run_args.memory, 468 512 prev_response_id, 469 513 metrics, 470 - )); 514 + ); 515 + chat_resp.parent_chat_id = Some(chat.id); 516 + return Ok(chat_resp); 471 517 } 472 518 473 519 //TODO: This will break if we ask the model to give an essay and all ··· 479 525 let model_text: Option<&str> = if run_args.memory { 480 526 v["choices"][0]["delta"]["content"].as_str() 481 527 } else { 482 - prev_response_id = serde_json::to_string(&v["id"])?; 483 - // println!("prev_id {}", prev_response_id); 528 + prev_response_id = serde_json::to_string(&v["id"])? 529 + .trim_matches('\"') 530 + .to_owned(); 531 + 484 532 if serde_json::to_string(&v["status"])?.contains("completed") { 485 533 output_completed = true; 486 534 } ··· 490 538 491 539 if let Some(delta) = model_text { 492 540 if !run_args.memory { 493 - // TODO: This doesn't support non-harmonic models, so need to handle it 494 541 if delta.contains("**[Answer]**") { 495 542 is_answer_start = true 496 543 } ··· 525 572 code: extract_python(content), 526 573 prev_response_id, 527 574 metrics, 575 + parent_chat_id: None, 528 576 } 529 577 } 530 578 ··· 574 622 Ok(path) 575 623 } 576 624 } 625 + 626 + fn create_chat_input(input: &str, prompt: &str, conversations: &[Message]) -> Vec<Message> { 627 + let dev_msg = Message { 628 + r#type: "message".to_owned(), 629 + role: Role::Developer, 630 + content: String::from(prompt), 631 + }; 632 + 633 + let input = Message { 634 + r#type: "message".to_owned(), 635 + role: Role::User, 636 + content: String::from(input), 637 + }; 638 + 639 + let last_n = if conversations.len() < 10 { 640 + conversations 641 + } else { 642 + &conversations[conversations.len() - 10..] 643 + }; 644 + 645 + if !conversations.is_empty() { 646 + let mut convo: Vec<Message> = vec![]; 647 + convo.push(dev_msg); 648 + convo.append(&mut last_n.to_vec()); 649 + convo.push(input); 650 + convo 651 + } else { 652 + vec![dev_msg, input] 653 + } 654 + }
-331
tiles/src/utils/accounts.rs
··· 1 - // Stuff related to account and identity system 2 - use anyhow::{Result, anyhow}; 3 - use std::fmt::Display; 4 - use tilekit::accounts::create_identity; 5 - use toml::Table; 6 - 7 - use crate::utils::config::save_config; 8 - const ROOT_USER_CONFIG_KEY: &str = "root-user"; 9 - 10 - const ROOT_PARSE_ERROR: &str = "Failed to parse root user config"; 11 - #[allow(dead_code)] 12 - pub struct RootUser { 13 - pub id: String, 14 - pub nickname: String, 15 - } 16 - 17 - impl Display for RootUser { 18 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 - write!(f, "id: {}\nnickname: {}\n", self.id, self.nickname) 20 - } 21 - } 22 - 23 - impl RootUser { 24 - pub fn new(config: &Table) -> Result<Self> { 25 - let id = config 26 - .get("id") 27 - .ok_or_else(|| anyhow!("Missing ID"))? 28 - .as_str() 29 - .ok_or_else(|| anyhow!("ID not a string"))?; 30 - let nickname = config 31 - .get("nickname") 32 - .ok_or_else(|| anyhow!("Missing Nickname"))? 33 - .as_str() 34 - .ok_or_else(|| anyhow!("Nickname not a string"))?; 35 - Ok(RootUser { 36 - id: id.to_owned(), 37 - nickname: nickname.to_owned(), 38 - }) 39 - } 40 - 41 - pub fn to_table(&self) -> Table { 42 - let mut root_user_table = Table::new(); 43 - root_user_table.insert(String::from("id"), toml::Value::String(self.id.clone())); 44 - root_user_table.insert( 45 - String::from("nickname"), 46 - toml::Value::String(self.nickname.clone()), 47 - ); 48 - root_user_table 49 - } 50 - } 51 - 52 - /// Returns a `RootUser`, which represents a root user 53 - /// 54 - /// # Params 55 - /// 56 - /// - config: A `Table` type of entire config.toml file 57 - pub fn get_root_user_details(config: &Table) -> Result<RootUser> { 58 - let root_user = config 59 - .get(ROOT_USER_CONFIG_KEY) 60 - .ok_or_else(|| anyhow!(ROOT_PARSE_ERROR))?; 61 - let root_user_table = root_user 62 - .as_table() 63 - .ok_or_else(|| anyhow!("root user not a table"))?; 64 - RootUser::new(root_user_table) 65 - } 66 - 67 - /// Create a root account 68 - /// Stores the private credentials in OS secure password manager 69 - /// 70 - /// # Params 71 - /// 72 - /// - config: A `Table` type of entire config.toml file 73 - /// - nickname: Nickname for the identity (Optional) 74 - /// 75 - /// Returns the root_user_config as a `Table` type 76 - pub fn create_root_account(config: &Table, nickname: Option<String>) -> Result<Table> { 77 - let root_user = config 78 - .get(ROOT_USER_CONFIG_KEY) 79 - .ok_or_else(|| anyhow!("{} doesn't exist", ROOT_USER_CONFIG_KEY))?; 80 - let root_user_table = root_user 81 - .as_table() 82 - .ok_or_else(|| anyhow!(ROOT_PARSE_ERROR))?; 83 - let root_user_data = RootUser::new(root_user_table)?; 84 - let did = root_user_data.id; 85 - if did.is_empty() { 86 - Ok(create_root_user(root_user_table, nickname)?) 87 - } else { 88 - Ok(root_user_table.clone()) 89 - } 90 - } 91 - 92 - /// Save the root config in `Table` type to config.toml 93 - /// 94 - /// # Params 95 - /// 96 - /// - config: A `Table` type of entire config.toml file 97 - /// - root_user_config: A `Table` type of root user 98 - pub fn save_root_account(mut config: Table, root_user_config: &Table) -> Result<()> { 99 - config.insert( 100 - String::from(ROOT_USER_CONFIG_KEY), 101 - toml::Value::Table(root_user_config.clone()), 102 - ); 103 - save_config(&config) 104 - } 105 - 106 - /// Sets nickname for the root account 107 - /// 108 - /// # Params 109 - /// 110 - /// - config: A `Table` type of entire config.toml file 111 - /// - nickname: Nickname for the identity 112 - /// 113 - /// Returns the root_user_config as a `Table` type 114 - pub fn set_nickname(config: &Table, nickname: &str) -> Result<Table> { 115 - let root_user = config 116 - .get(ROOT_USER_CONFIG_KEY) 117 - .ok_or_else(|| anyhow!("{} doesn't exist", ROOT_USER_CONFIG_KEY))?; 118 - 119 - let mut root_user_table = root_user 120 - .as_table() 121 - .ok_or_else(|| anyhow!(ROOT_PARSE_ERROR))? 122 - .clone(); 123 - let did = root_user_table 124 - .get("id") 125 - .and_then(|v| v.as_str()) 126 - .ok_or(anyhow!("Failed to get id from config"))?; 127 - if did.is_empty() { 128 - Err(anyhow::anyhow!("No Root user available")) 129 - } else { 130 - root_user_table.insert("id".to_owned(), toml::Value::String(did.to_owned())); 131 - root_user_table.insert( 132 - "nickname".to_owned(), 133 - toml::Value::String(nickname.to_owned()), 134 - ); 135 - Ok(root_user_table) 136 - } 137 - } 138 - 139 - fn create_root_user(root_user_config: &Table, nickname: Option<String>) -> Result<Table> { 140 - let mut root_user_table = root_user_config.clone(); 141 - match create_identity("tiles") { 142 - Ok(did) => { 143 - root_user_table.insert("id".to_owned(), toml::Value::String(did)); 144 - if let Some(nickname) = nickname { 145 - root_user_table.insert("nickname".to_owned(), toml::Value::String(nickname)); 146 - } 147 - Ok(root_user_table) 148 - } 149 - Err(err) => Err(err), 150 - } 151 - } 152 - 153 - #[cfg(test)] 154 - mod tests { 155 - use anyhow::Result; 156 - use keyring::{mock, set_default_credential_builder}; 157 - use toml::Table; 158 - 159 - use crate::utils::accounts::{ 160 - RootUser, create_root_account, get_root_user_details, set_nickname, 161 - }; 162 - 163 - #[test] 164 - fn test_get_root_user_details_empty_id() -> Result<()> { 165 - let config: Table = toml::from_str( 166 - r#" 167 - [root-user] 168 - id = '' 169 - nickname = '' 170 - "#, 171 - ) 172 - .unwrap(); 173 - let acc_details = get_root_user_details(&config)?; 174 - assert!(acc_details.id.is_empty()); 175 - Ok(()) 176 - } 177 - 178 - #[test] 179 - fn test_get_root_user_details_valid_id() -> Result<()> { 180 - let config: Table = toml::from_str( 181 - r#" 182 - [root-user] 183 - id = 'did:key:xyz' 184 - nickname = '' 185 - "#, 186 - ) 187 - .unwrap(); 188 - let acc_details = get_root_user_details(&config)?; 189 - assert!(acc_details.id.contains("did:key")); 190 - Ok(()) 191 - } 192 - 193 - #[test] 194 - fn test_create_root_account_but_exists() { 195 - let config: Table = toml::from_str( 196 - r#" 197 - [root-user] 198 - id = 'did:key:xyz' 199 - nickname = '' 200 - "#, 201 - ) 202 - .unwrap(); 203 - let root_user = create_root_account(&config, None).unwrap(); 204 - 205 - assert_eq!( 206 - root_user.get("id").unwrap().as_str().unwrap(), 207 - "did:key:xyz" 208 - ); 209 - } 210 - 211 - #[test] 212 - fn test_create_root_account_new() { 213 - set_default_credential_builder(mock::default_credential_builder()); 214 - let config: Table = toml::from_str( 215 - r#" 216 - [root-user] 217 - id = '' 218 - nickname = '' 219 - "#, 220 - ) 221 - .unwrap(); 222 - let root_user = create_root_account(&config, None).unwrap(); 223 - 224 - assert_ne!( 225 - root_user.get("id").unwrap().as_str().unwrap(), 226 - "did:key:xyz" 227 - ); 228 - 229 - assert!( 230 - root_user 231 - .get("id") 232 - .unwrap() 233 - .as_str() 234 - .unwrap() 235 - .starts_with("did:key") 236 - ); 237 - } 238 - 239 - #[test] 240 - fn test_create_root_account_new_w_nickname() { 241 - set_default_credential_builder(mock::default_credential_builder()); 242 - let config: Table = toml::from_str( 243 - r#" 244 - [root-user] 245 - id = '' 246 - nickname = '' 247 - "#, 248 - ) 249 - .unwrap(); 250 - let root_user = create_root_account(&config, Some(String::from("madclaws"))).unwrap(); 251 - 252 - assert_ne!( 253 - root_user.get("id").unwrap().as_str().unwrap(), 254 - "did:key:xyz" 255 - ); 256 - 257 - assert!( 258 - root_user 259 - .get("id") 260 - .unwrap() 261 - .as_str() 262 - .unwrap() 263 - .starts_with("did:key") 264 - ); 265 - 266 - assert_eq!( 267 - root_user.get("nickname").unwrap().as_str().unwrap(), 268 - "madclaws" 269 - ); 270 - } 271 - 272 - #[test] 273 - fn test_get_root_user_details_missing_key() { 274 - let config: Table = toml::from_str( 275 - r#" 276 - # no root-user table 277 - [other] 278 - foo = "bar" 279 - "#, 280 - ) 281 - .unwrap(); 282 - 283 - let res = get_root_user_details(&config); 284 - assert!(res.is_err(), "Expected error when root-user key is missing"); 285 - } 286 - 287 - #[test] 288 - fn test_root_user_new_wrong_types() { 289 - // id is integer, nickname is table 290 - let config: Table = toml::from_str( 291 - r#" 292 - [root-user] 293 - id = 123 294 - nickname = { nested = "value" } 295 - "#, 296 - ) 297 - .unwrap(); 298 - 299 - let root_tbl = config.get("root-user").unwrap().as_table().unwrap().clone(); 300 - assert!( 301 - RootUser::new(&root_tbl).is_err(), 302 - "Expected error for wrong types" 303 - ); 304 - } 305 - 306 - #[test] 307 - fn test_root_user_roundtrip_table() -> Result<()> { 308 - let user = RootUser { 309 - id: "did:key:abc".into(), 310 - nickname: "nick".into(), 311 - }; 312 - let tbl = user.to_table(); 313 - let parsed = RootUser::new(&tbl)?; 314 - assert_eq!(parsed.id, user.id); 315 - assert_eq!(parsed.nickname, user.nickname); 316 - Ok(()) 317 - } 318 - 319 - #[test] 320 - fn test_set_nickname_but_invalid_config() { 321 - let config: Table = toml::from_str( 322 - r#" 323 - [ruser] 324 - id = '' 325 - "#, 326 - ) 327 - .unwrap(); 328 - 329 - assert!(set_nickname(&config, "madclaws").is_err()) 330 - } 331 - }
+22 -2
tiles/src/utils/config.rs
··· 88 88 } 89 89 90 90 fn get_user_data_dir(&self) -> Result<PathBuf> { 91 - let data_dir = self.get_data_dir()?; 92 - Ok(data_dir.join("data")) 91 + let root_config = get_or_create_config()?; 92 + let data_config = root_config 93 + .get("data") 94 + .expect("Failed to get data") 95 + .as_table() 96 + .expect("Failed to parse to table (data)") 97 + .clone(); 98 + 99 + if let Some(path) = data_config 100 + .get("path") 101 + .expect("failed to parse data -> path") 102 + .as_str() 103 + { 104 + if path.is_empty() { 105 + let data_dir = self.get_data_dir()?; 106 + Ok(data_dir.join("data")) 107 + } else { 108 + PathBuf::from_str(path).map_err(|_e| anyhow!("Failed to convert to pathbuf")) 109 + } 110 + } else { 111 + Err(anyhow!("Failed to get data path")) 112 + } 93 113 } 94 114 95 115 fn get_lib_dir(&self) -> Result<PathBuf> {
+9 -1
tiles/src/utils/mod.rs
··· 1 - pub mod accounts; 1 + use std::time::{SystemTime, UNIX_EPOCH}; 2 + 2 3 pub mod config; 3 4 pub mod hf_model_downloader; 4 5 pub mod installer; 6 + 7 + pub fn get_unix_time_now() -> u64 { 8 + SystemTime::now() 9 + .duration_since(UNIX_EPOCH) 10 + .expect("time went backwards") 11 + .as_secs() 12 + }