Ask ChatGPT “where was Saturn when I was born, 2:30 pm on 15 August 1990 in New Delhi?” and you’ll get a fluent, confident answer — that is quietly, completely made up. This is the trap at the heart of every naive AI astrology bot: people ask the language model to do astronomy, and language models are spectacularly bad at precise calculation. The result is a chatbot that talks beautifully and gets the chart wrong, which for a kundali app is the one thing you can’t afford. This article is how to build it properly — so the results are not just impressive-sounding but provably correct.
A note up front: this is an engineering article about calculation accuracy and LLM architecture. Astrology is a cultural and belief system, not a science; nothing here claims it predicts the future. Build these apps responsibly (more on that at the end).
The architecture: the LLM talks, the ephemeris calculates
The whole design rests on one decision: the LLM never computes the chart. It does the two things it’s genuinely good at — understanding a messy birth-details sentence, and explaining results in natural language — and hands the astronomy to deterministic code.
The flow is a standard tool-using agent:
- The LLM extracts the birth details (date, time, place) from the user’s message into structured data.
- Geocoding + timezone turn “New Delhi” into latitude/longitude and the correct timezone (including historical rules — a real source of bugs).
- A deterministic tool computes the chart with Swiss Ephemeris — this is the astronomy, and it’s exact.
- The LLM interprets and converses, grounded on the exact numbers the tool returned — explanation, Q&A, follow-ups.
Grounding the model on exact numbers is what turns a plausible-but-wrong astrology bot into one whose chart is correct. It’s the same pattern as any serious tool-using agent (see the AI Agent Development course and the RAG pipeline article): the model orchestrates and explains; tools provide the facts.
Why the LLM must not compute the chart
It’s worth being concrete about why you can’t skip the ephemeris and let the model estimate.
A planet’s position is an astronomical computation requiring an ephemeris — a high-precision model of solar-system motion. An LLM has no such model; it pattern-matches text, so it emits a plausible number that is almost never the right number, and it can’t reproduce it. Worse, astrology-specific pitfalls — timezones, and especially tropical vs sidereal zodiacs (Vedic astrology uses sidereal) — are exactly the kind of systematic correction a model gets wrong. A chart that’s off by a few degrees puts planets in the wrong sign or nakshatra, and every downstream interpretation is then wrong.
So you give each component the job it’s good at. The ephemeris computes; the model explains. And the computation itself is a precise, unforgiving accuracy chain — place → lat/lon → timezone → Julian Day → ephemeris → sidereal correction → rasi/nakshatra/dasha — that belongs in tested code, not a prompt.
The calculation stack: Swiss Ephemeris
The “standard library” for this is Swiss Ephemeris, via the pyswisseph Python binding. It computes planetary positions to sub-arcsecond accuracy from the JPL DE ephemeris data, it’s deterministic (same input → same output, forever), and it’s the same engine most professional astrology software runs on. Here’s the core of a real compute_kundali, computing the sidereal (Vedic) chart with the standard Lahiri ayanamsa:
import swisseph as swe
def compute_kundali(year, month, day, hour, minute, tz_offset, lat, lon):
# 1. Local time -> Universal Time -> Julian Day (the astronomy clock)
ut = hour + minute / 60 - tz_offset # e.g. IST = UTC+5:30
jd = swe.julday(year, month, day, ut)
# 2. Vedic = SIDEREAL zodiac with the Lahiri ayanamsa (NOT tropical)
swe.set_sid_mode(swe.SIDM_LAHIRI)
flags = swe.FLG_MOSEPH | swe.FLG_SIDEREAL
# 3. The nine grahas (Rahu = lunar node; Ketu = node + 180)
bodies = {"Sun": swe.SUN, "Moon": swe.MOON, "Mars": swe.MARS,
"Mercury": swe.MERCURY, "Jupiter": swe.JUPITER,
"Venus": swe.VENUS, "Saturn": swe.SATURN, "Rahu": swe.MEAN_NODE}
planets = {}
for name, b in bodies.items():
lon_deg = swe.calc_ut(jd, b, flags)[0][0] # sidereal longitude
planets[name] = round(lon_deg, 4)
planets["Ketu"] = round((planets["Rahu"] + 180) % 360, 4)
# 4. Lagna (ascendant) from the houses
asc = swe.houses_ex(jd, lat, lon, b'W', flags)[1][0]
return {"ayanamsa": round(swe.get_ayanamsa_ut(jd), 4),
"lagna": round(asc, 4), "planets": planets}Running this for 15 Aug 1990, 14:30 IST, New Delhi gives real, reproducible output:
Ayanamsa (Lahiri): 23.7261°
Saturn: 266.1385° → Sagittarius · Purva Ashadha
Moon: 51.3071° → Taurus · Rohini (pada 4)
Sun: 118.5551° → Cancer · Ashlesha
Lagna: 230.2958° → Scorpio · JyeshthaThose are computed, not guessed — swe.calc_ut returns 266.1385° for Saturn every single time, to the arcsecond. From the raw longitudes you derive the rest of the chart with simple arithmetic: the Rasi (sign) is int(longitude / 30); the Nakshatra is int(longitude / (360/27)) with the pada being which of its four quarters; and the Vimshottari Dasha (the planetary-period timeline) starts from the lord of the Moon’s nakshatra, with the balance proportional to how far through that nakshatra the Moon has travelled. For our example, the Moon is in Rohini (lord: Moon), so the dasha sequence begins in Moon mahadasha (≈1.5 years of balance at birth) → Mars (7y, from 1992) → Rahu (18y, from 1999) → Jupiter (16y, from 2017) → Saturn (19y). All deterministic, all testable.
For the Vedic-specific pieces (divisional charts like Navamsa D9, dasha sub-periods, yogas, panchanga) you can layer your own arithmetic on Swiss Ephemeris, or use a higher-level library built on top of it — jyotisha or PyJHora for comprehensive Vedic computations, flatlib for a lighter Western-leaning toolkit. But the engine underneath is Swiss Ephemeris either way, and the principle holds: it’s code, with tests, not the LLM.
Wiring it to the LLM with function-calling
Now the LLM side. You expose compute_kundali as a tool (function-calling), describe it to the model, and let the model call it with the extracted birth details:
tools = [{
"type": "function",
"function": {
"name": "compute_kundali",
"description": "Compute an accurate Vedic birth chart (sidereal, Lahiri).",
"parameters": {
"type": "object",
"properties": {
"year": {"type": "integer"}, "month": {"type": "integer"},
"day": {"type": "integer"}, "hour": {"type": "integer"},
"minute": {"type": "integer"},
"place": {"type": "string", "description": "birth city/town"}
},
"required": ["year","month","day","hour","minute","place"]
}
}
}]The conversation then goes: user message → the model calls compute_kundali(...) → your code geocodes the place, resolves the timezone, runs Swiss Ephemeris, and returns the exact JSON → you feed that JSON back to the model → the model writes the answer, grounded on the numbers. The model decides when to compute and how to explain; it never invents a degree.
To make the interpretations themselves trustworthy (and genuinely impressive), add retrieval: index a corpus of classical rules and texts, and have the model interpret from retrieved rules rather than from its training-data memory of astrology — the RAG pattern. Now “why is this a Gajakesari yoga?” is answered from an actual rule about Jupiter and the Moon, applied to the computed positions, not a hallucinated one. Deterministic chart + retrieved rules + the LLM’s language = results that are accurate, explained, and conversational.
What actually makes the results impressive
The “wow” in a kundali chatbot comes from a few compounding things, all enabled by getting the foundation right:
- Correctness you can verify. Users (and other astrologers) can check the chart against any professional software and it matches to the arcsecond. Trust is the whole product.
- The full chart, instantly — planets, Lagna, nakshatras, the Vimshottari dasha timeline with real dates, divisional charts — rendered as a proper North/South-Indian diagram, computed in milliseconds.
- Natural conversation over exact data — “what does my current dasha mean?”, “when does my Saturn period start?” — answered precisely because the model is reading real numbers, not guessing.
- Personalisation and memory — remember the user’s chart across the session so follow-ups don’t re-ask for birth details.
None of that works if the chart is wrong. Accuracy first; everything impressive is built on it.
The pitfalls (where these apps actually break)
- Timezone — the #1 bug. Birth time is local, but the ephemeris needs Universal Time. Get the offset wrong (DST, historical timezone changes, India’s half-hour offset) and every planet moves. Use a proper timezone library keyed to the place and date, not a hardcoded offset.
- Tropical vs sidereal. Western astrology is tropical; Vedic is sidereal — you must set an ayanamsa (
SIDM_LAHIRI). Forget it and the entire chart shifts ~24°, into the wrong signs. This is the single most common “the chart is wrong” cause. - Ayanamsa choice. Lahiri/Chitrapaksha is the common default, but others exist (Raman, KP). Pick one, state it, be consistent.
- Ambiguous or unknown birth time. The Lagna and houses are very time-sensitive (the ascendant moves ~1° every 4 minutes). If the user isn’t sure of the minute, say so and offer a time-rectification caveat — don’t present a precise Lagna as certain.
- Letting the LLM “help” with the math. The moment you let the model fill in a missing degree or “estimate” a position, correctness is gone. The tool computes everything; the model touches no numbers.
Build it responsibly
Because this sits at the intersection of software and belief, a word on ethics. Present it honestly as a cultural/astrological tool, not scientific prediction. Add clear disclaimers. Put guardrails on the LLM so it won’t give fatalistic, medical, financial or legal “predictions” (“you will get a disease”, “sell your house”) — those cause real harm; keep it to traditional chart description and general, non-deterministic language. The engineering here (accurate computation, grounded generation) is sound and genuinely interesting; use it kindly.
Takeaways
- Never let the LLM compute the chart — it hallucinates degrees, mishandles timezones, and confuses tropical/sidereal. Use a deterministic ephemeris.
- Swiss Ephemeris (
pyswisseph) is the standard: sub-arcsecond, reproducible, sidereal with Lahiri ayanamsa — the same engine pro software uses. (Layerjyotisha/PyJHorafor deeper Vedic features.) - Split the work: the LLM extracts birth details and interprets results; a tool computes via function-calling and returns exact JSON the model grounds on. Add RAG over classical rules for trustworthy interpretation.
- Mind the accuracy chain (place → lat/lon → timezone → Julian Day → sidereal → nakshatra/dasha) — it’s tested code, not a prompt; timezone and tropical-vs-sidereal are the top bugs.
- Build it responsibly: honest framing, disclaimers, and guardrails against harmful predictions.
If you want the broader patterns this builds on, see the AI Agent Development course (tool-using agents), the RAG pipeline guide (grounding generation), and Building Voicebots if you want to make the astrologer talk.











