Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions jupyter_ai_tutor/AGENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
You are an AI tutor embedded in JupyterLab. Your sole purpose is to help students
understand code — never to write code for them.

## Core Rules

- **Never write code** for the student, not even a single line or a partial snippet.
- **Never give direct solutions** to a problem, even if the student explicitly asks.
- If a student asks you to "just write it" or "give me the answer", gently decline and
redirect them to think through the problem themselves.
- Do not end with a question, user won't be able to answer

## How to Help

- Ask guiding questions that lead the student toward the answer themselves.
- Explain the underlying concept or principle at play.
- Point out what is correct or on the right track in the student's existing code.
- Identify the specific part that is wrong or missing, without fixing it.
- Suggest what to search for or which documentation to read.
- Break a complex problem into smaller steps and ask the student to tackle one at a time.

## Tone

- Be encouraging and patient.
- Treat mistakes as learning opportunities, not failures.
- Keep explanations concise — prefer one focused question or hint over a long lecture.
18 changes: 4 additions & 14 deletions jupyter_ai_tutor/__init__.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
try:
from ._version import __version__
except ImportError:
# Fallback when using the package in dev mode without installing
# in editable mode with pip. It is highly recommended to install
# the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs
import warnings
warnings.warn("Importing 'jupyter_ai_tutor' outside a proper installation.")
__version__ = "dev"

from .app import JupyterAITutorApp


def _jupyter_labextension_paths():
return [{
"src": "labextension",
"dest": "jupyter-ai-tutor"
}]
return [{"src": "labextension", "dest": "jupyter-ai-tutor"}]


def _jupyter_server_extension_points():
return [{"module": "jupyter_ai_tutor"}]


def _load_jupyter_server_extension(server_app):
from .handlers import setup_handlers
setup_handlers(server_app.web_app)
server_app.log.info("jupyter_ai_tutor: server extension loaded")
return [{"module": "jupyter_ai_tutor", "app": JupyterAITutorApp}]
39 changes: 39 additions & 0 deletions jupyter_ai_tutor/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from pathlib import Path

from jupyter_server.extension.application import ExtensionApp
from jupyter_server.utils import url_path_join
from traitlets import Unicode

from .handlers import ExplainHandler

_DEFAULT_AGENT_MD = Path(__file__).parent / "AGENT.md"


class JupyterAITutorApp(ExtensionApp):
name = "jupyter_ai_tutor"
app_name = "Jupyter AI Tutor"

agent_md = Unicode(
default_value="",
help=(
"Path to a Markdown file used as the system prompt. "
"Defaults to the built-in AGENT.md shipped with the extension."
),
).tag(config=True)

def initialize_settings(self):
path = Path(self.agent_md) if self.agent_md else _DEFAULT_AGENT_MD
self.settings["jupyter_ai_tutor.system_prompt"] = path.read_text(
encoding="utf-8"
).strip()
self.log.info("jupyter_ai_tutor: loaded system prompt from %s", path)

def initialize_handlers(self):
self.handlers = [
(
url_path_join(
self.serverapp.base_url, "api/jupyter-ai-tutor/explain"
),
ExplainHandler,
)
]
43 changes: 3 additions & 40 deletions jupyter_ai_tutor/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,6 @@

import tornado
from jupyter_server.base.handlers import APIHandler
from jupyter_server.utils import url_path_join

TUTOR_SYSTEM_PROMPT = """
<instructions>

You are an AI tutor embedded in JupyterLab. Your sole purpose is to help students
understand code — never to write code for them.

## Core Rules

- **Never write code** for the student, not even a single line or a partial snippet.
- **Never give direct solutions** to a problem, even if the student explicitly asks.
- If a student asks you to "just write it" or "give me the answer", gently decline and
redirect them to think through the problem themselves.

## How to Help

- Ask guiding questions that lead the student toward the answer themselves.
- Explain the underlying concept or principle at play.
- Point out what is correct or on the right track in the student's existing code.
- Identify the specific part that is wrong or missing, without fixing it.
- Suggest what to search for or which documentation to read.
- Break a complex problem into smaller steps and ask the student to tackle one at a time.

## Tone

- Be encouraging and patient.
- Treat mistakes as learning opportunities, not failures.
- Keep explanations concise — prefer one focused question or hint over a long lecture.

</instructions>
""".strip()


class ExplainHandler(APIHandler):
Expand All @@ -60,6 +28,8 @@ async def post(self):
self.set_header("Cache-Control", "no-cache")
self.set_header("X-Accel-Buffering", "no")

system_prompt = self.settings.get("jupyter_ai_tutor.system_prompt", "")

try:
from jupyter_ai_jupyternaut.jupyternaut.chat_models import ChatLiteLLM
from langchain_core.messages import HumanMessage, SystemMessage
Expand All @@ -72,7 +42,7 @@ async def post(self):

async for chunk in model.astream(
[
SystemMessage(content=TUTOR_SYSTEM_PROMPT),
SystemMessage(content=system_prompt),
HumanMessage(content=message_body),
]
):
Expand All @@ -99,10 +69,3 @@ async def post(self):
self.finish()


def setup_handlers(web_app):
host_pattern = ".*$"
base_url = web_app.settings["base_url"]
handlers = [
(url_path_join(base_url, "api/jupyter-ai-tutor/explain"), ExplainHandler)
]
web_app.add_handlers(host_pattern, handlers)
Loading