studytrails 0.2.2__tar.gz → 0.2.3__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.
- {studytrails-0.2.2 → studytrails-0.2.3}/PKG-INFO +5 -2
- {studytrails-0.2.2 → studytrails-0.2.3}/README.md +16 -6
- {studytrails-0.2.2 → studytrails-0.2.3}/docs/PYPI.md +4 -1
- {studytrails-0.2.2 → studytrails-0.2.3}/docs/RELEASING.md +3 -2
- {studytrails-0.2.2 → studytrails-0.2.3}/docs/SETUP.md +14 -2
- {studytrails-0.2.2 → studytrails-0.2.3}/docs/USER_GUIDE.md +11 -1
- {studytrails-0.2.2 → studytrails-0.2.3}/pyproject.toml +1 -1
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/agent.py +13 -10
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/cli.py +18 -5
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/onboarding.py +15 -8
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/preferences.py +5 -1
- studytrails-0.2.3/study_agent/providers.py +10 -0
- studytrails-0.2.3/study_agent/ui.py +21 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/tests/test_agent.py +28 -10
- {studytrails-0.2.2 → studytrails-0.2.3}/tests/test_onboarding.py +30 -3
- studytrails-0.2.3/tests/test_ui.py +32 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/tests/wheel_smoke.py +3 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/uv.lock +1 -1
- {studytrails-0.2.2 → studytrails-0.2.3}/.gitignore +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/.python-version +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/CONTRIBUTING.md +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/LICENSE +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/assets/banner.svg +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/assets/logo.svg +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/__init__.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/__main__.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/config.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/default_notes/java/basics.md +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/default_notes/python/collections.md +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/default_notes/python/functions.md +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/default_notes/python/loops.md +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/demo.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/models.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/notes.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/storage.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/tools.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/tests/conftest.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/tests/test_cli.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/tests/test_config.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/tests/test_storage.py +0 -0
- {studytrails-0.2.2 → studytrails-0.2.3}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: studytrails
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A personal multi-subject AI study coach with tools, local progress, and an offline demo.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -33,7 +33,9 @@ may be another provider, such as Groq. Users may configure other model families.
|
|
|
33
33
|
|
|
34
34
|
- Interactive provider, model, and hidden API-key setup.
|
|
35
35
|
- OpenAI-compatible Chat Completions endpoints with tool-calling models.
|
|
36
|
-
- OpenAI and
|
|
36
|
+
- OpenAI, Groq, and DeepSeek presets; custom compatible endpoints supported.
|
|
37
|
+
- Formatted coach answers with readable headings, lists, and code blocks.
|
|
38
|
+
- Explicit y/n prompts with input validation and a safe default of no.
|
|
37
39
|
- Multi-subject coaching and subject-specific note retrieval.
|
|
38
40
|
- Paste notes or import UTF-8 `.md` and `.txt` files up to 200 KB.
|
|
39
41
|
- Multiple-choice quizzes, explanations, and local SQLite progress.
|
|
@@ -52,6 +54,7 @@ studytrails scores
|
|
|
52
54
|
studytrails pending
|
|
53
55
|
studytrails demo
|
|
54
56
|
studytrails doctor
|
|
57
|
+
studytrails --version
|
|
55
58
|
```
|
|
56
59
|
|
|
57
60
|
Run `studytrails doctor` to see the personal storage location. Existing source
|
|
@@ -53,7 +53,7 @@ the command so it can run from any directory without activating a virtual enviro
|
|
|
53
53
|
Welcome to StudyTrail!
|
|
54
54
|
Learn from your notes. Practise with quizzes. Track your progress.
|
|
55
55
|
|
|
56
|
-
Configure AI now? [y/
|
|
56
|
+
Configure AI now? [y/n] (Enter = no):
|
|
57
57
|
|
|
58
58
|
1. Start learning 2. Manage notes 3. View progress
|
|
59
59
|
4. Configure AI 5. Pending quizzes 6. Offline demo 0. Exit
|
|
@@ -61,7 +61,7 @@ Configure AI now? [y/N]:
|
|
|
61
61
|
|
|
62
62
|
Choose **Configure AI** to supply:
|
|
63
63
|
|
|
64
|
-
1. An OpenAI-compatible custom endpoint, or the OpenAI/Groq
|
|
64
|
+
1. An OpenAI-compatible custom endpoint, or the OpenAI/Groq/DeepSeek preset.
|
|
65
65
|
2. The exact model ID from your provider. It must support Chat Completions tool calling.
|
|
66
66
|
3. Your API key, entered with hidden input.
|
|
67
67
|
|
|
@@ -75,10 +75,19 @@ and may consume paid quota. Skipping it saves unverified settings. Provider
|
|
|
75
75
|
compatibility is covered with mocked HTTP tests; no live cross-provider certification
|
|
76
76
|
is claimed.
|
|
77
77
|
|
|
78
|
-
Keys are saved using a supported operating-system credential store. They are never
|
|
78
|
+
Keys are saved using a supported operating-system credential store. They are never
|
|
79
79
|
written to `config.json`. If your machine has no usable credential store, supply
|
|
80
80
|
`STUDYTRAIL_API_KEY` through your environment and rerun setup. There is no plaintext
|
|
81
|
-
fallback. Run `studytrails config` to change providers or models later.
|
|
81
|
+
fallback. Run `studytrails config` to change providers or models later.
|
|
82
|
+
|
|
83
|
+
For DeepSeek, choose **4** and press Enter to use `deepseek-flash`, or enter a
|
|
84
|
+
current model ID from your DeepSeek account. Paste your DeepSeek API key at the
|
|
85
|
+
hidden prompt. StudyTrail uses non-thinking mode with DeepSeek. See the
|
|
86
|
+
[DeepSeek API documentation](https://api-docs.deepseek.com/) for models and pricing.
|
|
87
|
+
|
|
88
|
+
Yes/no prompts display `[y/n]`: enter `y` for yes, `n` for no, or press Enter for no.
|
|
89
|
+
Coach replies render Markdown headings, lists, and code blocks in both chat and
|
|
90
|
+
single-question mode.
|
|
82
91
|
|
|
83
92
|
**Free app does not mean free API.** Offline features need no API calls. Live
|
|
84
93
|
coaching uses your provider's free allowance or paid plan. StudyTrail cannot inspect
|
|
@@ -132,8 +141,9 @@ studytrails scores
|
|
|
132
141
|
studytrails pending
|
|
133
142
|
studytrails quiz QUIZ_ID
|
|
134
143
|
studytrails demo
|
|
135
|
-
studytrails doctor
|
|
136
|
-
studytrails --
|
|
144
|
+
studytrails doctor
|
|
145
|
+
studytrails --version
|
|
146
|
+
studytrails --help
|
|
137
147
|
```
|
|
138
148
|
|
|
139
149
|
Add `--demo` to `scores`, `pending`, or `quiz QUIZ_ID` to use demo progress.
|
|
@@ -18,7 +18,9 @@ may be another provider, such as Groq. Users may configure other model families.
|
|
|
18
18
|
|
|
19
19
|
- Interactive provider, model, and hidden API-key setup.
|
|
20
20
|
- OpenAI-compatible Chat Completions endpoints with tool-calling models.
|
|
21
|
-
- OpenAI and
|
|
21
|
+
- OpenAI, Groq, and DeepSeek presets; custom compatible endpoints supported.
|
|
22
|
+
- Formatted coach answers with readable headings, lists, and code blocks.
|
|
23
|
+
- Explicit y/n prompts with input validation and a safe default of no.
|
|
22
24
|
- Multi-subject coaching and subject-specific note retrieval.
|
|
23
25
|
- Paste notes or import UTF-8 `.md` and `.txt` files up to 200 KB.
|
|
24
26
|
- Multiple-choice quizzes, explanations, and local SQLite progress.
|
|
@@ -37,6 +39,7 @@ studytrails scores
|
|
|
37
39
|
studytrails pending
|
|
38
40
|
studytrails demo
|
|
39
41
|
studytrails doctor
|
|
42
|
+
studytrails --version
|
|
40
43
|
```
|
|
41
44
|
|
|
42
45
|
Run `studytrails doctor` to see the personal storage location. Existing source
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Releasing StudyTrail
|
|
2
2
|
|
|
3
3
|
The distribution name is `studytrails`; the import remains `study_agent`.
|
|
4
|
-
|
|
4
|
+
Version 0.2.2 is published on PyPI. Version 0.2.3 is the next local candidate,
|
|
5
|
+
adding a DeepSeek preset, formatted coach replies, visible y/n prompts, and `--version`.
|
|
5
6
|
Build, tests, and CI do not publish a release.
|
|
6
7
|
|
|
7
8
|
## Before each release
|
|
@@ -52,7 +53,7 @@ blindly combining indexes for production installs.
|
|
|
52
53
|
For a manual release, first review the files listed under `dist/`, then publish:
|
|
53
54
|
|
|
54
55
|
```powershell
|
|
55
|
-
uv publish
|
|
56
|
+
uv publish dist/studytrails-0.2.3-py3-none-any.whl dist/studytrails-0.2.3.tar.gz
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
Supply authentication securely as documented by uv. This command publishes publicly;
|
|
@@ -93,12 +93,21 @@ You will need:
|
|
|
93
93
|
3. A model ID that your provider account can access and that supports
|
|
94
94
|
OpenAI-compatible Chat Completions with tool calling.
|
|
95
95
|
|
|
96
|
-
StudyTrail offers
|
|
97
|
-
provider address;
|
|
96
|
+
StudyTrail offers presets for OpenAI, Groq, and DeepSeek. These fill in the
|
|
97
|
+
provider address; they do not provide a key or guarantee every model will work.
|
|
98
98
|
For another compatible service, choose the custom endpoint option and enter its
|
|
99
99
|
API base URL and model ID. StudyTrail cannot use provider APIs with an incompatible
|
|
100
100
|
request format in this release.
|
|
101
101
|
|
|
102
|
+
For DeepSeek: choose provider **4**, press Enter at the model prompt to accept
|
|
103
|
+
`deepseek-flash`, and paste your own key from
|
|
104
|
+
[DeepSeek API keys](https://platform.deepseek.com/api_keys). You may enter another
|
|
105
|
+
current tool-calling model ID from your account. StudyTrail uses non-thinking mode
|
|
106
|
+
for DeepSeek. Its API may require paid credit; check your provider account.
|
|
107
|
+
|
|
108
|
+
At every `[y/n]` prompt, type `y` for yes or `n` for no. Pressing Enter selects no.
|
|
109
|
+
You can also type `yes` or `no`; other answers prompt you to try again.
|
|
110
|
+
|
|
102
111
|
The key prompt is hidden while you type. Paste the key only into that prompt. Do
|
|
103
112
|
not paste it into the provider number or model prompt, a command line, this guide,
|
|
104
113
|
or a public issue. Setup can make an optional small connection test; the request
|
|
@@ -154,6 +163,9 @@ The macOS/Linux activation command is `source .venv/bin/activate`. If you used
|
|
|
154
163
|
|
|
155
164
|
## 6. Find your notes and progress
|
|
156
165
|
|
|
166
|
+
Check the installed release with `studytrails --version`. This command is available
|
|
167
|
+
from version 0.2.3 onward and does not require an API key.
|
|
168
|
+
|
|
157
169
|
Run this command from an activated environment or uv tool installation:
|
|
158
170
|
|
|
159
171
|
```text
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# StudyTrail user guide
|
|
2
2
|
|
|
3
|
-
## Setup and storage
|
|
3
|
+
## Setup and storage
|
|
4
|
+
|
|
5
|
+
Provider 4 is DeepSeek (`https://api.deepseek.com`). Its suggested model is
|
|
6
|
+
`deepseek-flash`; press Enter to accept it or enter a current model ID from your
|
|
7
|
+
account. Both study requests and the optional connection test use non-thinking
|
|
8
|
+
mode with DeepSeek. See [DeepSeek's tool-calling guide](https://api-docs.deepseek.com/guides/tool_calls/).
|
|
9
|
+
An API key and any required account credit must be supplied by the user.
|
|
10
|
+
|
|
11
|
+
All yes/no prompts display `[y/n] (Enter = no)`. They accept y/yes and n/no,
|
|
12
|
+
case-insensitively, and reprompt for other input. Chat and `ask` render Markdown
|
|
13
|
+
headings, lists, and code examples, retaining note citations.
|
|
4
14
|
|
|
5
15
|
Run `studytrails` to open the menu, or `studytrails config` to configure AI directly.
|
|
6
16
|
Choose a provider preset or enter an OpenAI-compatible Chat Completions base URL.
|
|
@@ -7,16 +7,20 @@ from openai import OpenAI
|
|
|
7
7
|
from pydantic import ValidationError
|
|
8
8
|
|
|
9
9
|
from .config import Settings
|
|
10
|
+
from .providers import chat_options
|
|
10
11
|
from .tools import StudyTools, tool_definitions
|
|
11
12
|
|
|
12
|
-
INSTRUCTIONS = """You are a practical personal multi-subject study coach for a beginner.
|
|
13
|
-
StudyTrail was created by Yash Patil. If asked who created or made StudyTrail, say
|
|
14
|
-
Yash Patil created the StudyTrail application and is learning Python and AI. The AI
|
|
15
|
-
model is supplied by the provider configured by this user; credit OpenAI for an
|
|
16
|
-
OpenAI model such as GPT-OSS, and name the configured provider as its API host when
|
|
17
|
-
appropriate. Do not claim OpenAI created StudyTrail or guess other personal details
|
|
18
|
-
about Yash Patil.
|
|
19
|
-
Use plain English. Help the learner understand, practise, and improve.
|
|
13
|
+
INSTRUCTIONS = """You are a practical personal multi-subject study coach for a beginner.
|
|
14
|
+
StudyTrail was created by Yash Patil. If asked who created or made StudyTrail, say
|
|
15
|
+
Yash Patil created the StudyTrail application and is learning Python and AI. The AI
|
|
16
|
+
model is supplied by the provider configured by this user; credit OpenAI for an
|
|
17
|
+
OpenAI model such as GPT-OSS, and name the configured provider as its API host when
|
|
18
|
+
appropriate. Do not claim OpenAI created StudyTrail or guess other personal details
|
|
19
|
+
about Yash Patil.
|
|
20
|
+
Use plain English. Help the learner understand, practise, and improve.
|
|
21
|
+
Format replies with short paragraphs and brief Markdown headings when helpful.
|
|
22
|
+
Use bullets for steps and fenced code blocks with a language name for code examples.
|
|
23
|
+
Avoid wide tables, excessive headings, and long walls of text. Keep note citations intact.
|
|
20
24
|
For practice recommendations, inspect get_scores first. Search local notes for the
|
|
21
25
|
chosen topic before teaching or creating a quiz. Use recent attempts as well as totals.
|
|
22
26
|
If no scores exist, say so and start at beginner level. Respect the learner's chosen topic.
|
|
@@ -84,8 +88,7 @@ class StudyAgent:
|
|
|
84
88
|
model=self.settings.model,
|
|
85
89
|
messages=[{"role": "system", "content": self.instructions}, *messages],
|
|
86
90
|
tools=tool_definitions(),
|
|
87
|
-
|
|
88
|
-
max_completion_tokens=2400,
|
|
91
|
+
**chat_options(self.settings.base_url),
|
|
89
92
|
)
|
|
90
93
|
choice = response.choices[0]
|
|
91
94
|
if choice.finish_reason not in {"stop", "tool_calls"}:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import sqlite3
|
|
5
|
+
from importlib.metadata import version
|
|
5
6
|
|
|
6
7
|
from openai import APIConnectionError, APIError, APIStatusError, AuthenticationError, RateLimitError
|
|
7
8
|
from rich.console import Console
|
|
@@ -14,12 +15,18 @@ from .onboarding import configure, ensure_notes, manage_notes, subject_slug
|
|
|
14
15
|
from .preferences import data_root
|
|
15
16
|
from .storage import Store
|
|
16
17
|
from .tools import StudyTools
|
|
18
|
+
from .ui import confirm, show_answer
|
|
17
19
|
|
|
18
20
|
console = Console(markup=False, highlight=False)
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def trace(name: str) -> None:
|
|
22
|
-
|
|
24
|
+
labels = {
|
|
25
|
+
"get_scores": "Checking your progress...",
|
|
26
|
+
"search_notes": "Searching your notes...",
|
|
27
|
+
"create_quiz": "Preparing your quiz...",
|
|
28
|
+
}
|
|
29
|
+
console.print(f" {labels.get(name, name)}", style="dim")
|
|
23
30
|
|
|
24
31
|
|
|
25
32
|
def show_scores(store: Store) -> None:
|
|
@@ -78,7 +85,7 @@ def offer_quizzes(tools: StudyTools) -> None:
|
|
|
78
85
|
if tools.store.is_completed(quiz_id):
|
|
79
86
|
continue
|
|
80
87
|
console.print(f"Quiz ready: {quiz_id}")
|
|
81
|
-
if console
|
|
88
|
+
if confirm(console, "Take it now?"):
|
|
82
89
|
take_quiz(tools.store, quiz_id)
|
|
83
90
|
else:
|
|
84
91
|
console.print(f"Saved for later: studytrails quiz {quiz_id}")
|
|
@@ -134,7 +141,7 @@ def chat(settings: Settings, tools: StudyTools) -> None:
|
|
|
134
141
|
try:
|
|
135
142
|
with console.status("Coach is working..."):
|
|
136
143
|
answer = agent.run(goal)
|
|
137
|
-
console
|
|
144
|
+
show_answer(console, answer)
|
|
138
145
|
offer_quizzes(tools)
|
|
139
146
|
except (ValueError, RuntimeError, APIError) as exc:
|
|
140
147
|
console.print(f"Error: {describe_error(exc)}", style="red")
|
|
@@ -157,6 +164,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
157
164
|
parser = argparse.ArgumentParser(
|
|
158
165
|
description="StudyTrail: your personal multi-subject AI study coach."
|
|
159
166
|
)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--version",
|
|
169
|
+
action="version",
|
|
170
|
+
version=f"studytrails {version('studytrails')}",
|
|
171
|
+
help="Show the installed version and exit.",
|
|
172
|
+
)
|
|
160
173
|
commands = parser.add_subparsers(dest="command")
|
|
161
174
|
commands.add_parser("demo", help="Try a fixed offline quiz; no API key or network required.")
|
|
162
175
|
live = commands.add_parser("chat", help="Start the live tool-using AI coach.")
|
|
@@ -188,7 +201,7 @@ def menu(settings: Settings) -> None:
|
|
|
188
201
|
ensure_notes(settings.root)
|
|
189
202
|
if not settings.api_key:
|
|
190
203
|
console.print("No API key configured. Offline notes, scores, and demo remain available.")
|
|
191
|
-
if console
|
|
204
|
+
if confirm(console, "Configure AI now?"):
|
|
192
205
|
settings = configure(console, settings.root)
|
|
193
206
|
while True:
|
|
194
207
|
console.print("\n1. Start learning 2. Manage notes 3. View progress")
|
|
@@ -281,7 +294,7 @@ def main() -> int:
|
|
|
281
294
|
chat(settings, tools)
|
|
282
295
|
case "ask":
|
|
283
296
|
answer = StudyAgent(settings, tools, trace).run(args.goal)
|
|
284
|
-
console
|
|
297
|
+
show_answer(console, answer)
|
|
285
298
|
offer_quizzes(tools)
|
|
286
299
|
case "scores":
|
|
287
300
|
show_scores(store)
|
|
@@ -9,18 +9,21 @@ from pathlib import Path
|
|
|
9
9
|
from uuid import uuid4
|
|
10
10
|
|
|
11
11
|
from openai import OpenAI
|
|
12
|
+
from rich.text import Text
|
|
12
13
|
|
|
13
14
|
from .config import Settings
|
|
14
15
|
from .preferences import PRESETS, save_settings, validate_base_url
|
|
16
|
+
from .providers import chat_options
|
|
17
|
+
from .ui import confirm
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
def configure(console, root: Path) -> Settings:
|
|
18
21
|
console.print("\nConfigure StudyTrail | bring your own API")
|
|
19
|
-
console.print("1. OpenAI-compatible custom endpoint
|
|
20
|
-
selection = console.input("Provider [1]: ").strip() or "1"
|
|
21
|
-
providers = {"1": "custom", "2": "openai", "3": "groq"}
|
|
22
|
+
console.print("1. OpenAI-compatible custom endpoint\n2. OpenAI\n3. Groq\n4. DeepSeek")
|
|
23
|
+
selection = console.input(Text("Provider [1]: ")).strip() or "1"
|
|
24
|
+
providers = {"1": "custom", "2": "openai", "3": "groq", "4": "deepseek"}
|
|
22
25
|
if selection not in providers:
|
|
23
|
-
raise ValueError("Choose 1, 2, or
|
|
26
|
+
raise ValueError("Choose 1, 2, 3, or 4.")
|
|
24
27
|
provider = providers[selection]
|
|
25
28
|
base_url = PRESETS.get(provider)
|
|
26
29
|
if base_url is None:
|
|
@@ -28,7 +31,12 @@ def configure(console, root: Path) -> Settings:
|
|
|
28
31
|
base_url = validate_base_url(base_url)
|
|
29
32
|
console.print(f"Your key and study requests will be sent to: {base_url}")
|
|
30
33
|
console.print("Use a model supporting Chat Completions function/tool calling.")
|
|
31
|
-
|
|
34
|
+
if provider == "deepseek":
|
|
35
|
+
console.print("DeepSeek API key: https://platform.deepseek.com/api_keys")
|
|
36
|
+
console.print("StudyTrail uses DeepSeek in non-thinking mode. API charges may apply.")
|
|
37
|
+
model = console.input(Text("Model ID [deepseek-flash]: ")).strip() or "deepseek-flash"
|
|
38
|
+
else:
|
|
39
|
+
model = console.input("Model ID from your provider: ").strip()
|
|
32
40
|
env_key = os.getenv("STUDYTRAIL_API_KEY", "").strip()
|
|
33
41
|
key = env_key
|
|
34
42
|
if not key:
|
|
@@ -44,7 +52,7 @@ def configure(console, root: Path) -> Settings:
|
|
|
44
52
|
settings = Settings(root=root, provider=provider, base_url=base_url, model=model, api_key=key)
|
|
45
53
|
settings.require_api()
|
|
46
54
|
console.print("An optional connection test uses API quota and may incur provider charges.")
|
|
47
|
-
if console
|
|
55
|
+
if confirm(console, "Test tool calling now?"):
|
|
48
56
|
probe_provider(settings)
|
|
49
57
|
console.print("Tool-calling test passed.")
|
|
50
58
|
else:
|
|
@@ -76,8 +84,7 @@ def probe_provider(settings: Settings) -> None:
|
|
|
76
84
|
}
|
|
77
85
|
],
|
|
78
86
|
tool_choice={"type": "function", "function": {"name": "connection_check"}},
|
|
79
|
-
|
|
80
|
-
max_completion_tokens=2400,
|
|
87
|
+
**chat_options(settings.base_url),
|
|
81
88
|
)
|
|
82
89
|
if not result.choices or not any(
|
|
83
90
|
call.function.name == "connection_check"
|
|
@@ -10,7 +10,11 @@ from keyring.errors import KeyringError
|
|
|
10
10
|
from platformdirs import user_data_path
|
|
11
11
|
|
|
12
12
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
13
|
-
PRESETS = {
|
|
13
|
+
PRESETS = {
|
|
14
|
+
"openai": "https://api.openai.com/v1",
|
|
15
|
+
"groq": "https://api.groq.com/openai/v1",
|
|
16
|
+
"deepseek": "https://api.deepseek.com",
|
|
17
|
+
}
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
def data_root() -> Path:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Request options for the supported Chat Completions endpoints."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import urlsplit
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def chat_options(base_url: str) -> dict:
|
|
7
|
+
if urlsplit(base_url).hostname == "api.deepseek.com":
|
|
8
|
+
# DeepSeek uses max_tokens; forced tool choice needs non-thinking mode.
|
|
9
|
+
return {"max_tokens": 2400, "extra_body": {"thinking": {"type": "disabled"}}}
|
|
10
|
+
return {"max_completion_tokens": 2400, "parallel_tool_calls": False}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared terminal prompts and readable coach output."""
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.markdown import Markdown
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def confirm(console: Console, question: str) -> bool:
|
|
9
|
+
while True:
|
|
10
|
+
answer = console.input(Text(f"{question} [y/n] (Enter = no): ")).strip().lower()
|
|
11
|
+
if answer in {"y", "yes"}:
|
|
12
|
+
return True
|
|
13
|
+
if answer in {"", "n", "no"}:
|
|
14
|
+
return False
|
|
15
|
+
console.print("Please enter y for yes or n for no.")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def show_answer(console: Console, answer: str) -> None:
|
|
19
|
+
console.print("\nCoach", style="bold cyan")
|
|
20
|
+
console.print(Markdown(answer, hyperlinks=False), width=min(console.width, 100))
|
|
21
|
+
console.print()
|
|
@@ -39,7 +39,7 @@ class ScriptedClient:
|
|
|
39
39
|
return next(self.queue)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def test_agent_uses_observations_and_creates_real_quiz(tools, quiz):
|
|
42
|
+
def test_agent_uses_observations_and_creates_real_quiz(tools, quiz):
|
|
43
43
|
client = ScriptedClient(
|
|
44
44
|
[
|
|
45
45
|
response(call("get_scores")),
|
|
@@ -56,10 +56,10 @@ def test_agent_uses_observations_and_creates_real_quiz(tools, quiz):
|
|
|
56
56
|
assert json.loads(observations[0]["content"])["completed_quizzes"] == 0
|
|
57
57
|
assert "loops.md" in observations[1]["content"]
|
|
58
58
|
assert len(tools.store.pending_quizzes()) == 1
|
|
59
|
-
assert tools.store.get_scores()["completed_quizzes"] == 0
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def test_bad_tool_arguments_return_feedback_and_allow_recovery(tools):
|
|
59
|
+
assert tools.store.get_scores()["completed_quizzes"] == 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_bad_tool_arguments_return_feedback_and_allow_recovery(tools):
|
|
63
63
|
client = ScriptedClient(
|
|
64
64
|
[
|
|
65
65
|
response(call("search_notes", "not-json")),
|
|
@@ -112,10 +112,17 @@ def test_chat_keeps_only_three_complete_turns(tools):
|
|
|
112
112
|
|
|
113
113
|
@pytest.mark.parametrize(
|
|
114
114
|
"base_url",
|
|
115
|
-
[
|
|
115
|
+
[
|
|
116
|
+
"https://api.groq.com/openai/v1",
|
|
117
|
+
"https://api.openai.com/v1",
|
|
118
|
+
"https://custom.example/v1",
|
|
119
|
+
"https://api.deepseek.com",
|
|
120
|
+
],
|
|
116
121
|
)
|
|
117
122
|
def test_actual_sdk_sends_tool_loop_to_configured_endpoint(tools, monkeypatch, base_url):
|
|
118
123
|
requests = []
|
|
124
|
+
deepseek = base_url == "https://api.deepseek.com"
|
|
125
|
+
model = "deepseek-flash" if deepseek else "openai/gpt-oss-120b"
|
|
119
126
|
|
|
120
127
|
def handler(request):
|
|
121
128
|
assert str(request.url) == base_url + "/chat/completions"
|
|
@@ -132,7 +139,7 @@ def test_actual_sdk_sends_tool_loop_to_configured_endpoint(tools, monkeypatch, b
|
|
|
132
139
|
"id": f"chatcmpl_{len(requests)}",
|
|
133
140
|
"object": "chat.completion",
|
|
134
141
|
"created": 0,
|
|
135
|
-
"model":
|
|
142
|
+
"model": model,
|
|
136
143
|
"choices": [
|
|
137
144
|
{
|
|
138
145
|
"index": 0,
|
|
@@ -147,10 +154,21 @@ def test_actual_sdk_sends_tool_loop_to_configured_endpoint(tools, monkeypatch, b
|
|
|
147
154
|
monkeypatch.setattr(
|
|
148
155
|
"study_agent.agent.OpenAI", lambda **kwargs: OpenAI(http_client=http, **kwargs)
|
|
149
156
|
)
|
|
150
|
-
agent = StudyAgent(
|
|
157
|
+
agent = StudyAgent(
|
|
158
|
+
Settings(api_key="test-not-a-real-key", base_url=base_url, model=model), tools
|
|
159
|
+
)
|
|
151
160
|
assert agent.run("Suggest practice") == "Start with loops."
|
|
152
|
-
assert requests[0]["model"] ==
|
|
153
|
-
|
|
161
|
+
assert requests[0]["model"] == model
|
|
162
|
+
for request in requests:
|
|
163
|
+
if deepseek:
|
|
164
|
+
assert request["max_tokens"] == 2400
|
|
165
|
+
assert request["thinking"] == {"type": "disabled"}
|
|
166
|
+
assert "max_completion_tokens" not in request
|
|
167
|
+
assert "parallel_tool_calls" not in request
|
|
168
|
+
else:
|
|
169
|
+
assert request["parallel_tool_calls"] is False
|
|
170
|
+
assert request["max_completion_tokens"] == 2400
|
|
171
|
+
assert "thinking" not in request
|
|
154
172
|
assert "input" not in requests[0]
|
|
155
173
|
assert "store" not in requests[0]
|
|
156
174
|
assert requests[0]["tools"][0]["function"]["name"] == "get_scores"
|
|
@@ -131,10 +131,16 @@ def test_failed_probe_does_not_replace_config(tmp_path, monkeypatch, vault):
|
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
@pytest.mark.parametrize("supported", [True, False])
|
|
134
|
-
|
|
134
|
+
@pytest.mark.parametrize("base_url", ["https://provider.example/v1", "https://api.deepseek.com"])
|
|
135
|
+
def test_connection_probe_checks_actual_tool_response(tmp_path, monkeypatch, supported, base_url):
|
|
135
136
|
def handler(request):
|
|
136
|
-
assert str(request.url) == "
|
|
137
|
+
assert str(request.url) == base_url + "/chat/completions"
|
|
137
138
|
payload = json.loads(request.content)
|
|
139
|
+
if base_url == "https://api.deepseek.com":
|
|
140
|
+
assert payload["thinking"] == {"type": "disabled"}
|
|
141
|
+
assert payload["max_tokens"] == 2400
|
|
142
|
+
assert "max_completion_tokens" not in payload
|
|
143
|
+
assert "parallel_tool_calls" not in payload
|
|
138
144
|
assert payload["tool_choice"]["function"]["name"] == "connection_check"
|
|
139
145
|
assert payload["model"] == "tool-model"
|
|
140
146
|
message = {"role": "assistant", "content": "Hello"}
|
|
@@ -169,7 +175,7 @@ def test_connection_probe_checks_actual_tool_response(tmp_path, monkeypatch, sup
|
|
|
169
175
|
)
|
|
170
176
|
settings = Settings(
|
|
171
177
|
root=tmp_path,
|
|
172
|
-
base_url=
|
|
178
|
+
base_url=base_url,
|
|
173
179
|
model="tool-model",
|
|
174
180
|
api_key="fake-key",
|
|
175
181
|
)
|
|
@@ -269,3 +275,24 @@ def test_import_notes_and_reject_pdf(tmp_path, monkeypatch):
|
|
|
269
275
|
answers = iter(["2", "java", "book", "book.pdf"])
|
|
270
276
|
with pytest.raises(ValueError, match="UTF-8"):
|
|
271
277
|
onboarding.manage_notes(console, tmp_path)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_deepseek_setup_default_model_and_literal_prompts(tmp_path, monkeypatch, vault):
|
|
281
|
+
import sys
|
|
282
|
+
|
|
283
|
+
output = StringIO()
|
|
284
|
+
console = Console(file=output, width=100)
|
|
285
|
+
monkeypatch.setattr(sys, "stdin", StringIO("4\n\nmaybe\nn\n"))
|
|
286
|
+
monkeypatch.setattr(onboarding, "getpass", lambda *args: "deepseek-test-secret")
|
|
287
|
+
settings = onboarding.configure(console, tmp_path)
|
|
288
|
+
loaded = Settings.load()
|
|
289
|
+
assert loaded.provider == settings.provider == "deepseek"
|
|
290
|
+
assert loaded.base_url == "https://api.deepseek.com"
|
|
291
|
+
assert loaded.model == "deepseek-flash"
|
|
292
|
+
assert loaded.api_key == "deepseek-test-secret"
|
|
293
|
+
assert "deepseek-test-secret" not in output.getvalue()
|
|
294
|
+
assert "deepseek-test-secret" not in (tmp_path / "config.json").read_text()
|
|
295
|
+
assert "Provider [1]:" in output.getvalue()
|
|
296
|
+
assert "Model ID [deepseek-flash]:" in output.getvalue()
|
|
297
|
+
assert "[y/n] (Enter = no):" in output.getvalue()
|
|
298
|
+
assert "Please enter y for yes or n for no." in output.getvalue()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from io import StringIO
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from study_agent.ui import confirm, show_answer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parametrize(
|
|
11
|
+
("answer", "expected"), [("y", True), ("YES", True), ("n", False), ("No", False), ("", False)]
|
|
12
|
+
)
|
|
13
|
+
def test_confirmation_keeps_choices_visible_and_accepts_answers(monkeypatch, answer, expected):
|
|
14
|
+
output = StringIO()
|
|
15
|
+
monkeypatch.setattr(sys, "stdin", StringIO(answer + "\n"))
|
|
16
|
+
assert confirm(Console(file=output), "Continue?") is expected
|
|
17
|
+
assert "Continue? [y/n] (Enter = no):" in output.getvalue()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_markdown_reply_formats_text_but_preserves_code_and_citations():
|
|
21
|
+
output = StringIO()
|
|
22
|
+
show_answer(
|
|
23
|
+
Console(file=output, width=70, force_terminal=False),
|
|
24
|
+
"## Loops\n\n**Repeat** a task. [loops.md:3]\n\n"
|
|
25
|
+
"```python\nfor i in range(3):\n print(i)\n```",
|
|
26
|
+
)
|
|
27
|
+
text = output.getvalue()
|
|
28
|
+
assert "Coach" in text and "Loops" in text
|
|
29
|
+
assert "**Repeat**" not in text and "Repeat" in text
|
|
30
|
+
assert "```" not in text
|
|
31
|
+
assert "[loops.md:3]" in text
|
|
32
|
+
assert "for i in range(3):" in text and " print(i)" in text
|
|
@@ -4,6 +4,7 @@ import os
|
|
|
4
4
|
import subprocess
|
|
5
5
|
import sys
|
|
6
6
|
import tempfile
|
|
7
|
+
from importlib.metadata import version
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
|
|
@@ -34,6 +35,8 @@ def main():
|
|
|
34
35
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
35
36
|
return result.stdout
|
|
36
37
|
|
|
38
|
+
assert run(["--version"]).strip() == f"studytrails {version('studytrails')}"
|
|
39
|
+
assert not (work / "personal").exists()
|
|
37
40
|
assert "Welcome to StudyTrail" in run([], "n\n0\n")
|
|
38
41
|
assert "API key: not configured" in run(["doctor"])
|
|
39
42
|
assert "100.0%" in run(["demo"], "b\nc\nd\n")
|
|
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
|
|
File without changes
|