No AI feature yet?
GyanSpark can still act like one.
Everything else on this site assumes you're wrapping an LLM call. This page is for before that — GyanSpark as a real-time consultant on content you already have. Describe the question bank in a prompt, ask which ones to pick for a specific student, and get an answer back.
from openai import OpenAI
from gyanspark import GyanSpark
gs = GyanSpark(api_key="gs-...", base_url="https://...") # once per process
client = gs.wrap(OpenAI(), roll_number="A-101")
# The bank stays exactly the format you already have it in — GyanSpark
# doesn't parse it. Just describe it, and say what you want back.
prompt = """\
Here's the question bank:
Q1. What does x mean?
Q2. If A happens, what will be the value of B?
Q3. Choose the correct option for x to be true: A, B, C, D
...(the rest of the question bank, in whatever question type
you already have)
Pick 10 question from this question bank and
return the question numbers selected by you in json format.
{
"selected_questions": ["Q1", "Q2", "Q7",...]
}"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
)
print(response.choices[0].message.content)
# {
# "selected_questions": ["Q1", "Q2", "Q5", "Q12", "Q15", "Q18", "Q21", "Q25", "Q30", "Q35"]
# } That's still your own LLM call, to your own provider, with your own key — GyanSpark's part is grounding it in this student's insights, the same way it grounds a chat reply.
Personalized Question Selection from Your Question Bank.
GyanSpark helps your platform select the right questions for each student from your existing question bank.
Using insights about a student's knowledge, learning progress, misconceptions, and performance patterns, GyanSpark pushes relevant insights into your LLM call. The model then evaluates the available questions and identifies those best suited to the student's individual learning needs.
Install, then build one client at startup.
Requires Python 3.9+. Works with OpenAI, Anthropic, and Gemini — you don't need any of them installed for GyanSpark itself to install.
pip install gyanspark import os
from gyanspark import GyanSpark
# Once per process — it holds a connection pool and background workers.
# Don't build one per request.
gs = GyanSpark(
api_key=os.environ["GYANSPARK_API_KEY"],
base_url=os.environ["GYANSPARK_BASE_URL"],
) Ask for a recommendation.
Wrap your client, same as any other call, then describe the questions and what you want back in your own words. Pick your provider; the pattern is identical either way.
from openai import OpenAI
from gyanspark import GyanSpark
gs = GyanSpark(api_key="gs-...", base_url="https://...") # once per process
client = gs.wrap(OpenAI(), roll_number="A-101")
# The bank stays exactly the format you already have it in — GyanSpark
# doesn't parse it. Just describe it, and say what you want back.
prompt = """\
Here's the question bank:
Q1. What does x mean?
Q2. If A happens, what will be the value of B?
Q3. Choose the correct option for x to be true: A, B, C, D
...(the rest of the question bank, in whatever question type
you already have)
Pick 10 question from this question bank and
return the question numbers selected by you in json format.
{
"selected_questions": ["Q1", "Q2", "Q7",...]
}"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
)
print(response.choices[0].message.content)
# {
# "selected_questions": ["Q1", "Q2", "Q5", "Q12", "Q15", "Q18", "Q21", "Q25", "Q30", "Q35"]
# } import anthropic
from gyanspark import GyanSpark
gs = GyanSpark(api_key="gs-...", base_url="https://...") # once per process
client = gs.wrap(anthropic.Anthropic(), roll_number="A-101")
# The bank stays exactly the format you already have it in — GyanSpark
# doesn't parse it. Just describe it, and say what you want back.
prompt = """\
Here's the question bank:
Q1. What does x mean?
Q2. If A happens, what will be the value of B?
Q3. Choose the correct option for x to be true: A, B, C, D
...(the rest of the question bank, in whatever question type
you already have)
Pick 10 question from this question bank and
return the question numbers selected by you in json format.
{
"selected_questions": ["Q1", "Q2", "Q7",...]
}"""
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
print(response.content[0].text)
# {
# "selected_questions": ["Q1", "Q2", "Q5", "Q12", "Q15", "Q18", "Q21", "Q25", "Q30", "Q35"]
# } from google import genai
from gyanspark import GyanSpark
gs = GyanSpark(api_key="gs-...", base_url="https://...") # once per process
client = gs.wrap(genai.Client(api_key="..."), roll_number="A-101")
# The bank stays exactly the format you already have it in — GyanSpark
# doesn't parse it. Just describe it, and say what you want back.
prompt = """\
Here's the question bank:
Q1. What does x mean?
Q2. If A happens, what will be the value of B?
Q3. Choose the correct option for x to be true: A, B, C, D
...(the rest of the question bank, in whatever question type
you already have)
Pick 10 question from this question bank and
return the question numbers selected by you in json format.
{
"selected_questions": ["Q1", "Q2", "Q7",...]
}"""
response = client.models.generate_content(
model="gemini-3-flash-preview",
contents=[{"role": "user", "parts": [{"text": prompt}]}],
)
print(response.text)
# {
# "selected_questions": ["Q1", "Q2", "Q5", "Q12", "Q15", "Q18", "Q21", "Q25", "Q30", "Q35"]
# } Keep one chapter's worth of questions in a prompt rather than mixing several — "pick 10 from this bank for this student" gives the model a coherent slice of that student's understanding to reason over. Nothing stops you from pooling questions across unrelated chapters, but the pick gets less useful the more you mix in: mastery of fractions and mastery of world war two aren't comparable on the same scale.
The reply's shape is whatever you ask for
Want just the ids and nothing else? Ask for that in the prompt — "reply with only a comma-separated list of ids, no explanation." There's no separate structured mode; the format of the answer follows the format you request.
The recommendation is only as good as what you send back.
GyanSpark has no visibility into your gradebook or assignment system. Once the student finishes the question you picked for them, tell it what happened — that's what makes the next recommendation better than a guess.
from openai import OpenAI
from gyanspark import GyanSpark
gs = GyanSpark(api_key="gs-...", base_url="https://...") # once per process
# Runs when the student finishes the question you picked for them — a
# separate call from the one above, often minutes later.
recorder = gs.wrap(OpenAI(), roll_number="A-101", record_only=True)
recorder.chat.completions.create(
model="gpt-4o", # ignored — OpenAI is never contacted
messages=[
{"role": "assistant", "content": shown_to_student}, # the question you picked
{"role": "user", "content": student_response}, # what they did with it, verbatim
],
) # -> None import anthropic
from gyanspark import GyanSpark
gs = GyanSpark(api_key="gs-...", base_url="https://...") # once per process
recorder = gs.wrap(anthropic.Anthropic(), roll_number="A-101", record_only=True)
recorder.messages.create(
model="claude-sonnet-5", # ignored — Anthropic is never contacted
max_tokens=1024,
messages=[
{"role": "assistant", "content": shown_to_student}, # the question you picked
{"role": "user", "content": student_response}, # what they did with it, verbatim
],
) # -> None from google import genai
from gyanspark import GyanSpark
gs = GyanSpark(api_key="gs-...", base_url="https://...") # once per process
recorder = gs.wrap(genai.Client(api_key="..."), roll_number="A-101", record_only=True)
recorder.models.generate_content(
model="gemini-3-flash-preview", # ignored — Gemini is never contacted
contents=[
{"role": "model", "parts": [{"text": shown_to_student}]}, # the question you picked
{"role": "user", "parts": [{"text": student_response}]}, # what they did with it, verbatim
],
) # -> None Send the real response, not a score
Pass the student's own answer as the user message — the
option they picked, the solution they wrote — not "correct" or "incorrect".
The actual attempt is what turns into a useful node.
Prompt as assistant, response as user
What you showed the student goes in as assistant,
their attempt as
user — the same shape as recording any non-chat feature.
Nothing comes back
In record_only mode your provider is never contacted
and the method just returns None, so problems are
silent by default. Enable the
gyanspark logger if you want to see them.
What to expect once it's in the call path.
Worth reading before you ship, not after.
It's the same wrap(), a different prompt
Nothing about the SDK changes for this use case — the mechanism, guarantees, and options below are identical to the chat integration. What differs is what you ask the model to do and how many calls you make.
Context lands in your system prompt, not your list
This student's insights are injected into the system slot automatically, the same as any wrapped call. The question bank and the ask stay in the message you write yourself — GyanSpark never touches or parses it.
It never breaks your flow
If GyanSpark is slow, unreachable, or has nothing to say yet, no context is added and your LLM call goes out exactly as if you'd never wrapped the client. An invalid API key is the one exception — that raises.
New students start empty
The first time a roll_number is seen there's nothing to ground a pick in, so no context is added. It builds from that call onward.
Options on wrap()
gs.wrap(llm_client, roll_number,
bypass=False, record_only=False) | Option | What it does |
|---|---|
roll_number required | Your own identifier for the student, and the only identity the SDK needs. An unseen value creates the student for you. Not a True/False flag — always required, on every call. |
bypass | True GyanSpark is switched off for this client. You get the original, unwrapped client back — no context retrieval, no injection, nothing recorded.
Falsedefault GyanSpark runs normally: context is retrieved, injected, and the exchange is recorded in the background. |
record_only | True The method becomes a pure recorder — your provider is never contacted, no LLM call goes out, and it returns None instead of a response.
Falsedefault A normal wrapped call: your provider is contacted, context is injected, and the response comes back as usual. |
bypass and record_only are mutually exclusive
— setting both
True at once raises ValidationError before either
takes effect.
Errors, and which ones can actually reach you.
Everything importable from gyanspark. In practice
only the first three surface — the rest are handled internally so
your LLM call still goes out.
| Exception | When |
|---|---|
ValidationError | Bad arguments to wrap() — a missing roll_number, an unsupported or async client, or both mode flags set. Raised before any network call. |
ConfigurationError | A missing api_key or base_url when constructing GyanSpark. |
AuthenticationError | Your API key is invalid or deactivated. |
GyanSparkTimeoutError | A request took too long. Handled internally — your LLM call still goes out. |
GyanSparkConnectionError | GyanSpark was unreachable. Handled internally — your LLM call still goes out. |
GyanSparkAPIError | Base class for the two above. |
Async clients aren't supported by wrap() yet, and are rejected
up front rather than silently slowing your event loop.
That's the whole surface area.
Book a demo if you want a walkthrough against your own question bank, or start wiring it up now. Add a full chat feature later and it's the exact same client you already wrapped.