taskai-cli 1.5.4__tar.gz → 1.6.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/DEVLOG.md +116 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/DEVPLAN.md +165 -52
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/PKG-INFO +6 -5
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/README.md +4 -3
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/pyproject.toml +2 -2
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/cli.py +8 -4
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/help_menu.py +2 -0
- taskai_cli-1.6.2/taskai/llm_models.py +57 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/models.py +2 -3
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/services/ai.py +52 -29
- taskai_cli-1.6.2/taskai/services/user_setup.py +75 -0
- taskai_cli-1.5.4/taskai/services/user_setup.py +0 -63
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/.github/workflows/publish-to-pypi.yml +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/.gitignore +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/.readthedocs.yaml +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/docs/conf.py +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/docs/index.rst +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/docs/requirements.txt +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/scripts/bump_version.py +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/browser.py +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/config.py +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/json_dir_database.py +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/services/pomodoro.py +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/services/repair_database.py +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/static/canvas.js +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/static/console.js +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/static/editpanel.js +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/static/index.html +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/static/style.css +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/taskai/views.py +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/test/test_cli.py +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/test/test_execution.py +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/test/test_json_dir_database.py +0 -0
- {taskai_cli-1.5.4 → taskai_cli-1.6.2}/test/test_view.py +0 -0
|
@@ -1,3 +1,119 @@
|
|
|
1
|
+
# 8-22
|
|
2
|
+
|
|
3
|
+
Found and fixed a real bug from the `litellm` migration earlier today: it
|
|
4
|
+
auto-loads `.env` via `python-dotenv` on import, and the project's `.env`
|
|
5
|
+
had a stray first line (`source .taskai-venv/bin/activate` — not a
|
|
6
|
+
`KEY=VALUE` pair), so every `task ai` run printed a "could not parse
|
|
7
|
+
statement starting at line 1" warning. Non-fatal (`GEMINI_API_KEY` on line
|
|
8
|
+
2 still loaded fine) but confusing. Diagnosed by reading `litellm/__init__.py`
|
|
9
|
+
directly rather than guessing — it calls `_dotenv.load_dotenv(...)` at
|
|
10
|
+
import time. Left the actual `.env` edit to Alex since it holds a live key.
|
|
11
|
+
|
|
12
|
+
Also stripped two leftover debug `print()`s from `ai_natural_language_service`
|
|
13
|
+
(the raw prompt and the full constructed `ai_prompt` — fine for iterating on
|
|
14
|
+
the prompt, noise now that this is a user-facing command) and reworked the
|
|
15
|
+
"about to run these commands" printout: instead of one `print(response_json)`
|
|
16
|
+
dump of the raw parsed list, it now prints each command right before running
|
|
17
|
+
it, reconstructed as `ai run 'task <command> ...'` (quoting only values with
|
|
18
|
+
spaces) via a small `_format_command_str()` helper — reads like a real
|
|
19
|
+
command instead of a dict dump, and shows progress as it goes rather than
|
|
20
|
+
front-loading everything.
|
|
21
|
+
|
|
22
|
+
Added `--context` and `--reasoning` to `task ai <prompt>` (DEVPLAN 2.2,
|
|
23
|
+
below, has the durable version of this):
|
|
24
|
+
|
|
25
|
+
- `--context a.md,b.md` reads file(s) and folds them into the prompt.
|
|
26
|
+
- While wiring it up, found `execute_commands`'s `case "ai":` dispatch
|
|
27
|
+
never forwarded `**kwargs` to `Controller.ai_natural_language` at all —
|
|
28
|
+
`--context` would've parsed fine and then been silently dropped before
|
|
29
|
+
reaching the service. Fixed by threading `**kwargs` through the whole
|
|
30
|
+
chain (`cli.py`'s dispatch → `Controller.ai_natural_language` →
|
|
31
|
+
`ai_natural_language_service`).
|
|
32
|
+
- `--reasoning <level>` came out of a tangent about `task ai` latency: Alex
|
|
33
|
+
asked whether "respond quickly" in the prompt does anything (no — output
|
|
34
|
+
token generation is sequential/autoregressive, prompt wording can't speed
|
|
35
|
+
that up) and what actually causes it. Real answer: some Gemini tiers
|
|
36
|
+
generate a hidden "thinking" pass before the visible output, which is a
|
|
37
|
+
genuine invisible latency cost. Read litellm's Gemini transformation
|
|
38
|
+
source directly to confirm the actual knob: `reasoning_effort` is a
|
|
39
|
+
standard litellm kwarg on `completion()`, mapped per-provider — accepts
|
|
40
|
+
`minimal|low|medium|high|disable|none`. Confirmed a real wrinkle for this
|
|
41
|
+
project specifically: on Gemini 2.5-series models it maps to a
|
|
42
|
+
`thinkingBudget` token count and `"none"` zeroes it out completely, but
|
|
43
|
+
on Gemini 3.x (what `llm_models.py`'s menu defaults to) it maps to a
|
|
44
|
+
`thinkingLevel` enum, and litellm's own code comment says Gemini 3
|
|
45
|
+
*cannot* fully disable thinking — `"disable"`/`"none"` just fall back to
|
|
46
|
+
the minimum level there. Only exposed for reasoning-capable tiers
|
|
47
|
+
(`supports_reasoning(...)` gated internally), so it may no-op or error on
|
|
48
|
+
something like Flash-Lite.
|
|
49
|
+
- Tried defaulting `--reasoning` to `"disable"` when omitted, then Alex
|
|
50
|
+
reverted that — omitting the flag now sends no `reasoning_effort` at all,
|
|
51
|
+
leaving it up to whatever the model/provider defaults to.
|
|
52
|
+
- Documented both flags under `task ai` in `help_menu.py` (this file is
|
|
53
|
+
also the AI's own command reference, so it needs to stay accurate even
|
|
54
|
+
though neither flag is something the model itself would ever emit).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
Migrated AI backend off direct `google-genai` calls, landing on `litellm`
|
|
59
|
+
after a detour through `aisuite`:
|
|
60
|
+
|
|
61
|
+
- First tried `aisuite` per DEVPLAN's original Phase 2 plan. Downloaded and
|
|
62
|
+
read the actual package source before committing to it (not just docs) —
|
|
63
|
+
found `aisuite`'s only Google provider is Vertex AI
|
|
64
|
+
(`GOOGLE_PROJECT_ID`/`GOOGLE_REGION`/a service-account JSON), with no path
|
|
65
|
+
to a plain Gemini/AI-Studio API key. Since the existing setup is just a
|
|
66
|
+
simple API key, that would've been a real functionality change disguised
|
|
67
|
+
as a library swap. Flagged it to Alex before writing any code.
|
|
68
|
+
- Alex opted to rework the setup rather than force-fit the old flow, but
|
|
69
|
+
then asked whether another compat layer supported plain Gemini API keys —
|
|
70
|
+
it does: `litellm` treats `gemini` and `vertex_ai` as separate providers
|
|
71
|
+
(confirmed by reading `litellm/utils.py`'s `validate_environment` and
|
|
72
|
+
`litellm/types/utils.py`'s `LlmProviders` enum directly), so
|
|
73
|
+
`gemini/<model>` + `GEMINI_API_KEY` keeps the current simple setup working
|
|
74
|
+
end to end. Redid the migration against `litellm` instead.
|
|
75
|
+
- `pyproject.toml`: `google-genai` → `litellm>=1.98`.
|
|
76
|
+
- `taskai/models.py`: `CLIConfig.GEMINI_MODEL`/`GEMINI_API_KEY` collapsed to
|
|
77
|
+
a single `AI_MODEL: str` (a `provider/model` string, litellm's own
|
|
78
|
+
separator).
|
|
79
|
+
- `taskai/services/ai.py`: both `ai_headstart_service` and
|
|
80
|
+
`ai_natural_language_service` now call
|
|
81
|
+
`litellm.completion(model=model_name, messages=[...])` instead of
|
|
82
|
+
`google.genai`; dropped the `api_key`/`@config("GEMINI_API_KEY", ...)`
|
|
83
|
+
plumbing since litellm reads provider credentials straight from env vars.
|
|
84
|
+
- New [taskai/llm_models.py](taskai/llm_models.py) (top-level, not under
|
|
85
|
+
`services/` — it's reference data, not a service): `PROVIDER_ENV_VARS`
|
|
86
|
+
(which env var(s) litellm expects per provider) and `PROVIDER_MODELS` (a
|
|
87
|
+
best-effort, non-exhaustive menu of known models per provider). Both
|
|
88
|
+
built by reading litellm's source directly rather than guessing.
|
|
89
|
+
- `services/user_setup.py` reworked into a two-step picker
|
|
90
|
+
(`_select_from_list`): pick a provider from a numbered menu, then pick a
|
|
91
|
+
model for that provider from another numbered menu. Either step also
|
|
92
|
+
accepts free-typed text instead of a number, so a stale/missing
|
|
93
|
+
`PROVIDER_MODELS` entry (several providers — `azure`, `ollama` — are
|
|
94
|
+
intentionally empty, since their "models" are a deployment name or
|
|
95
|
+
whatever you've pulled locally) never blocks setup.
|
|
96
|
+
- Caught a live example of "models go stale fast" mid-session: Alex hit
|
|
97
|
+
`gemini-3.6-flash` referenced by Google's own tooling, which wasn't in
|
|
98
|
+
training data. Web-searched Google's current model docs and corrected the
|
|
99
|
+
`gemini`/`vertex_ai` entries in `llm_models.py` — `gemini-2.0-flash` (the
|
|
100
|
+
old hardcoded default) was actually shut down 2026-06-01. If `task
|
|
101
|
+
setup`'s Gemini options look wrong again, re-check against
|
|
102
|
+
https://ai.google.dev/gemini-api/docs/models rather than trusting the
|
|
103
|
+
hardcoded list.
|
|
104
|
+
- README's Config section updated to match (`AI_MODEL` as `provider/model`,
|
|
105
|
+
points at `task setup`).
|
|
106
|
+
- Not yet done: `pip install "litellm>=1.98"` / `pip uninstall google-genai`
|
|
107
|
+
in `.taskai-venv` — Alex needs to run this manually (environment-mutating
|
|
108
|
+
commands aren't run unprompted in this project).
|
|
109
|
+
|
|
110
|
+
DEVPLAN.md's Phase 2 (2.1, 2.3) updated to match — checked off, `aisuite`
|
|
111
|
+
references swapped for `litellm`, and 2.4 (the later tool-calling-loop
|
|
112
|
+
entry point) flagged for re-scoping since it was written around `aisuite`'s
|
|
113
|
+
automatic tool-runner, which `litellm` has no equivalent for.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
1
117
|
# 8-19
|
|
2
118
|
|
|
3
119
|
Fixed a typo in `browser.py` that caused uvicorn to fail with "Could not import
|
|
@@ -24,10 +24,10 @@ depth, and the same node type is used whether it's acting as a "list" or a
|
|
|
24
24
|
(`recurs_every`/`recurs_until`/`recur_keep_incomplete`). `Comment` is a
|
|
25
25
|
simple `{content, item_id, created_on}`. `UserData` is the whole-DB
|
|
26
26
|
container: `todo_items: dict[str, dict]`, `comments: dict[str, dict]`,
|
|
27
|
-
`config: dict`, `id_counter`. `CLIConfig`
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
`DISPLAY_COLORS` for CLI rendering.
|
|
27
|
+
`config: dict`, `id_counter`. `CLIConfig` holds a single `AI_MODEL` field (a
|
|
28
|
+
`provider/model` string, e.g. `gemini/gemini-3.6-flash` — set via `task
|
|
29
|
+
setup`'s two-step provider→model picker, done in Phase 2) plus
|
|
30
|
+
`DISPLAY_STRING`/`DISPLAY_COLORS` for CLI rendering.
|
|
31
31
|
|
|
32
32
|
**Storage.** [taskai/json_dir_database.py](taskai/json_dir_database.py):
|
|
33
33
|
`JsonDirectoryDatabase` — one JSON file per user under `.taskai/task_db/
|
|
@@ -64,9 +64,11 @@ opening a second connection.
|
|
|
64
64
|
`ai_headstart_service` (single item → LLM suggests a next concrete step,
|
|
65
65
|
saved as a comment) and `ai_natural_language_service` (prompt → LLM returns
|
|
66
66
|
a JSON list of `{command, args, kwargs}` → executed through
|
|
67
|
-
`taskai.cli.execute_commands`, one call per entry). Both
|
|
68
|
-
|
|
69
|
-
`
|
|
67
|
+
`taskai.cli.execute_commands`, one call per entry). Both call
|
|
68
|
+
`litellm.completion(model=model_name, messages=[...])` — provider-agnostic
|
|
69
|
+
per the `AI_MODEL` config value, no more hardcoded Gemini/`google-genai`.
|
|
70
|
+
See [taskai/llm_models.py](taskai/llm_models.py) for the provider→env-var
|
|
71
|
+
and provider→model reference data `task setup` uses. The natural-language prompt embeds
|
|
70
72
|
[taskai/help_menu.py](taskai/help_menu.py)'s `help_general` string as the
|
|
71
73
|
LLM's command reference — **that file is the single source of truth for the
|
|
72
74
|
command surface, shown to humans via `task help` and to the LLM verbatim.
|
|
@@ -75,12 +77,19 @@ it's gone stale before.
|
|
|
75
77
|
|
|
76
78
|
**Known code quirks (found but intentionally not fixed — out of scope
|
|
77
79
|
unless you're told to fix them):**
|
|
78
|
-
-
|
|
80
|
+
- ~~`Controller.update_item` (`cli.py`) resolves a non-numeric `item_id` via
|
|
79
81
|
`_find_model_by_stringmatch` but assigns the *whole model object* back
|
|
80
|
-
into `item_id` instead of pulling `.id` off it
|
|
81
|
-
|
|
82
|
-
`
|
|
83
|
-
|
|
82
|
+
into `item_id` instead of pulling `.id` off it~~ **Fixed.** All identifier
|
|
83
|
+
resolution (id or name) now routes through one function,
|
|
84
|
+
`Controller._resolve_item()` (throws via `throw_error` if not found; a
|
|
85
|
+
non-throwing `_find_item_by_identifier` still backs the soft-fail `show`
|
|
86
|
+
path). Every command that takes an item identifier — `update`, `rename`,
|
|
87
|
+
`status`, `done`/`complete`, `comment`, `depend`, `reorder`, `move`,
|
|
88
|
+
`link`, `delete`, `create`'s parent — now accepts a name via `fnmatch`,
|
|
89
|
+
not just a numeric id. Net effect was less code, not more: two
|
|
90
|
+
name-only-duplicate methods (`show_by_item_name`, `delete_item_by_name`)
|
|
91
|
+
and several inline `_is_int(...)` dispatch branches in `execute_commands`
|
|
92
|
+
were deleted outright rather than kept alongside the id path.
|
|
84
93
|
- `task show <id1>,<id2>,...` is documented in the README but
|
|
85
94
|
`execute_commands`'s `show` case never dispatches to
|
|
86
95
|
`Controller.show_items`/`view_items` — there's no wired path to it.
|
|
@@ -336,48 +345,152 @@ through the existing `Controller`.
|
|
|
336
345
|
|
|
337
346
|
## Phase 2 — AI tools
|
|
338
347
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
-
|
|
356
|
-
`
|
|
357
|
-
|
|
358
|
-
`
|
|
359
|
-
`
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
348
|
+
Two entry points, built in stages, not one replacing the other:
|
|
349
|
+
**`task ai <prompt>`** (2.1-2.3 below) is the existing one-shot flow —
|
|
350
|
+
model sees the prompt once, returns a full JSON list of commands up front,
|
|
351
|
+
they execute with no gating, same as today just on `litellm`. **`task ai
|
|
352
|
+
agent <prompt>`** (2.4, later) is a separate, more elaborate entry point for
|
|
353
|
+
multi-step workflows using a real tool-calling loop, added only after 2.1-2.3
|
|
354
|
+
are working and any kinks (context injection, multi-file support) are worked
|
|
355
|
+
out on the simpler path first.
|
|
356
|
+
|
|
357
|
+
### 2.1 — `task ai <prompt>` on `litellm` (no gating, same one-shot shape as today)
|
|
358
|
+
|
|
359
|
+
- [x] Depended on the Phase 0 fixes (prompt bug, `_add_info` bug, actually
|
|
360
|
+
executing parsed commands) — done.
|
|
361
|
+
- [x] Originally planned on `aisuite` (below was written for it), but
|
|
362
|
+
pivoted after reading `aisuite`'s actual provider source: its only
|
|
363
|
+
Google provider is Vertex AI (`GOOGLE_PROJECT_ID`/`GOOGLE_REGION`/a
|
|
364
|
+
service-account JSON) — no path to a plain Gemini/AI-Studio API key,
|
|
365
|
+
which is what the existing setup actually used. `litellm` has a
|
|
366
|
+
separate `gemini` provider (distinct from its own `vertex_ai`) that
|
|
367
|
+
reads `GEMINI_API_KEY` directly, so the simple-API-key setup carries
|
|
368
|
+
over unchanged. Added `litellm>=1.98` to `pyproject.toml`; dropped
|
|
369
|
+
`google-genai` and the `from google import genai` import from
|
|
370
|
+
`services/ai.py`.
|
|
371
|
+
- [x] Swapped both call sites (`ai_headstart_service`,
|
|
372
|
+
`ai_natural_language_service`) to
|
|
373
|
+
`litellm.completion(model=model_name, messages=[...])`, reading
|
|
374
|
+
`response.choices[0].message.content`.
|
|
375
|
+
**Scope note:** plain chat completions only, no `tools=`. The model
|
|
376
|
+
still returns one JSON list of `{command, args, kwargs}` up front,
|
|
377
|
+
same shape/contract as today, executed through `execute_commands`
|
|
378
|
+
with no confirmation step — that's deliberate for this pass, not an
|
|
379
|
+
oversight. The tool-calling loop is 2.4, later, not here. Model
|
|
380
|
+
strings are `provider/model` (litellm's separator, e.g.
|
|
381
|
+
`gemini/gemini-3.6-flash`, `openai/gpt-4o-mini`).
|
|
382
|
+
- [x] `litellm` reads provider credentials from standard provider env vars
|
|
383
|
+
(`OPENAI_API_KEY`, `GEMINI_API_KEY`, etc.), not a key passed in.
|
|
384
|
+
Dropped the `@config("GEMINI_API_KEY", "api_key")` plumbing entirely —
|
|
366
385
|
one less thing to configure and one less place a secret sits in the
|
|
367
386
|
plaintext user JSON db.
|
|
368
|
-
- [
|
|
369
|
-
`AI_MODEL` field holding the `provider
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
387
|
+
- [x] Renamed `CLIConfig.GEMINI_MODEL`/`GEMINI_API_KEY` to a single
|
|
388
|
+
`AI_MODEL` field holding the `provider/model` string.
|
|
389
|
+
- [x] Kept printing the parsed command list before executing it
|
|
390
|
+
(`print(response_json)`) — cheap safety net given the parsed commands
|
|
391
|
+
can include `delete`/`nuke`. Still just visibility, not a confirmation
|
|
392
|
+
prompt — real gating is what 2.4 adds.
|
|
393
|
+
|
|
394
|
+
### 2.2 — Context injection (`--context`), including the multi-file kink
|
|
395
|
+
|
|
396
|
+
- [x] `--context <path1,path2,...>` on `task ai <prompt>`: comma-separated
|
|
397
|
+
as planned (sidesteps the `_parse_remaining` repeated-flag kink
|
|
398
|
+
entirely — never needed to touch it). `_read_context_files()` in
|
|
399
|
+
`services/ai.py` reads each file and folds them into the prompt as
|
|
400
|
+
`--- <path> ---\n<contents>` sections, inserted between the item tree
|
|
401
|
+
and the user's actual prompt.
|
|
402
|
+
- [x] Real landmine found while wiring this up, not in the original plan:
|
|
403
|
+
`execute_commands`'s `case "ai":` dispatch built the prompt string
|
|
404
|
+
from `args[1:]` but never forwarded `**kwargs` to
|
|
405
|
+
`Controller.ai_natural_language` at all — any `--flag` on `task ai
|
|
406
|
+
...` was silently parsed and then dropped on the floor before
|
|
407
|
+
`--context` even had a chance to matter. Fixed by forwarding
|
|
408
|
+
`**kwargs` through the `case "ai":` dispatch and having
|
|
409
|
+
`Controller.ai_natural_language` (`cli.py`) and
|
|
410
|
+
`ai_natural_language_service` (`services/ai.py`) both accept it
|
|
411
|
+
explicitly. A missing/misspelled path just raises `FileNotFoundError`
|
|
412
|
+
naturally, caught by `execute_commands`'s existing catch-all — no new
|
|
413
|
+
error handling needed.
|
|
414
|
+
- [x] Documented `--context` (and `--reasoning`, below) under `task ai` in
|
|
415
|
+
`help_menu.py`, which is also the AI's own command reference — this
|
|
416
|
+
command doesn't affect what the LLM is allowed to emit (it's a
|
|
417
|
+
human-only flag on the `ai` invocation itself, not a command the
|
|
418
|
+
model generates), but the doc was out of sync with reality otherwise.
|
|
419
|
+
|
|
420
|
+
**Also added, not originally scoped:** `--reasoning <level>` on `task ai
|
|
421
|
+
<prompt>`, passed straight through as `reasoning_effort` on the
|
|
422
|
+
`litellm.completion(...)` call — came out of a discussion about `task ai`
|
|
423
|
+
latency. Accepted values (`minimal|low|medium|high|disable|none`) aren't
|
|
424
|
+
validated client-side; an invalid value raises litellm's own
|
|
425
|
+
`Invalid reasoning effort: <value>` error, which already surfaces cleanly
|
|
426
|
+
through the same catch-all. No default is set — omitting the flag means no
|
|
427
|
+
`reasoning_effort` is sent at all, so behavior is whatever the model/provider
|
|
428
|
+
defaults to. (A `"disable"` default was tried and explicitly reverted this
|
|
429
|
+
session — worth knowing if it comes up again.) Confirmed by reading
|
|
430
|
+
litellm's Gemini transformation source directly: on Gemini 2.5-series models
|
|
431
|
+
this maps to a `thinkingBudget` token count and `"none"` genuinely zeroes
|
|
432
|
+
it out, but on Gemini 3.x (the default in `llm_models.py`'s menu) it maps
|
|
433
|
+
to a `thinkingLevel` enum and litellm's own code comment states Gemini 3
|
|
434
|
+
*cannot* fully disable thinking — `"disable"`/`"none"` just fall back to
|
|
435
|
+
the minimum level. Also gated behind `supports_reasoning(model, ...)`
|
|
436
|
+
internally, so it's silently a no-op (or may error) on non-reasoning tiers
|
|
437
|
+
like Flash-Lite.
|
|
438
|
+
|
|
439
|
+
### 2.3 — Update setup script for `litellm`
|
|
440
|
+
|
|
441
|
+
- [x] `services/user_setup.py` reworked into a two-step picker: select a
|
|
442
|
+
provider, then select a model, both via a numbered-menu-or-free-text
|
|
443
|
+
prompt (`_select_from_list`) — typing a number picks from the menu,
|
|
444
|
+
typing anything else is taken literally, so a stale/missing menu entry
|
|
445
|
+
never blocks you. Combines to `AI_MODEL = "<provider>/<model>"` and
|
|
446
|
+
prints the exact env var(s) that provider needs.
|
|
447
|
+
- [x] New [taskai/llm_models.py](taskai/llm_models.py) (top-level, not
|
|
448
|
+
under `services/`, since it's reference data, not a service):
|
|
449
|
+
`PROVIDER_ENV_VARS` and `PROVIDER_MODELS`, both keyed by litellm's
|
|
450
|
+
real provider names, pulled from `litellm.types.utils.LlmProviders`
|
|
451
|
+
and `litellm.utils.validate_environment` directly rather than
|
|
452
|
+
guessed. `PROVIDER_MODELS` is a non-exhaustive, best-effort menu —
|
|
453
|
+
model names go stale fast, confirmed live mid-session: the `gemini`
|
|
454
|
+
entries needed a web-search correction after `gemini-2.0-flash`
|
|
455
|
+
turned out to have been shut down 2026-06-01 and `gemini-3.6-flash`
|
|
456
|
+
didn't exist yet in training data. Re-verify against
|
|
457
|
+
https://ai.google.dev/gemini-api/docs/models if `task setup`'s Gemini
|
|
458
|
+
options look off again.
|
|
459
|
+
- [x] Updated README's Config section to match the new `AI_MODEL`
|
|
460
|
+
`provider/model` shape and point at `task setup`.
|
|
461
|
+
|
|
462
|
+
### 2.4 — `task ai agent <prompt>` (later — real tool-calling loop)
|
|
463
|
+
|
|
464
|
+
A separate entry point, not a replacement for `task ai`. `task ai <prompt>`
|
|
465
|
+
stays the fast, ungated, one-shot-plan-and-execute path from 2.1-2.3;
|
|
466
|
+
`task ai agent <prompt>` is for workflows where the model actually needs to
|
|
467
|
+
see intermediate results (e.g. the real id of an item it just created)
|
|
468
|
+
before deciding the next step, which a one-shot plan can't do.
|
|
469
|
+
|
|
470
|
+
**Note (post-litellm-pivot):** this section was originally scoped around
|
|
471
|
+
`aisuite`'s automatic tool-runner (`tools=[...]` + `max_turns`, which drives
|
|
472
|
+
the call → execute → feed-result-back loop for you). `litellm.completion`
|
|
473
|
+
also accepts `tools=[...]` (OpenAI-style function-calling schema) but has no
|
|
474
|
+
automatic multi-turn runner — the loop below would need to be hand-rolled.
|
|
475
|
+
Re-scope this section before starting 2.4.
|
|
476
|
+
|
|
477
|
+
- [ ] Small, explicitly-typed wrapper functions — one per meaningful
|
|
478
|
+
command (create, update, complete, delete, comment, depend, link,
|
|
479
|
+
move; roughly 8-12 total, not one-per-every-CLI-verb) — calling
|
|
480
|
+
straight into the existing `Controller` methods, exposed as
|
|
481
|
+
`tools=[...]` to `litellm.completion`.
|
|
482
|
+
- [ ] Hand-roll the call → execute → feed-result-back loop (no automatic
|
|
483
|
+
runner in `litellm`, unlike the originally-planned `aisuite`), capped
|
|
484
|
+
at some max number of turns.
|
|
485
|
+
- [ ] Gate destructive tools (`delete`, `nuke`) behind an explicit yes/no
|
|
486
|
+
confirmation before executing — not just the print-before-execute
|
|
487
|
+
visibility 2.1 has.
|
|
488
|
+
- [ ] Print/log the tool-call transcript as it runs so the agent shows its
|
|
489
|
+
work — same spirit as 2.1's visibility bullet, scaled to a multi-step
|
|
490
|
+
run.
|
|
491
|
+
- [ ] Flagged, not scoped yet: multi-turn memory across calls in one
|
|
492
|
+
session, a per-call `--model` override, streaming for plain-text
|
|
493
|
+
responses (`headstart`). Revisit after 2.4 ships if still wanted.
|
|
381
494
|
|
|
382
495
|
---
|
|
383
496
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: taskai-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.2
|
|
4
4
|
Author-email: Alex Paskal <alexcpaskal@gmail.com>
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: fastapi>=0.141.1
|
|
7
|
-
Requires-Dist:
|
|
7
|
+
Requires-Dist: litellm>=1.98
|
|
8
8
|
Requires-Dist: orjson>=3.11
|
|
9
9
|
Requires-Dist: pydantic>=2.13
|
|
10
10
|
Requires-Dist: rich>=15
|
|
@@ -84,11 +84,12 @@ Pass any of these as `--field value` to `create`, `add`, or `update`:
|
|
|
84
84
|
|
|
85
85
|
## Config
|
|
86
86
|
|
|
87
|
-
AI features require a
|
|
87
|
+
AI features require a model, set as a `provider/model` string (via [litellm](https://github.com/BerriAI/litellm)),
|
|
88
|
+
plus that provider's credentials set as environment variables (e.g. `GEMINI_API_KEY`, `OPENAI_API_KEY`).
|
|
89
|
+
`task setup` will walk you through picking a provider and model and tell you which env vars it needs.
|
|
88
90
|
|
|
89
91
|
```bash
|
|
90
|
-
task config set
|
|
91
|
-
task config set GEMINI_MODEL gemini-2.0-flash
|
|
92
|
+
task config set AI_MODEL gemini/gemini-2.0-flash
|
|
92
93
|
```
|
|
93
94
|
|
|
94
95
|
---
|
|
@@ -71,11 +71,12 @@ Pass any of these as `--field value` to `create`, `add`, or `update`:
|
|
|
71
71
|
|
|
72
72
|
## Config
|
|
73
73
|
|
|
74
|
-
AI features require a
|
|
74
|
+
AI features require a model, set as a `provider/model` string (via [litellm](https://github.com/BerriAI/litellm)),
|
|
75
|
+
plus that provider's credentials set as environment variables (e.g. `GEMINI_API_KEY`, `OPENAI_API_KEY`).
|
|
76
|
+
`task setup` will walk you through picking a provider and model and tell you which env vars it needs.
|
|
75
77
|
|
|
76
78
|
```bash
|
|
77
|
-
task config set
|
|
78
|
-
task config set GEMINI_MODEL gemini-2.0-flash
|
|
79
|
+
task config set AI_MODEL gemini/gemini-2.0-flash
|
|
79
80
|
```
|
|
80
81
|
|
|
81
82
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "taskai-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.6.2"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = [{name = "Alex Paskal", email = "alexcpaskal@gmail.com"}]
|
|
6
6
|
readme = "README.md"
|
|
@@ -9,7 +9,7 @@ dependencies = [
|
|
|
9
9
|
"pydantic>=2.13",
|
|
10
10
|
"rich>=15",
|
|
11
11
|
"orjson>=3.11",
|
|
12
|
-
"
|
|
12
|
+
"litellm>=1.98",
|
|
13
13
|
"fastapi>=0.141.1",
|
|
14
14
|
"uvicorn>=0.52.3",
|
|
15
15
|
]
|
|
@@ -179,8 +179,12 @@ class Controller:
|
|
|
179
179
|
Controller.create_comment(item.id, comment_content)
|
|
180
180
|
print(comment_content)
|
|
181
181
|
|
|
182
|
-
def ai_natural_language(prompt: str):
|
|
183
|
-
ai_natural_language_service(
|
|
182
|
+
def ai_natural_language(prompt: str, **kwargs):
|
|
183
|
+
ai_natural_language_service(
|
|
184
|
+
db, prompt,
|
|
185
|
+
context=kwargs.get("context"),
|
|
186
|
+
reasoning=kwargs.get("reasoning"),
|
|
187
|
+
)
|
|
184
188
|
|
|
185
189
|
def throw_error(error_description: str, *args, **kwargs):
|
|
186
190
|
print(f"[red]ERROR: {error_description}[/red]\nargs={args}\nkwargs={kwargs}")
|
|
@@ -272,7 +276,7 @@ class Controller:
|
|
|
272
276
|
db.commit()
|
|
273
277
|
|
|
274
278
|
def browser_service():
|
|
275
|
-
subprocess.run(['uvicorn','taskai.browser:app','--reload'])
|
|
279
|
+
subprocess.run([sys.executable, '-m', 'uvicorn', 'taskai.browser:app', '--reload'])
|
|
276
280
|
|
|
277
281
|
|
|
278
282
|
|
|
@@ -399,7 +403,7 @@ def execute_commands(*args, **kwargs) -> int:
|
|
|
399
403
|
case "ai":
|
|
400
404
|
match args[1]:
|
|
401
405
|
case "headstart": Controller.ai_headstart(*args[2:], **kwargs)
|
|
402
|
-
case _: Controller.ai_natural_language(" ".join(args[1:]))
|
|
406
|
+
case _: Controller.ai_natural_language(" ".join(args[1:]), **kwargs)
|
|
403
407
|
|
|
404
408
|
case "nuke":
|
|
405
409
|
db.remove()
|
|
@@ -47,6 +47,8 @@ Item fields (pass as '--field value' to create/add/update):
|
|
|
47
47
|
|
|
48
48
|
AI:
|
|
49
49
|
'task ai {prompt}' --> feed a prompt to an LLM, which converts it into a series of the commands above and runs them
|
|
50
|
+
--context {path[,path...]} fold the contents of one or more files into the prompt as extra context
|
|
51
|
+
--reasoning {level} how hard the model should think before answering: minimal|low|medium|high|disable|none
|
|
50
52
|
'task ai headstart {id}' --> ask an LLM to suggest the next concrete step for an item; the answer is saved as a comment
|
|
51
53
|
|
|
52
54
|
Config:
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reference data for AI setup: known providers, the env vars litellm reads for
|
|
3
|
+
each, and a best-effort (non-exhaustive) list of models per provider.
|
|
4
|
+
|
|
5
|
+
Model names go stale fast - this is a helpful menu, not a hard whitelist.
|
|
6
|
+
Provider keys match litellm's `provider/model` naming convention exactly
|
|
7
|
+
(see litellm.types.utils.LlmProviders) - keep in sync if that changes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# env vars litellm reads per provider
|
|
11
|
+
PROVIDER_ENV_VARS = {
|
|
12
|
+
"openai": ["OPENAI_API_KEY"],
|
|
13
|
+
"anthropic": ["ANTHROPIC_API_KEY"],
|
|
14
|
+
"gemini": ["GEMINI_API_KEY"],
|
|
15
|
+
"vertex_ai": ["VERTEXAI_PROJECT", "VERTEXAI_LOCATION", "GOOGLE_APPLICATION_CREDENTIALS"],
|
|
16
|
+
"groq": ["GROQ_API_KEY"],
|
|
17
|
+
"mistral": ["MISTRAL_API_KEY"],
|
|
18
|
+
"together_ai": ["TOGETHERAI_API_KEY"],
|
|
19
|
+
"xai": ["XAI_API_KEY"],
|
|
20
|
+
"deepseek": ["DEEPSEEK_API_KEY"],
|
|
21
|
+
"cohere": ["COHERE_API_KEY"],
|
|
22
|
+
"fireworks_ai": ["FIREWORKS_AI_API_KEY"],
|
|
23
|
+
"nebius": ["NEBIUS_API_KEY"],
|
|
24
|
+
"sambanova": ["SAMBANOVA_API_KEY"],
|
|
25
|
+
"huggingface": ["HUGGINGFACE_API_KEY"],
|
|
26
|
+
"azure": ["AZURE_API_KEY", "AZURE_API_VERSION", "AZURE_API_BASE"],
|
|
27
|
+
"bedrock": ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION_NAME"],
|
|
28
|
+
"watsonx": ["WATSONX_API_KEY", "WATSONX_PROJECT_ID", "WATSONX_URL"],
|
|
29
|
+
"cerebras": ["CEREBRAS_API_KEY"],
|
|
30
|
+
"inception": ["INCEPTION_API_KEY"],
|
|
31
|
+
"ollama": ["OLLAMA_API_BASE"],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# example/known models per provider - not exhaustive, just a menu to pick from
|
|
35
|
+
# or type past (e.g. a newer model the provider has added since this was written)
|
|
36
|
+
PROVIDER_MODELS = {
|
|
37
|
+
"openai": ["gpt-5", "gpt-4o", "gpt-4o-mini", "o3", "o3-mini"],
|
|
38
|
+
"anthropic": ["claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5-20251001", "claude-fable-5"],
|
|
39
|
+
"gemini": ["gemini-3.7-flash", "gemini-3.1-pro-preview", "gemini-3.6-flash", "gemini-3.5-flash-lite", "gemini-2.5-pro"],
|
|
40
|
+
"vertex_ai": ["gemini-3.7-flash", "gemini-3.1-pro-preview", "gemini-3.6-flash", "gemini-2.5-pro"],
|
|
41
|
+
"groq": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma2-9b-it"],
|
|
42
|
+
"mistral": ["mistral-large-latest", "mistral-small-latest", "codestral-latest", "pixtral-large-latest"],
|
|
43
|
+
"together_ai": ["meta-llama/Llama-3.3-70B-Instruct-Turbo", "Qwen/Qwen2.5-72B-Instruct-Turbo", "deepseek-ai/DeepSeek-V3"],
|
|
44
|
+
"xai": ["grok-4", "grok-4-fast", "grok-3", "grok-3-mini"],
|
|
45
|
+
"deepseek": ["deepseek-chat", "deepseek-reasoner"],
|
|
46
|
+
"cohere": ["command-a", "command-r-plus", "command-r"],
|
|
47
|
+
"fireworks_ai": ["accounts/fireworks/models/llama-v3p3-70b-instruct", "accounts/fireworks/models/deepseek-v3"],
|
|
48
|
+
"nebius": ["meta-llama/Llama-3.3-70B-Instruct", "Qwen/Qwen2.5-72B-Instruct"],
|
|
49
|
+
"sambanova": ["Meta-Llama-3.3-70B-Instruct", "Meta-Llama-3.1-8B-Instruct"],
|
|
50
|
+
"huggingface": ["meta-llama/Llama-3.3-70B-Instruct", "mistralai/Mixtral-8x7B-Instruct-v0.1"],
|
|
51
|
+
"azure": [], # model choice is your own Azure deployment name, not a fixed list
|
|
52
|
+
"bedrock": ["anthropic.claude-3-5-sonnet-20241022-v2:0", "meta.llama3-1-70b-instruct-v1:0", "amazon.titan-text-premier-v1:0"],
|
|
53
|
+
"watsonx": ["ibm/granite-3-8b-instruct", "meta-llama/llama-3-3-70b-instruct"],
|
|
54
|
+
"cerebras": ["llama-3.3-70b", "llama3.1-8b"],
|
|
55
|
+
"inception": ["mercury-coder-small"],
|
|
56
|
+
"ollama": [], # whatever model you've pulled locally, e.g. "llama3"
|
|
57
|
+
}
|
|
@@ -12,12 +12,10 @@ from taskai.models import TodoItem, Comment
|
|
|
12
12
|
from taskai.config import config
|
|
13
13
|
from taskai.help_menu import help_general
|
|
14
14
|
|
|
15
|
-
@config("
|
|
16
|
-
@config("GEMINI_MODEL", "model_name")
|
|
15
|
+
@config("AI_MODEL", "model_name")
|
|
17
16
|
def ai_headstart_service(
|
|
18
17
|
db: JsonDirectoryDatabase,
|
|
19
18
|
item_id: str,
|
|
20
|
-
api_key: str,
|
|
21
19
|
model_name: str
|
|
22
20
|
):
|
|
23
21
|
"""
|
|
@@ -32,10 +30,7 @@ def ai_headstart_service(
|
|
|
32
30
|
|
|
33
31
|
item:TodoItem = db.get_item(item_id)
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
from google import genai
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
import litellm
|
|
39
34
|
|
|
40
35
|
# contruct prompt
|
|
41
36
|
prompt = f"""
|
|
@@ -69,29 +64,35 @@ task name: {item.name}
|
|
|
69
64
|
comment: Comment = db.get_comment(comment_id)
|
|
70
65
|
prompt += f"\n\t- {comment.content}"
|
|
71
66
|
# query model
|
|
72
|
-
|
|
73
|
-
response = client.models.generate_content(
|
|
67
|
+
response = litellm.completion(
|
|
74
68
|
model=model_name,
|
|
75
|
-
|
|
69
|
+
messages=[{"role": "user", "content": prompt}],
|
|
76
70
|
)
|
|
77
71
|
|
|
78
|
-
return response.
|
|
72
|
+
return response.choices[0].message.content
|
|
73
|
+
|
|
74
|
+
def _read_context_files(context: str) -> str:
|
|
75
|
+
"""context is a comma-separated list of file paths"""
|
|
76
|
+
sections = []
|
|
77
|
+
for path in context.split(","):
|
|
78
|
+
path = path.strip()
|
|
79
|
+
with open(path) as f:
|
|
80
|
+
sections.append(f"--- {path} ---\n{f.read()}")
|
|
81
|
+
return "\n\n".join(sections)
|
|
79
82
|
|
|
80
|
-
@config("
|
|
81
|
-
@config("GEMINI_MODEL", "model_name")
|
|
83
|
+
@config("AI_MODEL", "model_name")
|
|
82
84
|
def ai_natural_language_service(
|
|
83
85
|
db: JsonDirectoryDatabase,
|
|
84
86
|
prompt: str,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
model_name: str,
|
|
88
|
+
context: str = None,
|
|
89
|
+
reasoning: str = None
|
|
87
90
|
):
|
|
88
91
|
"""
|
|
89
92
|
This service queries an LLM with a natural language
|
|
90
93
|
prompt from the user. The response is a series of terminal commands
|
|
91
94
|
called directly
|
|
92
95
|
"""
|
|
93
|
-
print(f"Ai prompt: {prompt}")
|
|
94
|
-
|
|
95
96
|
# recursively build user info
|
|
96
97
|
user_info = []
|
|
97
98
|
_visited_set = set()
|
|
@@ -112,6 +113,14 @@ def ai_natural_language_service(
|
|
|
112
113
|
|
|
113
114
|
user_info = "\n".join(user_info)
|
|
114
115
|
|
|
116
|
+
context_block = ""
|
|
117
|
+
if context:
|
|
118
|
+
context_block = f"""
|
|
119
|
+
Here is additional context the user has provided:
|
|
120
|
+
|
|
121
|
+
{_read_context_files(context)}
|
|
122
|
+
"""
|
|
123
|
+
|
|
115
124
|
# build ai prompt
|
|
116
125
|
ai_prompt = f"""
|
|
117
126
|
You are a todo agent. Your job is to convert a natural language description from a user into a set
|
|
@@ -122,7 +131,7 @@ of CLI operations using our app. Here are a comprehensive list of operations tha
|
|
|
122
131
|
Here are all of the user's existing item names, each prepended with their id
|
|
123
132
|
|
|
124
133
|
{user_info}
|
|
125
|
-
|
|
134
|
+
{context_block}
|
|
126
135
|
Here is the user's prompt:
|
|
127
136
|
|
|
128
137
|
{prompt}
|
|
@@ -178,34 +187,48 @@ markdown formatting. Just the raw json string.
|
|
|
178
187
|
|
|
179
188
|
"""
|
|
180
189
|
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
import litellm
|
|
191
|
+
|
|
192
|
+
completion_kwargs = {}
|
|
193
|
+
if reasoning:
|
|
194
|
+
completion_kwargs["reasoning_effort"] = reasoning
|
|
183
195
|
|
|
184
|
-
|
|
185
|
-
client = genai.Client(api_key=api_key)
|
|
186
|
-
response = client.models.generate_content(
|
|
196
|
+
response = litellm.completion(
|
|
187
197
|
model=model_name,
|
|
188
|
-
|
|
198
|
+
messages=[{"role": "user", "content": ai_prompt}],
|
|
199
|
+
**completion_kwargs,
|
|
189
200
|
)
|
|
201
|
+
response_text = response.choices[0].message.content
|
|
190
202
|
|
|
191
203
|
try:
|
|
192
|
-
response_json = json.loads(
|
|
204
|
+
response_json = json.loads(response_text)
|
|
193
205
|
except json.JSONDecodeError:
|
|
194
|
-
print(
|
|
206
|
+
print(response_text)
|
|
195
207
|
print("\n\ndecode error")
|
|
196
208
|
import sys
|
|
197
209
|
sys.exit(-1)
|
|
198
210
|
|
|
199
|
-
print(response_json)
|
|
200
|
-
|
|
201
211
|
from taskai.cli import execute_commands
|
|
202
212
|
|
|
203
213
|
for entry in response_json:
|
|
204
214
|
command = entry["command"]
|
|
205
215
|
cmd_args = entry.get("args", [])
|
|
216
|
+
raw_kwargs = entry.get("kwargs", {})
|
|
206
217
|
cmd_kwargs = {
|
|
207
218
|
k.lstrip("-"): v
|
|
208
|
-
for k, v in
|
|
219
|
+
for k, v in raw_kwargs.items()
|
|
209
220
|
}
|
|
221
|
+
print(f"ai run '{_format_command_str(command, cmd_args, raw_kwargs)}'")
|
|
210
222
|
execute_commands(command, *cmd_args, **cmd_kwargs)
|
|
211
223
|
|
|
224
|
+
def _format_command_str(command: str, args: list, kwargs: dict) -> str:
|
|
225
|
+
def quote(value):
|
|
226
|
+
value = str(value)
|
|
227
|
+
return f"'{value}'" if " " in value else value
|
|
228
|
+
|
|
229
|
+
parts = ["task", command] + [quote(a) for a in args]
|
|
230
|
+
for k, v in kwargs.items():
|
|
231
|
+
key = k if k.startswith("--") else f"--{k}"
|
|
232
|
+
parts.append(f"{key} {quote(v)}")
|
|
233
|
+
return " ".join(parts)
|
|
234
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This service will handle user setup and configuration
|
|
3
|
+
|
|
4
|
+
Basically, we're going to have a set of steps that we're going to iterate through
|
|
5
|
+
"""
|
|
6
|
+
from taskai.json_dir_database import JsonDirectoryDatabase
|
|
7
|
+
from taskai.llm_models import PROVIDER_ENV_VARS, PROVIDER_MODELS
|
|
8
|
+
|
|
9
|
+
from rich.prompt import Prompt
|
|
10
|
+
from rich import print
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def user_setup_service(
|
|
14
|
+
db: JsonDirectoryDatabase
|
|
15
|
+
):
|
|
16
|
+
|
|
17
|
+
config = db.get_config()
|
|
18
|
+
# setup ai model
|
|
19
|
+
print("Beginning setup")
|
|
20
|
+
if "AI_MODEL" not in config:
|
|
21
|
+
model = _get_ai_model()
|
|
22
|
+
if model:
|
|
23
|
+
db.update_config(AI_MODEL=model)
|
|
24
|
+
else:
|
|
25
|
+
print("AI model already specified")
|
|
26
|
+
print("Setup complete! Use 'task config set|get|list' to interact with your configuration options")
|
|
27
|
+
db.commit()
|
|
28
|
+
|
|
29
|
+
def _select_from_list(prompt_text: str, options: list[str]) -> str:
|
|
30
|
+
"""Show a numbered menu of options; accept either a number or free text."""
|
|
31
|
+
if options:
|
|
32
|
+
print("\n".join(f" {i}. {opt}" for i, opt in enumerate(options, 1)))
|
|
33
|
+
response = Prompt.ask(prompt_text)
|
|
34
|
+
if response.isdigit() and 1 <= int(response) <= len(options):
|
|
35
|
+
return options[int(response) - 1]
|
|
36
|
+
return response
|
|
37
|
+
|
|
38
|
+
def _get_ai_model() -> str|None:
|
|
39
|
+
provider = _select_from_list(
|
|
40
|
+
"\nSelect a provider (number or name)", list(PROVIDER_ENV_VARS)
|
|
41
|
+
)
|
|
42
|
+
if not provider:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
model_options = PROVIDER_MODELS.get(provider, [])
|
|
46
|
+
model = _select_from_list(
|
|
47
|
+
f"\nSelect a model for '{provider}' (number or name)", model_options
|
|
48
|
+
)
|
|
49
|
+
if not model:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
response = f"{provider}/{model}"
|
|
53
|
+
print(f"Storing: [green]{response}[/green]")
|
|
54
|
+
|
|
55
|
+
env_vars = PROVIDER_ENV_VARS.get(provider)
|
|
56
|
+
if env_vars is None:
|
|
57
|
+
print(
|
|
58
|
+
f"Unrecognized provider '{provider}' - check litellm's docs for the "
|
|
59
|
+
f"env var(s) it expects, and add it to PROVIDER_ENV_VARS in "
|
|
60
|
+
f"llm_models.py if you use it regularly."
|
|
61
|
+
)
|
|
62
|
+
elif env_vars:
|
|
63
|
+
print("Make sure these environment variables are set:\n" + "\n".join(f" {v}" for v in env_vars))
|
|
64
|
+
else:
|
|
65
|
+
print(f"'{provider}' runs locally - no credentials needed.")
|
|
66
|
+
return response
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
import os
|
|
71
|
+
db = JsonDirectoryDatabase(
|
|
72
|
+
".taskai/task_db", user=os.getenv("USER")
|
|
73
|
+
)
|
|
74
|
+
db.connect()
|
|
75
|
+
user_setup_service(db)
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This service will handle user setup and configuration
|
|
3
|
-
|
|
4
|
-
Basically, we're going to have a set of steps that we're going to iterate through
|
|
5
|
-
"""
|
|
6
|
-
from taskai.json_dir_database import JsonDirectoryDatabase
|
|
7
|
-
|
|
8
|
-
from rich.prompt import Prompt
|
|
9
|
-
from rich import print
|
|
10
|
-
import os
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def user_setup_service(
|
|
14
|
-
db: JsonDirectoryDatabase
|
|
15
|
-
):
|
|
16
|
-
|
|
17
|
-
config = db.get_config()
|
|
18
|
-
# setup gemini model
|
|
19
|
-
print("Beginning setup")
|
|
20
|
-
if "GEMINI_API_KEY" not in config:
|
|
21
|
-
api_key = _get_gemini_api_key()
|
|
22
|
-
if api_key:
|
|
23
|
-
db.update_config(GEMINI_API_KEY=api_key)
|
|
24
|
-
else:
|
|
25
|
-
print("gemini key already specified")
|
|
26
|
-
|
|
27
|
-
if "GEMINI_MODEL" not in config:
|
|
28
|
-
model = _select_gemini_model()
|
|
29
|
-
if model:
|
|
30
|
-
db.update_config(GEMINI_MODEL=model)
|
|
31
|
-
else:
|
|
32
|
-
print("gemini model already specified")
|
|
33
|
-
print("Setup complete! Use 'task config set|get|list' to interact with your configuration options")
|
|
34
|
-
db.commit()
|
|
35
|
-
|
|
36
|
-
def _get_gemini_api_key() -> str|None:
|
|
37
|
-
response = Prompt.ask("Please enter your Gemini API key (use '$--' to access env vars)")
|
|
38
|
-
|
|
39
|
-
if response.startswith("$"):
|
|
40
|
-
response = os.getenv(response[1:])
|
|
41
|
-
if not response:
|
|
42
|
-
return
|
|
43
|
-
print(f"Storing: [green]{response}[/green]")
|
|
44
|
-
return response
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _select_gemini_model():
|
|
48
|
-
response = Prompt.ask(
|
|
49
|
-
"Please select which Gemini model you would like:",
|
|
50
|
-
choices=[
|
|
51
|
-
"gemini-3.5-flash"
|
|
52
|
-
]
|
|
53
|
-
)
|
|
54
|
-
return response
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if __name__ == "__main__":
|
|
58
|
-
import os
|
|
59
|
-
db = JsonDirectoryDatabase(
|
|
60
|
-
".taskai/task_db", user=os.getenv("USER")
|
|
61
|
-
)
|
|
62
|
-
db.connect()
|
|
63
|
-
user_setup_service(db)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|