Singularity: AI Engineering for an Agentic Astronomy Assistant
How Singularity uses LlamaIndex function agents, local RAG, Chainlit, Phoenix and Stellarium to answer astronomy questions and plan observing nights.
An astronomy chatbot that answers a question about Mars is quick to build. It gets harder when the same assistant has to resolve a follow-up question correctly, cite its sources, turn location, time and equipment into an observing plan, recognise unsuitable weather, and then open the selected target in Stellarium.
That is exactly what I built Singularity for. The project started as part of the CAS AI Engineering at FFHS Zurich and has grown well beyond a simple RAG prototype. Singularity connects a LlamaIndex AgentWorkflow with specialised FunctionAgent instances, a local Qdrant knowledge base, deterministic astronomy tools, Chainlit as the interface, Phoenix for tracing, and a local Stellarium integration over MCP.
The interesting part is not that an LLM can produce text about astronomy. The engineering work lies in routing each request, deciding which parts the model may handle and which have to be computed, preserving state across conversational turns, exposing sources, errors and tool calls, and measuring whether a change actually improves the system.
This article describes the state as of July 2026. Singularity is deliberately built as a local single-user system and is not yet a production-ready multi-tenant service.
The task is bigger than a chat window
Singularity covers three functionally distinct paths.
The knowledge path answers astronomy questions. It first searches a locally stored knowledge base built from OpenStax Astronomy and a curated set of Wikipedia articles. If the hits are not good enough, the agent can fall back to a web search. Answers carry source references, and for explicit image requests it searches Wikimedia Commons and the NASA Image Library.
The planning path walks step by step through preparing an observing night. It needs a location, a point in time, the available equipment and optional object preferences. It then checks the weather, computes visible objects and rates them by altitude above the horizon, brightness, light pollution and equipment.
The Stellarium path turns a plan or a direct object request into a concrete action. Singularity can open an object in a locally running Stellarium instance, check visibility beforehand, and then display a screenshot of the view inside Chainlit.
These three paths share conversation context and planning state, but they have different tools and different rules. That is where the multi-agent architecture came from.
Development in layers
I did not start Singularity as a complete multi-agent system. Development happened in several clearly separated steps.
- Technical foundation: configuration, OpenRouter integration, local embeddings via LM Studio, Qdrant, Phoenix and a first smoke test.
- Knowledge path: ingestion, local retrieval, query rewriting, web fallback, image search and source management.
- Observation planning: location and time detection, weather data, ephemerides, light pollution and deterministic scoring.
- Agents and interface: orchestrator, specialists, shared workflow state, streaming and visible tool steps in Chainlit.
- Hardening and extension: native LlamaIndex handoffs, persistent planning preferences, personas, Stellarium over MCP, tracing and evaluation harnesses.
That order mattered. Only after the tools worked individually and were testable were they handed to an agent. When something failed, it stayed visible whether the problem was in the model, the routing, the tool or the domain logic.
Architecture overview
Simplified, the architecture looks like this:
User
│
▼
Chainlit UI
│
▼
LlamaIndex AgentWorkflow
│
├── SingularityOrchestrator
│ ├── KnowledgeAgent
│ │ ├── Qdrant RAG
│ │ ├── Brave Search
│ │ ├── Wikimedia Commons
│ │ └── NASA Image Library
│ │
│ ├── PlanningAgent
│ │ ├── Nominatim / TimezoneFinder
│ │ ├── dateparser
│ │ ├── OpenWeatherMap
│ │ ├── Astropy
│ │ └── World Atlas of Artificial Sky Brightness
│ │
│ └── StellariumAgent
│ └── FastMCP adapter
│ └── Stellarium Remote Control API
│
├── shared workflow state
├── chat memory per Chainlit session
└── Phoenix tracing via OpenTelemetry/OpenInference
All LLM calls go through OpenRouter. The model used is configurable per role in config.yaml. Embeddings are generated separately and locally through an OpenAI-compatible LM Studio endpoint. Qdrant and Phoenix run as local containers.
This split is not only an infrastructure question. It makes it possible to swap models without rebuilding retrieval, agents or domain logic. At the same time the embedding model stays independent of whichever model currently handles routing or answer generation.
The LlamaIndex FunctionAgent as the unit of execution
The agents in Singularity are LlamaIndex FunctionAgent instances. A FunctionAgent receives a system prompt, a description, an LLM and a list of ordinary Python functions that are callable as tools.
LlamaIndex derives the tool schemas for the model from signatures and docstrings. A tool is therefore not a freely worded prompt fragment but a function with expected parameters and a defined return value. The agent decides which tool is needed and with which arguments it should be called. The actual work happens in Python code.
The KnowledgeAgent, for example, is built with four tools:
return FunctionAgent(
name="KnowledgeAgent",
description=(
"Answers astronomy knowledge questions using local RAG "
"and web fallback."
),
system_prompt=knowledge_prompt,
tools=[
search_local_knowledge,
search_multiple_topics,
search_web,
search_object_image,
],
llm=get_llm("knowledge_agent"),
)
That sounds unremarkable at first. In practice, though, a lot depends on whether the chosen model really handles native tool calling reliably. An OpenAI-compatible chat endpoint alone is not enough. Some models merely produce text blocks that look like tool calls instead of triggering an actual function call. Others call a tool correctly but then fail to close the delegation loop cleanly.
That is why models for Singularity are not selected by price, context window or benchmark alone. They have to demonstrate in a real FunctionAgent run that they call tools natively, supply structured arguments and produce a usable answer from the tool result. This is one of the points where AI engineering goes well beyond a successful prompt in a playground.
Why Singularity uses several specialised agents
A single agent would have access to retrieval, weather, location resolution, ephemerides, image search, Stellarium and memory functions. Technically that would work. But its system prompt would then have to describe routing, citation rules, the planning dialogue, weather decisions, object resolution, image display and Stellarium behaviour all at once.
That raises the chance of the agent picking the wrong tool or mixing rules from different domains. The alternative is a split by responsibility.
The SingularityOrchestrator is the root agent. It answers no astronomy questions itself and owns no domain tools. Its only job is to classify a message as a knowledge question, an observation plan, a Stellarium request or an off-topic request, and to hand it to the appropriate specialist.
The KnowledgeAgent answers astronomy questions. It knows only the tools for local knowledge, parallel multi-topic search, web search and images. It is a terminal specialist and does not delegate further.
The PlanningAgent runs the planning dialogue and calls the tools for location, time, weather and the observing plan. A knowledge question during planning can be handed to the KnowledgeAgent. A request such as “open the first target in Stellarium” can be passed on to the StellariumAgent.
The StellariumAgent handles object resolution, missing context, visibility and the local Stellarium instance. If the request turns back into a general knowledge question or a new plan, it can delegate back to the corresponding specialist.
The agents are registered in a shared AgentWorkflow:
return AgentWorkflow(
agents=[
orchestrator,
knowledge_agent,
planning_agent,
stellarium_agent,
],
root_agent=orchestrator.name,
initial_state=initial_state(...),
state_prompt=STATE_PROMPT,
)
The important point is LlamaIndex’s native handoff. The specialists are members of the same workflow. LlamaIndex manages context, state, tool execution, streaming and tracing. Singularity does not have to start its own sub-agent wrappers and does not copy state between separate workflows.
An earlier iteration still used custom delegation wrappers. That worked in principle, but it created unnecessary complexity around state handover, events and traces. Moving to native handoffs was therefore not a cosmetic refactor. It simplified the architecture and removed several classes of bug.
A detail that only shows up in multi-turn operation
LlamaIndex remembers the currently active agent in the workflow context. That makes sense within a running handoff. In Singularity, though, the same context is reused across several Chainlit messages so that planning data and conversation context survive.
Without extra handling, the next user message would therefore land directly at the specialist that was active last. A knowledge question after a planning session could get stuck in the PlanningAgent even though it should be classified afresh.
Before every new workflow.run(), Singularity therefore resets only the active agent back to the root agent. The rest of the context stays. Every new message is routed through the orchestrator again, without losing planning state or chat memory.
That is a small implementation detail, but a good example of why agent frameworks have to be tested differently in multi-turn operation than with isolated single prompts.
State is not the same thing as memory
In discussions about agents, “memory” is often treated as a single feature. Singularity separates three different kinds of context.
Chat memory holds the most recent messages of a Chainlit session. It is bounded by a token budget and is not persisted.
Workflow state holds structured data needed for the current task. That includes the active path, the planning parameters gathered so far, the generated observing plan and the object discussed most recently.
{
"current_path": None,
"planning_data": {
"location": None,
"datetime_iso": None,
"duration_hours": 2.0,
"equipment": None,
"object_preferences": None,
"weather_checked": False,
"weather_ok": None,
"generated_plan": None,
},
"last_discussed_object": None,
"user_profile_memory": None,
}
On top of that there is a small persistent profile memory for stable observing preferences. It can store the usual location, the typical equipment, preferred object types and a default duration. This data is loaded when a session starts and used as visible assumptions if the user does not state a different value during the current planning session.
The current session always takes precedence. Stored equipment must not override a new statement. The profile also stores no complete chats. It is a small structured preference store, not a supposedly omniscient long-term memory.
This separation makes behaviour easier to follow and reduces surprises. It also means each layer can be tested separately and later swapped for different persistence.
Local RAG is more than Qdrant plus embeddings
The knowledge base consists of the OpenStax textbook Astronomy and a curated list of Wikipedia articles. It is built in two separate steps.
First, source adapters load the content and normalise it into JSONL documents. Live access is disabled by default and has to be enabled explicitly. Raw data is cached locally so that a repeat run can reproduce the knowledge base from cache without fetching every source again.
In the second step the normalised documents are split into sections, embedded locally and written to Qdrant. Point IDs are generated deterministically from source, URL and chunk index. That way, re-indexing does not produce an entirely new set of points on every run.
The current setup uses text-embedding-qwen3-embedding-8b served locally in LM Studio with 4096 dimensions. The knowledge base is predominantly English, but questions may be in German. The multilingual embedding model handles the cross-language mapping.
Query rewriting for follow-up questions
A request like “how warm is it there?” is useless for a vector search if the preceding conversation context is not taken into account. Singularity therefore rewrites such follow-up questions into a self-contained search query before retrieval.
From a previous conversation about Mars and the question “how warm is it there?”, it produces “how warm is it on Mars?”. Only the last few messages and the current question are passed to a small rewrite step. If the question is already self-contained, it stays unchanged. If rewriting fails, the original question is used.
The original and rewritten queries are attached to Phoenix as trace attributes. That makes it possible to check later whether a poor retrieval hit was caused by the embedding model or already by a failed rewrite.
Single-topic and multi-topic search
Not every knowledge question should be searched the same way. A simple question about a single object uses search_local_knowledge. A comparison between Hubble and the James Webb Space Telescope, by contrast, is decomposed into several independent search topics and executed in parallel.
That prevents a long comparison question from ending up as a single vector midway between several topics and getting genuinely good hits for none of them. The results of the sub-queries are then merged again by the KnowledgeAgent.
Metadata filters with a deliberate fallback
For named topics, retrieval can filter on metadata.title. That increases precision when a document title matches the object being searched for. The available sources, however, do not have the same title granularity everywhere. Information about a planet may exist only as a section of a larger OpenStax chapter.
If an exact title filter returns no hits, Singularity repeats the search once without the filter. This behaviour is implemented deliberately and is logged. An empty filtered result does not automatically mean the local knowledge base contains nothing on the topic.
Sources are produced inside the tool
The agent should not reconstruct sources after the fact from free-form text. The knowledge tools therefore return, alongside the content, a deduplicated reference ID, an inline marker and a formatted citation.
The registry is reset for every new user message. Multiple chunks from the same source get the same reference number within one answer. Locally stored sources are additionally marked as a local snapshot, because the original may have changed since ingestion.
That turns citation from a polite request in a prompt into a concrete data contract between tool and agent.
Observation planning: the LLM asks, Python computes
The PlanningAgent is the part of Singularity where the separation between language model and domain logic is clearest.
The LLM runs the conversation. It recognises which details are already present, asks for missing information one item at a time and picks the appropriate tools. But it computes neither planetary positions nor light pollution nor visibility itself.
Simplified, the flow looks like this:
Resolve location
│
├── take coordinates directly
└── resolve place name via Nominatim
└── determine timezone via TimezoneFinder
Parse the time
└── German and English expressions via dateparser
Classify equipment
└── naked eye, binoculars, small or large telescope
Check the weather
└── cloud cover, precipitation, visibility and forecast range
Compute the observing plan
├── ephemerides with Astropy
├── light pollution from a local GeoTIFF
├── score the objects
└── sort and truncate the result
Ephemerides instead of model knowledge
For the given location and time window, Astropy computes the positions of planets, the Moon and a compact catalogue of bright deep-sky objects. The window is sampled in 15-minute steps. For each object the best moment within the window is determined.
Objects that stay below the horizon for the entire window are discarded. An LLM could certainly produce a plausible-sounding list, but without computation it would have no dependable statement about what is actually visible at that place and time.
Light pollution from real geodata
Light pollution is read from the World Atlas of Artificial Sky Brightness. Singularity accesses a GeoTIFF locally and determines an approximate Bortle value at the given location, plus an SQM estimate derived from it.
The large data file deliberately does not live in the repository. It is downloaded locally and referenced in the configuration. That keeps the repository small while the computation stays reproducible.
Deterministic scoring
The candidates are not sorted by the LLM’s gut feeling. Scoring accounts for the limiting magnitude of the equipment, a penalty for light pollution, the difficulty of the object type, the altitude above the horizon and the user’s preferences.
Simplified, it is based on this scheme:
effective_limit = (
limiting_magnitude[equipment]
- light_pollution_penalty[bortle]
- object_type_difficulty[object_type]
)
score = 0.65 * magnitude_score + 0.35 * altitude_score
score *= preference_multiplier
Below ten degrees of altitude an object gets no score at all. Very faint objects are discarded if they sit well outside the effective limiting magnitude. Preferred object types are weighted higher, but the others are not excluded entirely.
In the end the model receives a sorted list of structured data: object name, type, magnitude, best observing time, altitude, azimuth, score and a short assessment. Its only remaining job is to turn that into an understandable answer.
For me, this division is one of the most important points of the whole project. Probabilistic models are well suited to language, intent recognition and flexible dialogue. Computable domain logic, on the other hand, should be computed.
A complete planning run
A request might read:
Plan two hours for me tomorrow evening in Kreuzlingen with a 10-inch Dobsonian. I mainly want to see galaxies.
The orchestrator hands off to the PlanningAgent. It resolves Kreuzlingen into coordinates and a timezone, interprets “tomorrow evening”, classifies the Dobsonian as a large telescope and adopts the preference for galaxies. Then it checks the weather forecast.
If cloud cover is too high or precipitation is expected, the agent does not simply produce an optimistic list anyway. It points out the conditions and asks whether the plan should be computed regardless. Only after confirmation does target computation continue.
A subsequent general question such as “how do spiral galaxies form?” is handed to the KnowledgeAgent without losing the plan. An object-specific follow-up such as “what is special about the first target?” can be answered by the PlanningAgent through its own knowledge tool. “Open it in Stellarium” switches to the StellariumAgent, which takes target, location and time from the existing plan.
Interruptions like these are exactly where the shared workflow state pays off. A multi-step assistant must not treat a request as a series of independent chat completions.
Chainlit as UI and runtime boundary
In Singularity, Chainlit is not just a pretty chat window. The interface reflects the runtime events of the AgentWorkflow.
When a session starts, the persona configuration is loaded, Phoenix is initialised, the local Stellarium MCP server is started, stored observing preferences are read and the shared workflow context is created.
For every message the following then happens:
- The selected persona is resolved.
- The source registry is reset.
- The active agent is reset to the orchestrator.
- The workflow is started with the existing context and chat memory.
- LlamaIndex events are streamed straight into Chainlit.
- Successful planning values are stored where appropriate.
Chainlit does not receive a text answer analysed after the fact, but real workflow events. AgentStream delivers text tokens, ToolCall opens a visible step and ToolCallResult closes it with the result.
Instead of a spinner, the user therefore sees concrete steps such as:
Routing to specialist...
Searching the knowledge base...
Checking weather...
Computing ephemerides and scoring targets...
Opening Stellarium target...
This transparency is not just cosmetic. When a result is unexpected, it is immediately visible whether the right tool was called at all.
Image results are shown as Chainlit elements with source, description and licence. Stellarium screenshots are embedded as local files. Duplicate markdown images or raw image URLs are stripped from the final text so the same output does not appear twice.
Errors are part of the UI architecture too. If OpenRouter, LM Studio, Qdrant, the weather API or Stellarium is unreachable, the user gets a comprehensible message. If every required tool call fails, the interface suppresses any unsupported model answer that may still have been generated and shows the failed tools instead.
Personas without new agents
Singularity offers several answer styles: the default plus variants modelled on Carl Sagan, Marvin, Data and H. P. Lovecraft.
The obvious but wrong solution would be a dedicated PersonaAgent or a separate workflow per style. A persona is not a domain responsibility. It should change neither the routing nor the choice of tools.
The personas therefore live as data in personas.yaml. Each configuration holds an ID, a display name, a description, an icon and an additional style instruction. Chainlit presents them as a mode selector in the input field.
The style override is only appended to the agents that formulate longer answers: the KnowledgeAgent and the PlanningAgent. The orchestrator stays neutral so its routing decision is not influenced by a role. Sources, numbers, warnings, errors and tool behaviour always take precedence over style.
The prompt priority is therefore explicit:
system and tool rules
> domain agent rules
> source and formatting rules
> persona style
That sounds obvious, but it prevents a typical failure mode. An entertaining persona must never decide that bad weather is “more dramatic” than the actual measurements, or that citations do not suit the role.
New personas can be added without touching routing code. It is an example of how a clean separation of behaviour and presentation makes later extensions considerably easier.
Connecting Stellarium over MCP
The Stellarium integration uses the Model Context Protocol not for its own sake, but as a clear local system boundary.
The call path consists of several layers:
StellariumAgent
│
▼
agent-side MCP client with typed tools
│
▼
local FastMCP server
│
▼
low-level client for Stellarium Remote Control
│
▼
Stellarium
The MCP server stays deliberately thin. It holds no dialogue, asks the user nothing and does not decide which object might be meant. That responsibility sits with the StellariumAgent.
For a directly entered name, Singularity first attempts local normalisation of Messier, NGC and IC designations. SIMBAD can then be used for resolution. Only when that produces no unambiguous result is the Stellarium search used as a fallback. Several plausible matches are not resolved silently but presented to the user for selection.
Before focusing, Singularity sets location and time in Stellarium and checks visibility from the application’s point of view. If the object is below the horizon, it is not opened as a meaningful observing view.
After a successful focus, the low-level client tries to save a screenshot via the ScriptService. Chainlit displays that file directly in the conversation. If only the screenshot fails, the actual Stellarium call still counts as successful. Here too, partial success and failure are handled separately.
If Stellarium is not running or the Remote Control plugin is disabled, the rest of the application still starts. The StellariumAgent reports the integration as unavailable when it is used. An optional local integration must not render the entire chatbot useless.
Observability: agents need traces
With classic application code, a bug can often be narrowed down with logs and a stack trace. With an agent, the answer can be wrong even though every individual function call succeeded technically.
Possible causes include:
- The orchestrator handed off to the wrong agent.
- A follow-up question was rewritten badly.
- A metadata filter was too narrow.
- The agent used web search despite good local hits.
- A tool was slow and dominated the overall run.
- The model called a tool repeatedly or with unsuitable arguments.
Singularity therefore uses Arize Phoenix together with OpenTelemetry and the OpenInference instrumentation for LlamaIndex. It captures LLM calls, tool calls, retrievals, agent steps and the entire workflow as one coherent trace.
Additional attributes make the traces filterable. They include the Chainlit session, the selected persona and both the original and the rewritten retrieval query. Native handoffs appear as their own tool spans with target agent and reason.
That turns “the agent behaved oddly” into an inspectable chain:
user message
-> orchestrator
-> handoff to KnowledgeAgent
-> query rewrite
-> Qdrant retrieval with title filter
-> unfiltered retry
-> answer with sources
Tracing in Singularity is therefore not an operational feature bolted on afterwards. It was already the most important interface during development for understanding routing, retrieval and tool usage.
Evaluation instead of gut feeling
An agent quickly feels “good” after a few successful manual conversations. That says little about whether changes to prompts, models or retrieval parameters improve the overall system.
Singularity therefore separates two kinds of evaluation.
RAG evaluation uses a curated JSONL test set with German and English questions. It contains simple single topics, more complex synthesis questions, comparisons, compound questions, expected title filters and cases where a web fallback ought to be necessary.
For retrieval, the classic metrics are computed:
- Recall@k
- Precision@k
- Mean reciprocal rank
- NDCG@k
It also checks whether the expected strategy was used. That includes the use of a title filter, decomposing a question into several topics, and the web fallback.
The RAG runner can be started as a fast dry run to check datasets, metrics and result files. With --live it uses the local retrieval tools and measures the document titles actually returned.
The end-to-end suite contains multi-step conversation scenarios, among them planning sessions, ambiguous locations, bad weather, knowledge interruptions and off-topic questions. The current runner validates these scenarios deterministically against the routing logic first. A full Phoenix-based evaluation with an LLM judge is planned as the next stage, but is not yet a permanent CI gate.
This limitation is deliberately visible. Evaluation should not be described as more impressive than it is implemented. At the same time the structure is in place to compare retrieval, routing and later the quality of complete answers across git revisions.
Mistakes that improved the design
Some of the most important architectural decisions did not come from the first draft but from concrete problems.
Tool calling has to be tested for real
A model can be documented as function-calling capable and still behave differently in a specific OpenRouter and LlamaIndex setup. What counts is the complete flow of tool selection, arguments, result processing and a clean finish.
Custom delegation wrappers were unnecessary infrastructure
Custom wrappers for specialist agents made state copying, event propagation and tracing more complicated. Native handoffs within a shared AgentWorkflow already solve exactly that problem at framework level.
The context parameter does not belong in the tool schema
Many tools internally receive a LlamaIndex Context. That parameter is injected by the framework and must not appear as an argument the model is expected to supply. Otherwise the LLM tries to invent a non-serialisable workflow context or asks for a missing ctx.
A RAG hit is not yet a supported answer
Retrieval, source attribution and answer formatting have to be treated as one connected chain. That is why the tools produce stable references instead of handing the model plain text blocks and hoping for correct citations.
State has to stay explicit
An agent should not have to guess from the chat history which location has already been resolved or which observing plan currently applies. Structured state is easier to inspect, to persist and to share between specialists.
Style must not influence domain behaviour
Personas only became clean once they were kept entirely out of routing and tool usage. They are a presentation option, not a new agent role.
What is deliberately still missing
Singularity is an extensive prototype, but not a finished public service.
There is no user login yet, no tenant separation and no durable chat history. The profile memory is designed for a single local user and is stored as a JSON file. Rate limits and cost budgets are not yet implemented as central guardrails.
The deep-sky catalogue used for observation planning is deliberately compact. It is enough to demonstrate architecture and scoring, but it does not replace a complete astronomical catalogue. Weather forecasts are naturally limited to the range the provider makes available.
Evaluation exists but is not fully automated yet. The end-to-end assessment with a stronger judge model in particular is meant to be developed further.
These limits belong to the architecture just as much as the finished features do. An AI engineering project does not get better by hiding every open point behind an impressive demo.
Running Singularity locally
Since release, the project can be set up with uv:
git clone https://github.com/dprinz/singularity.git
cd singularity
uv sync
cp .env.example .env
The API keys for OpenRouter, Brave Search and OpenWeatherMap go into .env. Qdrant and Phoenix start via Docker Compose:
docker compose up -d
For the embeddings, the embedding model configured in config.yaml has to be available in LM Studio through the OpenAI-compatible endpoint. The application then starts with:
uv run chainlit run app.py
Observation planning additionally needs the World Atlas of Artificial Sky Brightness locally. The download and ingestion steps are documented in the project README.
Conclusion
For me, Singularity is above all an AI engineering project, not just an astronomy chatbot.
The LLM handles the parts it is suited to: understanding language, spotting missing details, selecting tools, switching between domain roles and explaining results comprehensibly. Retrieval, source management, location resolution, weather checks, ephemerides, light pollution and scoring live in clearly defined tools instead.
LlamaIndex provides the agentic runtime through FunctionAgent, AgentWorkflow and native handoffs. Chainlit makes text and tool execution visible. Phoenix shows what actually happens within a run. Qdrant and local embeddings form the knowledge layer. The MCP adapter connects the agent to a real desktop application.
The result is not an autonomous system meant to somehow handle an unbounded set of tasks. It is a bounded assistant with known responsibilities, explicit state, traceable tools and measurable segments.
That, to me, is the difference between a convincing LLM demo and AI engineering.
The complete source code is available on GitHub. Technical feedback on the architecture, on LlamaIndex handoffs, retrieval strategies or evaluation is welcome.