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.
Files changed (41) hide show
  1. {studytrails-0.2.2 → studytrails-0.2.3}/PKG-INFO +5 -2
  2. {studytrails-0.2.2 → studytrails-0.2.3}/README.md +16 -6
  3. {studytrails-0.2.2 → studytrails-0.2.3}/docs/PYPI.md +4 -1
  4. {studytrails-0.2.2 → studytrails-0.2.3}/docs/RELEASING.md +3 -2
  5. {studytrails-0.2.2 → studytrails-0.2.3}/docs/SETUP.md +14 -2
  6. {studytrails-0.2.2 → studytrails-0.2.3}/docs/USER_GUIDE.md +11 -1
  7. {studytrails-0.2.2 → studytrails-0.2.3}/pyproject.toml +1 -1
  8. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/agent.py +13 -10
  9. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/cli.py +18 -5
  10. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/onboarding.py +15 -8
  11. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/preferences.py +5 -1
  12. studytrails-0.2.3/study_agent/providers.py +10 -0
  13. studytrails-0.2.3/study_agent/ui.py +21 -0
  14. {studytrails-0.2.2 → studytrails-0.2.3}/tests/test_agent.py +28 -10
  15. {studytrails-0.2.2 → studytrails-0.2.3}/tests/test_onboarding.py +30 -3
  16. studytrails-0.2.3/tests/test_ui.py +32 -0
  17. {studytrails-0.2.2 → studytrails-0.2.3}/tests/wheel_smoke.py +3 -0
  18. {studytrails-0.2.2 → studytrails-0.2.3}/uv.lock +1 -1
  19. {studytrails-0.2.2 → studytrails-0.2.3}/.gitignore +0 -0
  20. {studytrails-0.2.2 → studytrails-0.2.3}/.python-version +0 -0
  21. {studytrails-0.2.2 → studytrails-0.2.3}/CONTRIBUTING.md +0 -0
  22. {studytrails-0.2.2 → studytrails-0.2.3}/LICENSE +0 -0
  23. {studytrails-0.2.2 → studytrails-0.2.3}/assets/banner.svg +0 -0
  24. {studytrails-0.2.2 → studytrails-0.2.3}/assets/logo.svg +0 -0
  25. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/__init__.py +0 -0
  26. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/__main__.py +0 -0
  27. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/config.py +0 -0
  28. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/default_notes/java/basics.md +0 -0
  29. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/default_notes/python/collections.md +0 -0
  30. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/default_notes/python/functions.md +0 -0
  31. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/default_notes/python/loops.md +0 -0
  32. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/demo.py +0 -0
  33. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/models.py +0 -0
  34. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/notes.py +0 -0
  35. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/storage.py +0 -0
  36. {studytrails-0.2.2 → studytrails-0.2.3}/study_agent/tools.py +0 -0
  37. {studytrails-0.2.2 → studytrails-0.2.3}/tests/conftest.py +0 -0
  38. {studytrails-0.2.2 → studytrails-0.2.3}/tests/test_cli.py +0 -0
  39. {studytrails-0.2.2 → studytrails-0.2.3}/tests/test_config.py +0 -0
  40. {studytrails-0.2.2 → studytrails-0.2.3}/tests/test_storage.py +0 -0
  41. {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.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 Groq address presets; custom compatible endpoints supported.
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/N]:
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 address preset.
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 --help
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 Groq address presets; custom compatible endpoints supported.
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
- Versions 0.2.0 and 0.2.1 are published on PyPI. Version 0.2.2 is the next local candidate.
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 address presets for OpenAI and Groq. A preset only fills in the
97
- provider address; it does not provide a key or guarantee every model will work.
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.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "studytrails"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "A personal multi-subject AI study coach with tools, local progress, and an offline demo."
9
9
  readme = "docs/PYPI.md"
10
10
  license = "MIT"
@@ -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
- parallel_tool_calls=False,
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
- console.print(f" Tool: {name}", style="dim")
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.input("Take it now? [y/N]: ").strip().lower() in {"y", "yes"}:
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.print(f"\nCoach: {answer}")
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.input("Configure AI now? [y/N]: ").strip().lower() in {"y", "yes"}:
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.print(f"\nCoach: {answer}")
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 2. OpenAI 3. Groq")
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 3.")
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
- model = console.input("Model ID from your provider: ").strip()
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.input("Test tool calling now? [y/N]: ").strip().lower() in {"y", "yes"}:
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
- parallel_tool_calls=False,
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 = {"openai": "https://api.openai.com/v1", "groq": "https://api.groq.com/openai/v1"}
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
- ["https://api.groq.com/openai/v1", "https://api.openai.com/v1", "https://custom.example/v1"],
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": "openai/gpt-oss-120b",
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(Settings(api_key="test-not-a-real-key", base_url=base_url), tools)
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"] == "openai/gpt-oss-120b"
153
- assert requests[0]["parallel_tool_calls"] is False
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
- def test_connection_probe_checks_actual_tool_response(tmp_path, monkeypatch, supported):
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) == "https://provider.example/v1/chat/completions"
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="https://provider.example/v1",
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")
@@ -742,7 +742,7 @@ wheels = [
742
742
 
743
743
  [[package]]
744
744
  name = "studytrails"
745
- version = "0.2.2"
745
+ version = "0.2.3"
746
746
  source = { editable = "." }
747
747
  dependencies = [
748
748
  { name = "keyring" },
File without changes
File without changes
File without changes
File without changes
File without changes