From ecbc8385d590cf8f52d437796ff91d6e55bfd55e Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 27 Nov 2025 22:26:02 -0500 Subject: Add Omni/Fact.hs core module with CRUD operations The Omni/Fact.hs module is complete with CRUD operations: - **createFact**: Create a new fact with project, content, related files - **getFact**: Retrieve a fact by ID - **getAllFacts**: Get all facts from the database - **getFactsByProject**: Get facts filtered by project - **getFactsByFile**: Get facts related to a specific file - **updateFact**: Update an existing fact's content, related files, and - **deleteFact**: Delete a fact by ID The module properly re-exports the `Fact` data type from `Omni.Task.Core Task-Id: t-158.2 --- Omni/Task/Core.hs | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) (limited to 'Omni/Task/Core.hs') diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 4ff0f5f..d15ed8e 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -103,6 +103,18 @@ data TaskActivity = TaskActivity } deriving (Show, Eq, Generic) +-- Fact for knowledge base +data Fact = Fact + { factId :: Maybe Int, + factProject :: Text, + factContent :: Text, + factRelatedFiles :: [Text], + factSourceTask :: Maybe Text, + factConfidence :: Double, + factCreatedAt :: UTCTime + } + deriving (Show, Eq, Generic) + instance ToJSON TaskType instance FromJSON TaskType @@ -143,6 +155,10 @@ instance ToJSON TaskActivity instance FromJSON TaskActivity +instance ToJSON Fact + +instance FromJSON Fact + -- HTTP API Instances (for Servant query params) instance FromHttpApiData Status where @@ -268,6 +284,38 @@ instance SQL.ToRow TaskActivity where SQL.toField (activityTokensUsed a) ] +instance SQL.FromRow Fact where + fromRow = do + fid <- SQL.field + proj <- SQL.field + content <- SQL.field + (relatedFilesJson :: String) <- SQL.field + sourceTask <- SQL.field + confidence <- SQL.field + createdAt <- SQL.field + let relatedFiles = fromMaybe [] (decode (BLC.pack relatedFilesJson)) + pure + Fact + { factId = fid, + factProject = proj, + factContent = content, + factRelatedFiles = relatedFiles, + factSourceTask = sourceTask, + factConfidence = confidence, + factCreatedAt = createdAt + } + +instance SQL.ToRow Fact where + toRow f = + [ SQL.toField (factId f), + SQL.toField (factProject f), + SQL.toField (factContent f), + SQL.toField (BLC.unpack (encode (factRelatedFiles f))), + SQL.toField (factSourceTask f), + SQL.toField (factConfidence f), + SQL.toField (factCreatedAt f) + ] + -- | Case-insensitive ID comparison matchesId :: Text -> Text -> Bool matchesId id1 id2 = normalizeId id1 == normalizeId id2 @@ -375,6 +423,17 @@ initTaskDb = do \ tokens_used INTEGER, \ \ FOREIGN KEY (task_id) REFERENCES tasks(id) \ \)" + SQL.execute_ + conn + "CREATE TABLE IF NOT EXISTS facts (\ + \ id INTEGER PRIMARY KEY AUTOINCREMENT, \ + \ project TEXT NOT NULL, \ + \ fact TEXT NOT NULL, \ + \ related_files TEXT NOT NULL, \ + \ source_task TEXT, \ + \ confidence REAL NOT NULL, \ + \ created_at DATETIME DEFAULT CURRENT_TIMESTAMP \ + \)" runMigrations conn -- | Run schema migrations to add missing columns to existing tables @@ -383,6 +442,7 @@ runMigrations conn = do migrateTable conn "task_activity" taskActivityColumns migrateTable conn "tasks" tasksColumns migrateTable conn "retry_context" retryContextColumns + migrateTable conn "facts" factsColumns -- | Expected columns for task_activity table (name, type, nullable) taskActivityColumns :: [(Text, Text)] @@ -427,6 +487,18 @@ retryContextColumns = ("notes", "TEXT") ] +-- | Expected columns for facts table +factsColumns :: [(Text, Text)] +factsColumns = + [ ("id", "INTEGER"), + ("project", "TEXT"), + ("fact", "TEXT"), + ("related_files", "TEXT"), + ("source_task", "TEXT"), + ("confidence", "REAL"), + ("created_at", "DATETIME") + ] + -- | Migrate a table by adding any missing columns migrateTable :: SQL.Connection -> Text -> [(Text, Text)] -> IO () migrateTable conn tableName expectedCols = do @@ -1212,3 +1284,52 @@ updateRetryNotes tid notes = do } Just ctx -> setRetryContext ctx {retryNotes = Just notes} + +-- Fact management + +-- | Save a fact to the database +saveFact :: Fact -> IO Int +saveFact f = + withDb <| \conn -> do + let filesJson = T.pack <| BLC.unpack <| encode (factRelatedFiles f) + SQL.execute + conn + "INSERT INTO facts (project, fact, related_files, source_task, confidence, created_at) \ + \VALUES (?, ?, ?, ?, ?, ?)" + (factProject f, factContent f, filesJson, factSourceTask f, factConfidence f, factCreatedAt f) + [SQL.Only factIdVal] <- SQL.query_ conn "SELECT last_insert_rowid()" :: IO [SQL.Only Int] + pure factIdVal + +-- | Load all facts from the database +loadFacts :: IO [Fact] +loadFacts = + withDb <| \conn -> + SQL.query_ + conn + "SELECT id, project, fact, related_files, source_task, confidence, created_at FROM facts" + +-- | Get facts for a specific project +getFactsForProject :: Text -> IO [Fact] +getFactsForProject proj = + withDb <| \conn -> + SQL.query + conn + "SELECT id, project, fact, related_files, source_task, confidence, created_at \ + \FROM facts WHERE project = ? ORDER BY confidence DESC" + (SQL.Only proj) + +-- | Get facts related to a specific file +getFactsForFile :: Text -> IO [Fact] +getFactsForFile filePath = + withDb <| \conn -> + SQL.query + conn + "SELECT id, project, fact, related_files, source_task, confidence, created_at \ + \FROM facts WHERE related_files LIKE ? ORDER BY confidence DESC" + (SQL.Only ("%" <> filePath <> "%")) + +-- | Delete a fact by ID +deleteFact :: Int -> IO () +deleteFact fid = + withDb <| \conn -> + SQL.execute conn "DELETE FROM facts WHERE id = ?" (SQL.Only fid) -- cgit v1.2.3