Skip to main content

GyanSpark

PyPI version Python versions License: MIT

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

gyanspark-0.1.1.tar.gz (39.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gyanspark-0.1.1-py3-none-any.whl (30.6 kB view details)

Uploaded Python 3

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

Hashes for gyanspark-0.1.1.tar.gz
Algorithm Hash digest
SHA256 203de113b0970e40982a71eceeecdddba3e710db6c70689b7a1ac1a7ef0d85bc
MD5 2aa2b8d1a73407f3403a1840aaa22cdd
BLAKE2b-256 c7294dead0d3d52ac61eb546b40e8fe434858dfefb405e4ceae0735031e15247

See more details on using hashes here.

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

Hashes for gyanspark-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 1ac730c86f25440dcce8641c2a84e4327aba92ac22f8423c333be357d9a07089
MD5 476c86ecc23d691168798ff03f2533be
BLAKE2b-256 b7b061f77c44e7d9330bab62c15c71e795b921dd23f9d6219eb04ce871516906

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page