Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Maintained by **Quantera.ai**.
| Database/Auth | Supabase Auth + Postgres |
| Storage | Cloudflare R2 / S3-compatible storage, with local fallback for development |
| AI Providers | Gemini, Anthropic, OpenRouter-compatible models |
| Legal research | [LegalDataHunter](https://legaldatahunter.com) case law & legislation across 178 jurisdictions |
| Legal research | [LegalDataHunter](https://legaldatahunter.com) (case law & legislation across 178 jurisdictions) and/or [Vaquill](https://www.vaquill.ai/legal-api) (AI Q&A across US primary law and Indian case law) |
| Document tooling | LibreOffice for DOC/DOCX conversion |

---
Expand Down Expand Up @@ -105,13 +105,23 @@ ANTHROPIC_API_KEY=your-anthropic-key
OPENROUTER_API_KEY=your-openrouter-key
RESEND_API_KEY=your-resend-key

# LegalDataHunter — required for the "Sources" panel and inline legal-research
# citations in the assistant chat. This is a paid third-party API. You MUST use
# YOUR OWN key — usage is billed against the key holder's account, and Open
# Specter ships no fallback. Sign up at https://legaldatahunter.com to get one.
# If this variable is unset, the Sources feature is silently disabled and the
# rest of the app keeps working.
# Legal-research providers. Set whichever one(s) you want to enable; both can
# be enabled simultaneously. The frontend chooses which to call. If neither is
# set, the Sources feature and inline legal-research citations are silently
# disabled and the rest of the app keeps working.
#
# Each is a paid third-party API. You MUST use YOUR OWN key. Usage is billed
# against the key holder's account; Open Specter ships no fallback.

# LegalDataHunter: case law and legislation across 178 jurisdictions.
# Sign up at https://legaldatahunter.com.
LEGAL_DATA_HUNTER_API_KEY=your-legaldatahunter-key

# Vaquill: AI-grounded Q&A across US primary law (Constitution, USC, CFR,
# Federal Rules, 50-state codes, Executive Orders since 2015) plus Indian
# case law (31M+ judgments) and citation-graph traversal. Sign up at
# https://www.vaquill.ai/legal-api. Key format: vq_key_...
VAQUILL_API_KEY=vq_key_your_vaquill_key
```

### Frontend
Expand Down Expand Up @@ -333,7 +343,7 @@ deployments.
## Third-party API keys are your responsibility

Open Specter integrates with paid third-party services (Supabase, Cloudflare
R2, Gemini, Anthropic, OpenRouter, Resend, LegalDataHunter). Open Specter
R2, Gemini, Anthropic, OpenRouter, Resend, LegalDataHunter, Vaquill). Open Specter
ships **no shared/upstream fallback** for any of them — every self-host must
provide its own credentials in `backend/.env`. Usage of each integration is
billed against the key holder's account; the project maintainers cannot be
Expand Down
13 changes: 13 additions & 0 deletions open-specter-main/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,16 @@ GEMINI_API_KEY=your-gemini-key
ANTHROPIC_API_KEY=your-anthropic-key
OPENROUTER_API_KEY=your-openrouter-key
RESEND_API_KEY=your-resend-key

# Legal-research providers. Set whichever one(s) you want to enable.
# Both can be enabled simultaneously; the frontend chooses which to call.

# LegalDataHunter: case law and legislation across 178 jurisdictions. Paid API.
# Sign up at https://legaldatahunter.com.
LEGAL_DATA_HUNTER_API_KEY=your-legaldatahunter-key

# Vaquill: AI-grounded Q&A across US primary law (Constitution, USC, CFR,
# Federal Rules, 50-state codes, Executive Orders since 2015) plus Indian
# case law and citation-graph traversal. Paid API. Sign up at
# https://www.vaquill.ai/legal-api.
VAQUILL_API_KEY=vq_key_your_vaquill_key
3 changes: 3 additions & 0 deletions open-specter-main/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { userRouter } from "./routes/user";
import { downloadsRouter } from "./routes/downloads";
import { activityRouter } from "./routes/activity";
import { legalDataRouter } from "./routes/legalData";
import { vaquillRouter } from "./routes/vaquill";

const app = express();
const PORT = process.env.PORT ?? 3001;
Expand All @@ -35,6 +36,7 @@ app.use("/users", userRouter);
app.use("/download", downloadsRouter);
app.use("/activity", activityRouter);
app.use("/legal-data", legalDataRouter);
app.use("/vaquill", vaquillRouter);

app.get("/health", (_req, res) => res.json({ ok: true }));

Expand All @@ -61,6 +63,7 @@ function logIntegrationStatus(): void {
console.log(` Resend (email) : ${status(has(process.env.RESEND_API_KEY))}`);
console.log(` Cloudflare R2 : ${status(has(process.env.R2_ENDPOINT_URL) && has(process.env.R2_ACCESS_KEY_ID))}`);
console.log(` LegalDataHunter : ${status(has(process.env.LEGAL_DATA_HUNTER_API_KEY))}`);
console.log(` Vaquill : ${status(has(process.env.VAQUILL_API_KEY))}`);
if (!has(process.env.LEGAL_DATA_HUNTER_API_KEY)) {
console.warn(
" ⚠ LEGAL_DATA_HUNTER_API_KEY is not set. The 'Sources' panel and inline\n" +
Expand Down
125 changes: 125 additions & 0 deletions open-specter-main/backend/src/routes/vaquill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Router } from "express";
import { requireAuth } from "../middleware/auth";

export const vaquillRouter = Router();

const VAQUILL_BASE = "https://api.vaquill.ai/api/v1";

/**
* Vaquill (https://www.vaquill.ai/legal-api) is an alternative legal-research
* provider. It exposes AI-grounded Q&A across US primary law (Constitution,
* USC, CFR, Federal Rules of Procedure, all 50 state statute codes, state
* constitutions, state court rules, Executive Orders since 2015) plus Indian
* case law (31M+ judgments) and citation-graph traversal.
*
* This router sits alongside legalData.ts (LegalDataHunter) so operators can
* pick whichever provider suits their corpus needs. Both can be enabled
* simultaneously; the frontend chooses which to call.
*
* Configure VAQUILL_API_KEY in backend/.env. Key format: vq_key_...
*/

function resolveVaquillKey(): string | null {
const key = process.env.VAQUILL_API_KEY;
return typeof key === "string" && key.trim() ? key.trim() : null;
}

/**
* POST /vaquill/ask: AI-grounded legal Q&A.
*
* Body:
* - question (required, string)
* - countryCode (optional, "US" | "IN", defaults upstream)
* - usState (optional, e.g. "tx")
* - mode (optional, "standard" | "deep")
*/
vaquillRouter.post("/ask", requireAuth, async (req, res) => {
const apiKey = resolveVaquillKey();
if (!apiKey) {
return void res.status(400).json({
detail: "Vaquill API key is not configured.",
});
}

const { question, countryCode, usState, mode } = req.body ?? {};
if (typeof question !== "string" || !question.trim()) {
return void res
.status(400)
.json({ detail: "`question` is required." });
}

const payload: Record<string, unknown> = { question: question.trim() };
if (countryCode) payload.countryCode = countryCode;
if (usState) payload.usState = usState;
if (mode) payload.mode = mode;

try {
const upstream = await fetch(`${VAQUILL_BASE}/ask`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
const body = await upstream.text();
res.status(upstream.status)
.setHeader("content-type", "application/json")
.send(body);
} catch (err) {
console.error("[vaquill] ask upstream error", err);
res.status(502).json({
detail: "Vaquill upstream failure.",
});
}
});

/**
* POST /vaquill/statutes/search: Search US primary law and Indian acts.
*
* Body:
* - q (required, string)
* - corpusType (optional, one of: USC, CFR, STATE, CONSTITUTION, FEDERAL_RULES, STATE_CONSTITUTION, STATE_RULES, EXECUTIVE_ACTION)
* - state (optional, ISO state code e.g. "tx" to restrict STATE-scoped corpora)
* - titleNumber (optional, integer)
* - limit (optional, integer)
*/
vaquillRouter.post("/statutes/search", requireAuth, async (req, res) => {
const apiKey = resolveVaquillKey();
if (!apiKey) {
return void res.status(400).json({
detail: "Vaquill API key is not configured.",
});
}

const { q, corpusType, state, titleNumber, limit } = req.body ?? {};
if (typeof q !== "string" || !q.trim()) {
return void res.status(400).json({ detail: "`q` is required." });
}

const payload: Record<string, unknown> = { q: q.trim() };
if (corpusType) payload.corpusType = corpusType;
if (state) payload.state = state;
if (titleNumber) payload.titleNumber = titleNumber;
if (limit) payload.limit = limit;

try {
const upstream = await fetch(`${VAQUILL_BASE}/statutes/search`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
const body = await upstream.text();
res.status(upstream.status)
.setHeader("content-type", "application/json")
.send(body);
} catch (err) {
console.error("[vaquill] statutes search upstream error", err);
res.status(502).json({
detail: "Vaquill upstream failure.",
});
}
});