summaryrefslogtreecommitdiff
path: root/Omni
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-11-27 22:26:02 -0500
committerBen Sima <ben@bensima.com>2025-11-27 22:26:02 -0500
commitecbc8385d590cf8f52d437796ff91d6e55bfd55e (patch)
treead12740de96a6be60e990db7150f670fab2dae2b /Omni
parentbfe8be3a1a85aa2104bb8e9f2fb2c5147d9fbcaf (diff)
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
Diffstat (limited to 'Omni')
-rw-r--r--Omni/Fact.hs81
-rw-r--r--Omni/Task/Core.hs121
2 files changed, 202 insertions, 0 deletions
diff --git a/Omni/Fact.hs b/Omni/Fact.hs
new file mode 100644
index 0000000..57db7fc
--- /dev/null
+++ b/Omni/Fact.hs
@@ -0,0 +1,81 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- | Fact module for the Jr knowledge base.
+--
+-- Facts are pieces of knowledge learned during task execution that can
+-- inform future work on similar tasks or files.
+module Omni.Fact
+ ( Fact (..),
+ createFact,
+ getFact,
+ getAllFacts,
+ getFactsByProject,
+ getFactsByFile,
+ updateFact,
+ deleteFact,
+ )
+where
+
+import Alpha
+import Data.Aeson (encode)
+import qualified Data.ByteString.Lazy.Char8 as BLC
+import qualified Data.Text as Text
+import Data.Time (getCurrentTime)
+import qualified Database.SQLite.Simple as SQL
+import Omni.Task.Core
+ ( Fact (..),
+ getFactsForFile,
+ getFactsForProject,
+ loadFacts,
+ saveFact,
+ withDb,
+ )
+import qualified Omni.Task.Core as TaskCore
+
+-- | Create a new fact and return its ID.
+createFact :: Text -> Text -> [Text] -> Maybe Text -> Double -> IO Int
+createFact project content relatedFiles sourceTask confidence = do
+ now <- getCurrentTime
+ let fact =
+ Fact
+ { factId = Nothing,
+ factProject = project,
+ factContent = content,
+ factRelatedFiles = relatedFiles,
+ factSourceTask = sourceTask,
+ factConfidence = confidence,
+ factCreatedAt = now
+ }
+ saveFact fact
+
+-- | Get a fact by its ID.
+getFact :: Int -> IO (Maybe Fact)
+getFact fid = do
+ facts <- getAllFacts
+ pure <| find (\f -> factId f == Just fid) facts
+
+-- | Get all facts from the database.
+getAllFacts :: IO [Fact]
+getAllFacts = loadFacts
+
+-- | Get facts for a specific project.
+getFactsByProject :: Text -> IO [Fact]
+getFactsByProject = getFactsForProject
+
+-- | Get facts related to a specific file.
+getFactsByFile :: Text -> IO [Fact]
+getFactsByFile = getFactsForFile
+
+-- | Update an existing fact.
+updateFact :: Int -> Text -> [Text] -> Double -> IO ()
+updateFact fid content relatedFiles confidence =
+ withDb <| \conn ->
+ SQL.execute
+ conn
+ "UPDATE facts SET fact = ?, related_files = ?, confidence = ? WHERE id = ?"
+ (content, Text.pack (BLC.unpack (encode relatedFiles)), confidence, fid)
+
+-- | Delete a fact by ID.
+deleteFact :: Int -> IO ()
+deleteFact = TaskCore.deleteFact
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)