Privacy-first document Q&A with local RAG
This guide covers development setup, contributing guidelines, and code structure for SafeQueryAI.
# Clone repository
git clone https://github.com/yourusername/SafeQueryAI.git
cd SafeQueryAI
# Start Ollama
ollama serve
# In another terminal, pull models
ollama pull nomic-embed-text
ollama pull llama3.2
Terminal 1 — Backend:
cd backend
dotnet watch run
Terminal 2 — Frontend:
cd frontend
npm install
npm run dev
Application should be available at http://localhost:5173
backend/
├── Controllers/ # HTTP endpoints
│ ├── FilesController.cs
│ ├── QuestionsController.cs
│ ├── SessionsController.cs
│ └── HealthController.cs
├── Services/ # Business logic
│ ├── DocumentIndexingService.cs
│ ├── OllamaService.cs
│ ├── QuestionAnsweringService.cs
│ ├── SessionService.cs
│ ├── TextExtractionService.cs
│ ├── VectorStoreService.cs
│ └── Interfaces/ # Service contracts
├── Models/ # Domain entities
│ ├── SessionInfo.cs
│ ├── DocumentChunk.cs
│ └── StoredFileInfo.cs
├── Contracts/ # DTOs for API
│ ├── AskQuestionRequest.cs
│ ├── AnswerStreamChunk.cs
│ └── ...
├── Program.cs # DI and middleware setup
└── appsettings.json # Configuration
frontend/
├── src/
│ ├── components/ # React components
│ │ ├── App.tsx # Root component
│ │ ├── QuestionForm.tsx
│ │ ├── FileUploadPanel.tsx
│ │ └── AnswerPanel.tsx
│ ├── services/ # API client
│ │ └── api.ts # Typed API wrapper
│ ├── types/ # TypeScript interfaces
│ │ └── api.ts # API types
│ ├── styles/ # CSS files
│ └── main.tsx # Entry point
├── vite.config.ts # Build config
├── tsconfig.json # TS config
└── package.json # Dependencies
async/await consistentlyILogger<T> from DIExample:
public class DocumentIndexingService : IDocumentIndexingService
{
private readonly ILogger<DocumentIndexingService> _logger;
private readonly IOllamaService _ollamaService;
public DocumentIndexingService(
ILogger<DocumentIndexingService> logger,
IOllamaService ollamaService)
{
_logger = logger;
_ollamaService = ollamaService;
}
public async Task<List<DocumentChunk>> IndexDocumentAsync(string filePath)
{
try
{
_logger.LogInformation("Indexing document: {FilePath}", filePath);
// Implementation
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to index document");
throw;
}
}
}
React.FC<Props>Example:
interface QuestionFormProps {
sessionId: string;
onSubmit: (question: string) => void;
isLoading: boolean;
}
export const QuestionForm: React.FC<QuestionFormProps> = ({
sessionId,
onSubmit,
isLoading
}) => {
const [question, setQuestion] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(question);
setQuestion('');
};
return (
<form onSubmit={handleSubmit}>
<input
value={question}
onChange={(e) => setQuestion(e.target.value)}
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
Ask
</button>
</form>
);
};
Create test files in backend/Tests/:
[TestFixture]
public class DocumentIndexingServiceTests
{
private Mock<IOllamaService> _ollamaServiceMock;
private DocumentIndexingService _service;
[SetUp]
public void Setup()
{
_ollamaServiceMock = new Mock<IOllamaService>();
_service = new DocumentIndexingService(_ollamaServiceMock.Object);
}
[Test]
public async Task IndexDocument_WithValidFile_ReturnsChunks()
{
// Arrange
var filePath = "test.pdf";
// Act
var result = await _service.IndexDocumentAsync(filePath);
// Assert
Assert.IsNotEmpty(result);
}
}
Run tests:
dotnet test backend/
Add Vitest to frontend/package.json:
npm install -D vitest @testing-library/react @testing-library/jest-dom
Create test file:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QuestionForm } from './QuestionForm';
describe('QuestionForm', () => {
it('submits question when form is submitted', async () => {
const onSubmit = vi.fn();
render(
<QuestionForm
sessionId="test-id"
onSubmit={onSubmit}
isLoading={false}
/>
);
const input = screen.getByRole('textbox');
await userEvent.type(input, 'What is AI?');
await userEvent.click(screen.getByRole('button'));
expect(onSubmit).toHaveBeenCalledWith('What is AI?');
});
});
Run tests:
npm run test
Contracts/:
public class MyRequest { }
Contracts/:
public class MyResponse { }
[HttpPost("my-endpoint")]
public async Task<MyResponse> MyEndpoint(MyRequest request)
{
// Implementation
}
services/api.ts:
myEndpoint: (data: MyRequest): Promise<MyResponse> =>
fetch('/api/my-endpoint', { method: 'POST', body: JSON.stringify(data) })
src/components/:
interface MyComponentProps { }
export const MyComponent: React.FC<MyComponentProps> = (props) => {
return <div>Component</div>;
};
App.tsx:
import { MyComponent } from './components/MyComponent';
styles/app.cssBackend:
cd backend
dotnet add package NameOfPackage
Frontend:
cd frontend
npm install package-name
Create .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/backend/bin/Debug/net8.0/SafeQueryAI.Api.dll",
"args": [],
"cwd": "${workspaceFolder}/backend",
"stopAtEntry": false,
"serverReadyAction": {
"port": 5000,
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
}
}
]
}
Use React DevTools browser extension or:
// In component
console.log('Debug info:', value);
debugger; // Pauses execution
Backend:
_logger.LogInformation("Info message");
_logger.LogWarning("Warning: {Details}", details);
_logger.LogError(ex, "Error occurred");
Frontend:
console.log('Message');
console.debug('Debug info', obj);
console.error('Error', error);
git checkout -b feature/my-featuredotnet test and npm testgit commit -m "Add feature: description"git push origin feature/my-featureBackend:
cd backend
dotnet publish -c Release -o ../publish
Frontend:
cd frontend
npm run build
Update version in:
backend/SafeQueryAI.Api.csproj: <Version>1.0.0</Version>frontend/package.json: "version": "1.0.0"Tag and push:
git tag v1.0.0
git push origin v1.0.0
dotnet-trace