Skip to content

Commit 19b8546

Browse files
committed
chore: Updated README.
chore: Organised environment variables.
1 parent 4ad774c commit 19b8546

File tree

9 files changed

+112
-55
lines changed

9 files changed

+112
-55
lines changed

README.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99

1010
The DQA aka _difficult questions attempted_ project utilises large language model (LLM) agent(s) to perform _multi-hop question answering_ (MHQA).
1111

12-
**Note that this repository is undergoing a complete overhaul of the [older, now obsolete, version of DQA](https://github.com/anirbanbasu/dqa-obsolete). The purpose of this overhaul is to standardise agent communication using the A2A protocol and to use the Dapr virtual actors to manage the backend logic.**
12+
_Note that this repository is undergoing a complete overhaul of the [older, now obsolete, version of DQA](https://github.com/anirbanbasu/dqa-obsolete). The purpose of this overhaul is to standardise agent communication using the A2A protocol and to use the Dapr virtual actors to manage the backend logic._
1313

1414
## Overview
15-
TBA
15+
DQA - Difficult Questions Attempted - is an agentic chatbot that attempts to answer non-trivial multi-hop questions using (large) language models (LLM) and tools available over the Model Context Protocol (MCP). The functionality of DQA is basic. As of late 2025, the functionality of DQA is available in most commercial chatbots.
16+
17+
However, DQA is experimental with the emphasis on standardising agentic communication and managing backend functionality using Dapr-managed virtual actors. Although the instructions below explain the deployment of DQA on a single machine, it can be deployed and run on a Kubernetes cluster, with minimal modifications to the configuration.
1618

1719
## Installation
1820

@@ -21,9 +23,36 @@ TBA
2123
- Configure Dapr to run [with docker](https://docs.dapr.io/operations/hosting/self-hosted/self-hosted-with-docker/).
2224
- Run `dapr init` to initialise `daprd` and the relevant containers.
2325

26+
_If deployment over Kubernetes is desired then check [these deployment instructions](https://docs.dapr.io/operations/hosting/kubernetes/kubernetes-deploy/)_.
27+
28+
## Configuration and environment variables
29+
30+
There are two main configuration files.
31+
- LLM configuration at `conf/llm.json`.
32+
- MCP configuration at `conf/mcp.json`.
33+
34+
There is already a default configuration provided for either of these.
35+
36+
There are Dapr related configuration files too.
37+
- The main Dapr application configuration at `dapr.yaml`. Change the various hosts and ports in this configuration if you want to use ports that are not the default ones.
38+
- Dapr telemetry configuration at `.dapr/config.yaml`.
39+
- Dapr hot-swappable component configuration files at `.dapr/components/`.
40+
41+
The following environment variables are also relevant but not essential, except for `OLLAMA_API_KEY`.
42+
- `OLLAMA_API_KEY`: The Ollama API key is required for MCP-based web-search and cloud hosted models on Ollama.
43+
- `APP_LOG_LEVEL`: The general log level of the DQA app. Defaults to `INFO`.
44+
- `DQA_MCP_SERVER_TRANSPORT`, `FASTMCP_HOST` and `FASTMCP_PORT`: These specify the transport type, the host and port for the built-in MCP server of DQA. The default values are `stdio`, `localhost` and `8000` respectively.
45+
- `LLM_CONFIG_FILE` and `MCP_CONFIG_FILE`: These specify where the LLM and MCP configurations These default to `conf/llm.json` and `conf/mcp.json` respectively.
46+
- [Gradio environment variables](https://www.gradio.app/guides/environment-variables) to configure the DQA web app. However, MCP server (not to be confused with DQA's built-in MCP server), SSR mode, API and public sharing will be disabled, irrespective of what is specified through the environment variables.
47+
- `BROWSER_STATE_SECRET`: This is the secret used by Gradio to encrypt the browser state data. The default value is `a2a_dapr_bstate_secret`.
48+
- `BROWSER_STATE_CHAT_HISTORIES`: This is the key in browser state used by Gradio to store the chat histories (local values).
49+
- `APP_DAPR_SVC_HOST` and `APP_DAPR_SVC_PORT`: The host and port at which Dapr actor service will listen on. These default to `127.0.0.1` and `32768`. Should you change these, you must change the corresponding information in `dapr.yaml`.
50+
- `DAPR_PUBSUB_NAME`: The configured name of the publish-subscribe component at `.dapr/components/pubsub.yaml`. Change this environment variable only if you change the corresponding pub-sub component configuration.
51+
- `APP_A2A_SRV_HOST` and `APP_MHQA_A2A_SRV_PORT`: The host and port at which A2A endpoint will be available. These default to `127.0.0.1` and `32770`. Should you change these, you must change the corresponding information in `dapr.yaml`.
52+
2453
## Usage
2554

26-
- Start the Dapr actor service and the A2A endpoints by running `./start_dapr_multi.sh`. (This will send the dapr sidecar processes in the background.)
55+
- Start the Dapr actor service and the A2A endpoints by running `./start_dapr_multi.sh`. (This will send the Dapr sidecar processes in the background.)
2756
- Invoke the A2A agent using JSON-RPC by calling `uv run dqa-cli --help` to learn about the various skills-based A2A endpoint invocations.
2857
- Or, start the Gradio web app by running `uv run dqa-web-app` and then browse to http://localhost:7860.
2958
- Once done, stop the dapr sidecars by running `./stop_dapr_multi.sh`.

src/dqa/__init__.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import logging
2+
from typing import ClassVar
23
from rich.logging import RichHandler
34
from environs import Env
45

6+
from marshmallow.validate import OneOf
7+
58
try:
69
from icecream import ic
710

@@ -13,8 +16,45 @@
1316
env = Env()
1417
env.read_env()
1518

19+
20+
class ParsedEnvVars:
21+
APP_LOG_LEVEL: str = env.str("APP_LOG_LEVEL", default="INFO").upper()
22+
DQA_MCP_SERVER_TRANSPORT: str = env.str(
23+
"DQA_MCP_SERVER_TRANSPORT",
24+
default="stdio",
25+
validate=OneOf(["stdio", "sse", "streamable-http"]),
26+
)
27+
FASTMCP_HOST: str = env.str("FASTMCP_HOST", default="localhost")
28+
FASTMCP_PORT: int = env.int("FASTMCP_PORT", default=8000)
29+
LLM_CONFIG_FILE: str = env.str("LLM_CONFIG_FILE", default="conf/llm.json")
30+
MCP_CONFIG_FILE: str = env.str("MCP_CONFIG_FILE", default="conf/mcp.json")
31+
APP_DAPR_SVC_HOST: str = env.str("APP_DAPR_SVC_HOST", default="127.0.0.1")
32+
APP_DAPR_SVC_PORT: int = env.int("APP_DAPR_SVC_PORT", default=32768)
33+
APP_A2A_SRV_HOST: str = env.str("APP_A2A_SRV_HOST", default="127.0.0.1")
34+
APP_MHQA_A2A_SRV_PORT: int = env.int("APP_MHQA_A2A_SRV_PORT", default=32770)
35+
APP_ECHO_A2A_SRV_PORT: int = env.int("APP_ECHO_A2A_SRV_PORT", default=32769)
36+
DAPR_PUBSUB_NAME: str = env.str("DAPR_PUBSUB_NAME", default="pubsub")
37+
MCP_SERVER_HOST: str = env.str("FASTMCP_HOST", default="localhost")
38+
MCP_SERVER_PORT: int = env.int("FASTMCP_PORT", default=8000)
39+
BROWSER_STATE_SECRET: str = env.str(
40+
"BROWSER_STATE_SECRET", default="a2a_dapr_bstate_secret"
41+
)
42+
BROWSER_STATE_CHAT_HISTORIES: str = env.str(
43+
"BROWSER_STATE_CHAT_HISTORIES", default="a2a_dapr_chat_histories"
44+
)
45+
46+
_instance: ClassVar = None
47+
48+
def __new__(cls: type["ParsedEnvVars"]) -> "ParsedEnvVars":
49+
if cls._instance is None:
50+
# Create instance using super().__new__ to bypass any recursion
51+
instance = super().__new__(cls)
52+
cls._instance = instance
53+
return cls._instance
54+
55+
1656
logging.basicConfig(
17-
level=env.str("APP_LOG_LEVEL", default="INFO").upper(),
57+
level=ParsedEnvVars().APP_LOG_LEVEL,
1858
format="%(message)s",
1959
datefmt="[%X]",
2060
handlers=[RichHandler()],

src/dqa/actor/mhqa.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from dapr.clients import DaprClient
2727
from pydantic import TypeAdapter
2828

29-
from dqa import env
29+
from dqa import ParsedEnvVars
3030
from dqa.actor import MHQAActorMethods
3131
from dqa.model.mhqa import MCPToolInvocation, MHQAResponse
3232

@@ -124,7 +124,7 @@ def __init__(self, ctx, actor_id):
124124
async def _on_activate(self) -> None:
125125
if not hasattr(self, "llm_config"):
126126
self.llm_config = {}
127-
llm_config_file = env.str("LLM_CONFIG_FILE", default="conf/llm.json")
127+
llm_config_file = ParsedEnvVars().LLM_CONFIG_FILE
128128
if os.path.exists(llm_config_file):
129129
with open(llm_config_file, "r") as f:
130130
self.llm_config = json.load(f)
@@ -133,7 +133,7 @@ async def _on_activate(self) -> None:
133133
self.mcp_features = []
134134
self.mcp_config = {}
135135
try:
136-
mcp_config_file = env.str("MCP_CONFIG_FILE", default="conf/mcp.json")
136+
mcp_config_file = ParsedEnvVars().MCP_CONFIG_FILE
137137
if os.path.exists(mcp_config_file):
138138
with open(mcp_config_file, "r") as f:
139139
self.mcp_config = json.load(f)
@@ -261,7 +261,7 @@ async def respond(self, data: dict) -> dict:
261261
tool_invocations=tool_invocations,
262262
)
263263
dc.publish_event(
264-
pubsub_name=env.str("DAPR_PUBSUB_NAME", default="pubsub"),
264+
pubsub_name=ParsedEnvVars().DAPR_PUBSUB_NAME,
265265
topic_name=f"topic-{self.__class__.__name__}-{self.id}-respond",
266266
data=response.model_dump_json().encode(),
267267
)

src/dqa/cli/a2a.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import httpx
1616

17-
from dqa import env
17+
from dqa import ParsedEnvVars
1818

1919
from a2a.types import Message
2020

@@ -68,11 +68,11 @@ def _interrupt_handler(self, signum: int, frame: FrameType | None):
6868

6969
def _initialize(self):
7070
logger.debug("Initialising A2A server URLs...")
71-
a2a_asgi_host = env.str("APP_A2A_SRV_HOST", "127.0.0.1")
72-
echo_a2a_asgi_port = env.int("APP_ECHO_A2A_SRV_PORT", 32769)
71+
a2a_asgi_host = ParsedEnvVars().APP_A2A_SRV_HOST
72+
echo_a2a_asgi_port = ParsedEnvVars().APP_ECHO_A2A_SRV_PORT
7373
self.echo_base_url = f"http://{a2a_asgi_host}:{echo_a2a_asgi_port}"
7474

75-
mhqa_a2a_asgi_port = env.int("APP_ECHO_A2A_SRV_PORT", 32770)
75+
mhqa_a2a_asgi_port = ParsedEnvVars().APP_MHQA_A2A_SRV_PORT
7676
self.mhqa_base_url = f"http://{a2a_asgi_host}:{mhqa_a2a_asgi_port}"
7777
logger.debug(f"Echo A2A base URL: {self.echo_base_url}")
7878
logger.debug(f"MHQA A2A base URL: {self.mhqa_base_url}")

src/dqa/mcp/primary.py

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import sys
44

55
from fastmcp import FastMCP
6-
from dqa import env
7-
from marshmallow.validate import OneOf
6+
from dqa import ParsedEnvVars
87

98
from dqa.mcp.ollama import app as ollama_mcp
109
from dqa.mcp.datetime import app as datetime_mcp
@@ -36,16 +35,6 @@ def server():
3635

3736

3837
def main(): # pragma: no cover
39-
MCP_SERVER_TRANSPORT = "DQA_MCP_SERVER_TRANSPORT"
40-
DEFAULT__MCP_SERVER_TRANSPORT = "streamable-http"
41-
ALLOWED__MCP_SERVER_TRANSPORT = ["stdio", "sse", "streamable-http"]
42-
43-
MCP_SERVER_HOST = "FASTMCP_HOST"
44-
DEFAULT__MCP_SERVER_HOST = "localhost"
45-
46-
MCP_SERVER_PORT = "FASTMCP_PORT"
47-
DEFAULT__MCP_SERVER_PORT = 8000
48-
4938
def sigint_handler(signal, frame):
5039
"""
5140
Signal handler to shut down the server gracefully.
@@ -56,11 +45,7 @@ def sigint_handler(signal, frame):
5645

5746
signal.signal(signal.SIGINT, sigint_handler)
5847

59-
transport_type = env.str(
60-
MCP_SERVER_TRANSPORT,
61-
default=DEFAULT__MCP_SERVER_TRANSPORT,
62-
validate=OneOf(ALLOWED__MCP_SERVER_TRANSPORT),
63-
)
48+
transport_type = ParsedEnvVars().DQA_MCP_SERVER_TRANSPORT
6449

6550
app = server()
6651

@@ -72,14 +57,8 @@ def sigint_handler(signal, frame):
7257
else:
7358
app.run(
7459
transport=transport_type,
75-
host=env.str(
76-
MCP_SERVER_HOST,
77-
default=DEFAULT__MCP_SERVER_HOST,
78-
),
79-
port=env.int(
80-
MCP_SERVER_PORT,
81-
default=DEFAULT__MCP_SERVER_PORT,
82-
),
60+
host=ParsedEnvVars().MCP_SERVER_HOST,
61+
port=ParsedEnvVars().MCP_SERVER_PORT,
8362
uvicorn_config={
8463
"timeout_graceful_shutdown": 5, # seconds
8564
},

src/dqa/server/dapr.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from contextlib import asynccontextmanager
1414

15-
from dqa import env
15+
from dqa import ParsedEnvVars
1616

1717

1818
@asynccontextmanager
@@ -48,8 +48,8 @@ async def lifespan(app: FastAPI):
4848
def main():
4949
uvicorn.run(
5050
app,
51-
host=env.str("APP_HOST", "127.0.0.1"),
52-
port=env.int("APP_DAPR_SVC_PORT", 32768),
51+
host=ParsedEnvVars().APP_DAPR_SVC_HOST,
52+
port=ParsedEnvVars().APP_DAPR_SVC_PORT,
5353
)
5454

5555

src/dqa/server/echo_a2a.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
AgentSkill,
1414
)
1515

16-
from dqa import env
16+
from dqa import ParsedEnvVars
1717
from dqa.executor.echo_task import EchoAgentExecutor
1818
from dqa.model.echo_task import EchoAgentSkills
1919

@@ -27,8 +27,8 @@ def sigint_handler(signal, frame):
2727
# This is absolutely necessary to exit the program
2828
sys.exit(0)
2929

30-
_a2a_uvicorn_host = env.str("APP_A2A_SRV_HOST", "127.0.0.1")
31-
_a2a_uvicorn_port = env.int("APP_ECHO_A2A_SRV_PORT", 32769)
30+
_a2a_uvicorn_host = ParsedEnvVars().APP_A2A_SRV_HOST
31+
_a2a_uvicorn_port = ParsedEnvVars().APP_ECHO_A2A_SRV_PORT
3232
signal.signal(signal.SIGINT, sigint_handler)
3333

3434
echo_skill = AgentSkill(

src/dqa/server/mhqa_a2a.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
AgentSkill,
1414
)
1515

16-
from dqa import env
16+
from dqa import ParsedEnvVars
1717
from dqa.executor.mhqa import MHQAAgentExecutor
1818
from dqa.model.mhqa import MHQAAgentSkills
1919

@@ -27,8 +27,8 @@ def sigint_handler(signal, frame):
2727
# This is absolutely necessary to exit the program
2828
sys.exit(0)
2929

30-
_a2a_uvicorn_host = env.str("APP_A2A_SRV_HOST", "127.0.0.1")
31-
_a2a_uvicorn_port = env.int("APP_MHQA_A2A_SRV_PORT", 32770)
30+
_a2a_uvicorn_host = ParsedEnvVars().APP_A2A_SRV_HOST
31+
_a2a_uvicorn_port = ParsedEnvVars().APP_MHQA_A2A_SRV_PORT
3232
signal.signal(signal.SIGINT, sigint_handler)
3333

3434
respond_skill = AgentSkill(

src/dqa/web/gradio.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import httpx
1414
from pydantic import TypeAdapter
15-
from dqa import env
15+
from dqa import ParsedEnvVars
1616
import gradio as gr
1717

1818
from dqa.client.a2a_mixin import A2AClientMixin
@@ -47,8 +47,8 @@ class GradioApp(A2AClientMixin):
4747

4848
def __init__(self):
4949
# self.ui = None
50-
self._mhqa_a2a_uvicorn_host = env.str("APP_A2A_SRV_HOST", "127.0.0.1")
51-
self._mhqa_a2a_uvicorn_port = env.int("APP_ECHO_A2A_SRV_PORT", 32770)
50+
self._mhqa_a2a_uvicorn_host = ParsedEnvVars().APP_A2A_SRV_HOST
51+
self._mhqa_a2a_uvicorn_port = ParsedEnvVars().APP_MHQA_A2A_SRV_PORT
5252
self._mhqa_a2a_base_url = (
5353
f"http://{self._mhqa_a2a_uvicorn_host}:{self._mhqa_a2a_uvicorn_port}"
5454
)
@@ -82,7 +82,7 @@ def convert_mhqa_response_to_chat_messages(self, response: MHQAResponse):
8282
content=f"Inputs: {tool_invocation.input}\nOutputs: {tool_invocation.output}\nMetadata: {tool_invocation.metadata}",
8383
metadata={
8484
"parent_id": message_id,
85-
"title": f"Tool used: {tool_invocation.name}",
85+
"title": f"⚙️ Tool used: {tool_invocation.name}",
8686
},
8787
)
8888
)
@@ -129,8 +129,8 @@ def component_main_content(self):
129129
gr.Markdown(GradioApp._MD_EU_AI_ACT_TRANSPARENCY)
130130
with gr.Column(scale=3):
131131
bstate_chat_histories = gr.BrowserState(
132-
storage_key="a2a_dapr_chat_histories",
133-
secret="a2a_dapr_bstate_secret",
132+
storage_key=ParsedEnvVars().BROWSER_STATE_CHAT_HISTORIES,
133+
secret=ParsedEnvVars().BROWSER_STATE_SECRET,
134134
)
135135
chatbot = gr.Chatbot(
136136
type="messages",
@@ -375,7 +375,7 @@ async def btn_echo_clicked(
375375
MHQAResponse(
376376
thread_id=selected_chat_id,
377377
user_input=txt_input,
378-
agent_output="...",
378+
agent_output="🤔 thinking, please wait...",
379379
)
380380
)
381381
)
@@ -445,7 +445,11 @@ async def btn_echo_clicked(
445445
return component
446446

447447
def construct_ui(self):
448-
with gr.Blocks(fill_width=True, fill_height=True) as self.ui:
448+
with gr.Blocks(
449+
fill_width=True,
450+
fill_height=True,
451+
theme=gr.themes.Monochrome(font="ui-sans-serif"),
452+
) as self.ui:
449453
gr.set_static_paths(
450454
paths=[
451455
GradioApp._APP_LOGO_PATH,
@@ -482,7 +486,12 @@ def sigint_handler(signal, frame):
482486
signal.signal(signal.SIGINT, sigint_handler)
483487

484488
try:
485-
app.construct_ui().queue().launch(share=False, ssr_mode=False, show_api=False)
489+
app.construct_ui().queue().launch(
490+
share=False,
491+
ssr_mode=False,
492+
show_api=False,
493+
mcp_server=False,
494+
)
486495
except InterruptedError:
487496
logger.warning("Gradio server interrupted, shutting down...")
488497
except Exception as e:

0 commit comments

Comments
 (0)