-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
187 lines (158 loc) · 6.37 KB
/
Copy pathProgram.cs
File metadata and controls
187 lines (158 loc) · 6.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
using Microsoft.EntityFrameworkCore;
using NotesApp.Api.Data;
using NotesApp.Api.Services;
using NotesApp.Core.Models;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Services.AddDbContext<NotesDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("NotesDb")));
// Typed HttpClient pointing at the local Ollama server. Configurable so we can
// point it elsewhere once the API is no longer co-located with Ollama.
builder.Services.AddHttpClient<OllamaAiService>(client =>
client.BaseAddress = new Uri(
builder.Configuration["Ollama:BaseUrl"] ?? "http://localhost:11434"));
builder.Services.AddScoped<NoteEmbeddingService>();
var app = builder.Build();
// Apply any pending migrations at startup so a fresh clone (e.g. on Windows via
// LocalDB) gets its schema created automatically, without running EF commands
// by hand. Safe to run every start: it's a no-op once the DB is up to date.
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<NotesDbContext>();
db.Database.Migrate();
}
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
// Returns every note, including deleted ones, so clients can mirror deletions during sync.
app.MapGet("/api/notes", async (NotesDbContext db) =>
await db.Notes
.OrderByDescending(n => n.UpdatedAt)
.ToListAsync())
.WithName("GetNotes");
// Upsert: creates the note if it doesn't exist yet, otherwise applies the
// incoming version only if it's newer than what's stored (last-write-wins).
app.MapPut("/api/notes/{id}", async (Guid id, Note incoming, NotesDbContext db) =>
{
var existing = await db.Notes.FindAsync(id);
if (existing is null)
{
incoming.Id = id;
db.Notes.Add(incoming);
}
else if (incoming.UpdatedAt > existing.UpdatedAt)
{
// The content changed, so the cached embedding no longer describes this
// note; clear it and the next related-notes query will recompute it.
if (existing.Title != incoming.Title || existing.Body != incoming.Body)
{
existing.Embedding = null;
}
// Copy EVERY client-owned field. When a field is added to Note, it must
// be added here too, or updates to it will silently never sync.
existing.Title = incoming.Title;
existing.Body = incoming.Body;
existing.FolderId = incoming.FolderId;
existing.Tags = incoming.Tags;
existing.IsFavorite = incoming.IsFavorite;
existing.CreatedAt = incoming.CreatedAt;
existing.UpdatedAt = incoming.UpdatedAt;
existing.IsDeleted = incoming.IsDeleted;
}
await db.SaveChangesAsync();
return Results.Ok(existing ?? incoming);
})
.WithName("UpsertNote");
// Folders sync the same way notes do: return all (including deleted) so clients
// can mirror deletions, and upsert with last-write-wins by UpdatedAt.
app.MapGet("/api/folders", async (NotesDbContext db) =>
await db.Folders
.OrderBy(f => f.Name)
.ToListAsync())
.WithName("GetFolders");
app.MapPut("/api/folders/{id}", async (Guid id, Folder incoming, NotesDbContext db) =>
{
var existing = await db.Folders.FindAsync(id);
if (existing is null)
{
incoming.Id = id;
db.Folders.Add(incoming);
}
else if (incoming.UpdatedAt > existing.UpdatedAt)
{
existing.Name = incoming.Name;
existing.CreatedAt = incoming.CreatedAt;
existing.UpdatedAt = incoming.UpdatedAt;
existing.IsDeleted = incoming.IsDeleted;
}
await db.SaveChangesAsync();
return Results.Ok(existing ?? incoming);
})
.WithName("UpsertFolder");
// Sends a note's text to the local Ollama model and returns a short summary.
app.MapPost("/api/ai/summarize", async (SummarizeRequest request, OllamaAiService ai) =>
{
if (string.IsNullOrWhiteSpace(request.Text))
{
return Results.BadRequest("Nothing to summarize.");
}
var summary = await ai.SummarizeAsync(request.Text);
return Results.Ok(new SummarizeResponse(summary));
})
.WithName("Summarize");
// Drafts a brand-new note (title + body) from a short instruction.
app.MapPost("/api/ai/draft", async (DraftRequest request, OllamaAiService ai) =>
{
if (string.IsNullOrWhiteSpace(request.Prompt))
{
return Results.BadRequest("Prompt is required.");
}
var draft = await ai.DraftNoteAsync(request.Prompt);
return Results.Ok(new DraftResponse(draft.Title, draft.Body));
})
.WithName("DraftNote");
// Rewrites existing note text per an instruction (edit, tidy, or "make a table").
app.MapPost("/api/ai/rewrite", async (RewriteRequest request, OllamaAiService ai) =>
{
if (string.IsNullOrWhiteSpace(request.Text) || string.IsNullOrWhiteSpace(request.Instruction))
{
return Results.BadRequest("Text and instruction are both required.");
}
var result = await ai.RewriteAsync(request.Text, request.Instruction);
return Results.Ok(new RewriteResponse(result));
})
.WithName("RewriteNote");
// Short inline continuation of the given text, for the editor's ghost-text autocomplete.
app.MapPost("/api/ai/complete", async (CompleteRequest request, OllamaAiService ai) =>
{
if (string.IsNullOrWhiteSpace(request.Text))
{
return Results.Ok(new CompleteResponse(string.Empty));
}
var suggestion = await ai.CompleteAsync(request.Text);
return Results.Ok(new CompleteResponse(suggestion));
})
.WithName("Complete");
// Finds notes semantically related to the given text (used to link related notes).
app.MapPost("/api/ai/related", async (RelatedRequest request, NoteEmbeddingService embeddings) =>
{
var related = await embeddings.FindRelatedAsync(request.NoteId, request.Text);
return Results.Ok(related);
})
.WithName("RelatedNotes");
app.Run();
// Request/response shapes for the AI endpoints.
record SummarizeRequest(string Text);
record SummarizeResponse(string Summary);
record DraftRequest(string Prompt);
record DraftResponse(string Title, string Body);
record RewriteRequest(string Text, string Instruction);
record RewriteResponse(string Result);
record CompleteRequest(string Text);
record CompleteResponse(string Suggestion);
record RelatedRequest(Guid NoteId, string Text);