GyanSpark
Give your AI tutor a memory of each student.
Wrap your existing LLM client with GyanSpark and every chat turn does two extra things automatically: it learns from the conversation so far, and it grounds the next reply in what's already known about that student. Your call signature doesn't change.
- response = client.chat.completions.create(model="gpt-4o", messages=messages)
+ client = gs.wrap(client, roll_number="A-101")
+ response = client.chat.completions.create(model="gpt-4o", messages=messages)
Install
pip install gyanspark
Requires Python 3.9+. Works with Gemini, OpenAI, and Anthropic — you don't need to have any of them installed for GyanSpark itself to install.
Setup
Create one client when your process starts, and reuse it for every student. It holds a connection pool and a background worker pool, so don't build one per request.
import os
from gyanspark import GyanSpark
gs = GyanSpark(
api_key=os.environ["GYANSPARK_API_KEY"],
base_url=os.environ["GYANSPARK_BASE_URL"],
)
Both are required. There's no default base_url on purpose — a silent
fallback to production when you meant to point at staging is the kind of
mistake that costs a day.
Wrap your client
wrap() is cheap — it allocates nothing but a small object — so call it once
per request, with that request's student.
model = gs.wrap(llm_client, roll_number="A-101")
roll_number is your own identifier for the student. Use whatever you already
key students by; if GyanSpark hasn't seen it before, the student is created
for you.
Examples
OpenAI
from openai import OpenAI
from gyanspark import GyanSpark
gs = GyanSpark(api_key="gs-...", base_url="https://...") # once per process
def handle_message(roll_number: str, messages: list) -> str:
client = gs.wrap(OpenAI(), roll_number=roll_number)
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
)
return response.choices[0].message.content
The student's context is added as a system message at the front of the
request. If your first message is already a system message, your text is
kept and the context is appended to it:
messages = [
{"role": "system", "content": "You are a patient math tutor."},
{"role": "user", "content": "how do i factor this?"},
]
# The model receives: "You are a patient math tutor.\n\n<student context>"
Anthropic
import anthropic
from gyanspark import GyanSpark
gs = GyanSpark(api_key="gs-...", base_url="https://...")
def handle_message(roll_number: str, messages: list) -> str:
client = gs.wrap(anthropic.Anthropic(), roll_number=roll_number)
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system="You are a patient math tutor.", # kept; context appended
messages=messages,
)
return response.content[0].text
Context goes into the top-level system parameter. If you pass system as a
list of blocks, a text block is appended and your blocks are left as they are.
Gemini
from google import genai
from gyanspark import GyanSpark
gs = GyanSpark(api_key="gs-...", base_url="https://...")
def handle_message(roll_number: str, contents: list) -> str:
client = gs.wrap(genai.Client(api_key="..."), roll_number=roll_number)
response = client.models.generate_content(
model="gemini-3-flash-preview",
contents=contents,
)
return response.text
Context goes into config.system_instruction. Pass your own config and it's
preserved — every other field is carried over untouched, and the object you
passed is never modified, so it's safe to build one config at startup and
reuse it:
config = genai.types.GenerateContentConfig(
system_instruction="You are a patient math tutor.",
temperature=0.4,
)
# reuse `config` for every request — it never accumulates anything
Which call is intercepted
| Provider | Call | Context is added to |
|---|---|---|
| OpenAI | client.chat.completions.create |
leading system message |
| Anthropic | client.messages.create |
system= parameter |
| Gemini | client.models.generate_content |
config.system_instruction |
Everything else on the client — other methods, other attributes, streaming variants — is passed straight through and behaves exactly as it always did.
What to expect
Your conversation objects are never modified. The context is merged into a
copy that exists only for that one request. The messages / contents /
config you passed in come back exactly as you passed them, so whatever you
save to your database is your own conversation and nothing else. Context is
also never added to the conversation itself — only to the system prompt slot —
so there's nothing to strip out later.
Nothing is cached between calls. Context is fetched fresh every time and is never reused, so a student who just had a breakthrough isn't described by last week's snapshot. This also means the wrapper behaves identically across processes, workers, and replicas — there's no session state to keep sticky.
Learning happens in the background. Recording what the student just did never blocks your response, and adds no latency to your reply.
Fetching context is on the critical path. The wrapped call waits briefly to retrieve the student's context before calling your LLM, since the reply depends on it. Budget a short round trip in your request timing.
It never breaks your chat. 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 hadn't wrapped the client. The one exception is an invalid API key, which raises — a bad key quietly degrading to "no memory, forever" is a much worse failure to debug.
New students start empty. The first time a roll_number is seen there's
nothing to ground a reply in yet, so no context is added. It builds up from
that conversation onward.
Nothing is recorded until an exchange completes. GyanSpark learns from the
completed question-and-answer pairs in the history you pass, so the first
message of a conversation records nothing. One consequence worth knowing: the
final exchange of a conversation is never recorded, because nothing follows it
to trigger the recording. If that matters for your use case, record it
explicitly with record_only (below) when the session ends.
Options
gs.wrap(llm_client, roll_number, directive=None,
bypass=False, record_only=False)
| Option | Effect |
|---|---|
roll_number |
Required. Your identifier for the student, and the only identity the SDK needs. Unknown values create a new student. |
directive |
Shapes how the returned context is framed for your feature — a quiz generator wants it phrased differently than a chat tutor. |
bypass |
Turn GyanSpark off entirely for this client. |
record_only |
Record an exchange without calling the LLM. |
bypass=True
You get your client back completely untouched — nothing is recorded, nothing is retrieved, nothing is injected, and there's no wrapper in the call path at all. Useful as a kill switch you can drive from config without editing any call site:
client = gs.wrap(OpenAI(), roll_number="A-101", bypass=settings.MEMORY_DISABLED)
Arguments are still validated, so switching it back off can't surprise you with a new error later.
record_only=True
The call stops being an LLM call. The exchange is recorded and the method
returns None — your provider is never contacted, and no context is
retrieved. Use it to submit things the student did outside of chat, like
graded answers, through the same integration you already have:
recorder = gs.wrap(OpenAI(), roll_number="A-101", record_only=True)
recorder.chat.completions.create(
model="gpt-4o", # ignored — nothing is sent to OpenAI
messages=[
{"role": "user", "content": "Q: What is the derivative of x²?"},
{"role": "assistant", "content": "Student answered: 2x, but wrote x³/3 first"},
],
) # -> None
Only the most recent user/assistant pair is recorded. Pass a longer trail and
the earlier turns are ignored, with a warning on the gyanspark logger. Since
nothing is returned, enable that logger if you want to see problems:
import logging
logging.getLogger("gyanspark").setLevel(logging.WARNING)
bypass and record_only are mutually exclusive — setting both raises
ValidationError.
Seeing what's happening
The wrapper is silent by design — which is exactly what you don't want while
integrating. Pass debug=True and it narrates every turn:
gs = GyanSpark(api_key=..., base_url=..., debug=True)
[gyanspark] turn : 3 conversation turn(s) in history; query='how do i factor it'; 1 completed exchange to record
[gyanspark] write : queued 8c21-... in the background (2 turn(s), student=A-101) - not waiting on it
[gyanspark] context : empty - student=A-101 is not enrolled yet; the background write is what creates them
[gyanspark] context : skipped - student is not enrolled yet, nothing recorded to fetch.
No system prompt will be injected this turn.
[gyanspark] write : background write 8c21-... finished - recorded
and on a later turn:
[gyanspark] context : got 412 chars (cutoff_k=3) for student=A-101
[gyanspark] context : <the exact text being added to your system prompt>
[gyanspark] inject : adding 412 chars to the system prompt
That tells you the four things worth knowing: whether the student is known to the graph yet, whether context came back and what it says, whether it was injected, and how the background recording finished. Anything skipped says why.
Leave it off in production. If you already configure logging, skip the flag and set the level yourself — same output, your handlers:
logging.getLogger("gyanspark").setLevel(logging.DEBUG)
Errors
Everything importable from gyanspark:
| Exception | When |
|---|---|
ValidationError |
Bad arguments to wrap() — missing roll_number, an unsupported client, an async client, or both mode flags set. Raised immediately, before any network call. |
ConfigurationError |
Missing api_key or base_url when constructing GyanSpark. |
AuthenticationError |
Your API key is invalid or deactivated. |
GyanSparkTimeoutError |
A request took too long. |
GyanSparkConnectionError |
GyanSpark was unreachable. |
GyanSparkAPIError |
Base class for the three above. |
Of these, only ValidationError, ConfigurationError, and
AuthenticationError can reach you in practice — the rest are handled
internally so your LLM call still goes out.
from gyanspark import AuthenticationError, GyanSparkError
try:
response = client.chat.completions.create(model="gpt-4o", messages=messages)
except AuthenticationError:
... # check your GyanSpark API key
Async clients aren't supported by wrap() yet and are rejected up front
rather than silently slowing your event loop.
Cleanup
GyanSpark holds a connection pool and background workers. Close it on
shutdown, or use it as a context manager:
gs.close()
# or
with GyanSpark(api_key=..., base_url=...) as gs:
...
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gyanspark-0.1.1.tar.gz.
File metadata
- Download URL: gyanspark-0.1.1.tar.gz
- Upload date:
- Size: 39.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
203de113b0970e40982a71eceeecdddba3e710db6c70689b7a1ac1a7ef0d85bc
|
|
| MD5 |
2aa2b8d1a73407f3403a1840aaa22cdd
|
|
| BLAKE2b-256 |
c7294dead0d3d52ac61eb546b40e8fe434858dfefb405e4ceae0735031e15247
|
File details
Details for the file gyanspark-0.1.1-py3-none-any.whl.
File metadata
- Download URL: gyanspark-0.1.1-py3-none-any.whl
- Upload date:
- Size: 30.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ac730c86f25440dcce8641c2a84e4327aba92ac22f8423c333be357d9a07089
|
|
| MD5 |
476c86ecc23d691168798ff03f2533be
|
|
| BLAKE2b-256 |
b7b061f77c44e7d9330bab62c15c71e795b921dd23f9d6219eb04ce871516906
|