├── bases ├── .keep └── example-base │ ├── resources │ └── example-base │ │ └── .keep │ ├── test │ └── ai │ │ └── obney │ │ └── grain │ │ └── example_base │ │ └── .keep │ ├── deps.edn │ └── src │ └── ai │ └── obney │ └── grain │ └── example_base │ └── core.clj ├── components ├── .keep ├── time │ ├── resources │ │ └── time │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── time │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── time │ │ └── interface.cljc ├── pubsub │ ├── resources │ │ └── pubsub │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── pubsub │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── pubsub │ │ ├── core │ │ ├── protocol.cljc │ │ └── core_async.cljc │ │ ├── interface.cljc │ │ └── core.cljc ├── anomalies │ ├── resources │ │ └── anomalies │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── anomalies │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── anomalies │ │ └── interface.cljc ├── clj-dspy │ ├── resources │ │ └── clj-dspy │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── clj_dspy │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── clj_dspy │ │ └── interface.clj ├── webserver │ ├── resources │ │ └── webserver │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── webserver │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── webserver │ │ ├── interface.clj │ │ └── core.clj ├── event-model │ ├── resources │ │ └── event-model │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── event_model │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── event_model │ │ └── interface.clj ├── query-schema │ ├── resources │ │ └── query-schema │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── query_schema │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── query_schema │ │ └── interface.clj ├── schema-util │ ├── resources │ │ └── schema-util │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── schema_util │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── schema_util │ │ └── interface.cljc ├── example-service │ ├── resources │ │ └── example-service │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── example_service │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── example_service │ │ ├── interface │ │ ├── queries.clj │ │ ├── commands.clj │ │ ├── read_models.clj │ │ ├── periodic_tasks.clj │ │ ├── todo_processors.clj │ │ └── schemas.clj │ │ └── core │ │ ├── periodic_tasks.clj │ │ ├── todo_processors.clj │ │ ├── queries.clj │ │ ├── read_models.clj │ │ └── commands.clj ├── periodic-task │ ├── resources │ │ └── periodic-task │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── periodic_task │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── periodic_task │ │ ├── interface.clj │ │ └── core.clj ├── query-processor │ ├── resources │ │ └── query-processor │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── query_processor │ │ │ ├── .keep │ │ │ └── interface_test.clj │ ├── src │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── query_processor │ │ │ ├── interface.clj │ │ │ └── core.clj │ └── deps.edn ├── todo-processor │ ├── resources │ │ └── todo-processor │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── todo_processor │ │ │ ├── .keep │ │ │ └── interface_test.clj │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── todo_processor │ │ ├── interface.clj │ │ └── core.clj ├── behavior-tree-v2 │ ├── resources │ │ └── behavior-tree-v2 │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── behavior_tree_v2 │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── behavior_tree_v2 │ │ ├── interface │ │ └── protocol.clj │ │ ├── core │ │ ├── engine.clj │ │ ├── nodes.clj │ │ └── long_term_memory.clj │ │ └── interface.clj ├── command-processor │ ├── resources │ │ └── command-procesor │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── command_processor │ │ │ └── .keep │ ├── src │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── command_processor │ │ │ ├── interface.clj │ │ │ ├── interface │ │ │ └── schemas.clj │ │ │ └── core.clj │ └── deps.edn ├── event-store-v2 │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── event_store_v2 │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── event_store_v2 │ │ ├── interface.cljc │ │ ├── interface │ │ ├── schemas.cljc │ │ └── protocol.cljc │ │ ├── core.cljc │ │ └── core │ │ └── in_memory.cljc ├── core-async-thread-pool │ ├── resources │ │ └── core-async-thread-pool │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── core_async_thread_pool │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── core_async_thread_pool │ │ ├── core.clj │ │ └── interface.clj ├── query-request-handler │ ├── resources │ │ └── query-request-handler │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── query_request_handler │ │ │ └── .keep │ ├── src │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── query_request_handler │ │ │ ├── interface.clj │ │ │ └── core.clj │ └── deps.edn ├── command-request-handler │ ├── resources │ │ └── command-request-handler │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── command_request_handler │ │ │ └── .keep │ ├── src │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── command_request_handler │ │ │ ├── interface.clj │ │ │ └── core.clj │ └── deps.edn ├── event-store-postgres-v2 │ ├── resources │ │ └── event-store-postgres-v2 │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── event_store_postgres_v2 │ │ │ └── .keep │ ├── src │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── event_store_postgres_v2 │ │ │ ├── interface.clj │ │ │ └── core.clj │ └── deps.edn ├── behavior-tree-v2-dspy-extensions │ ├── resources │ │ └── behavior-tree-v2-dspy-extensions │ │ │ └── .keep │ ├── test │ │ └── ai │ │ │ └── obney │ │ │ └── grain │ │ │ └── behavior_tree_v2_dspy_extensions │ │ │ └── .keep │ ├── deps.edn │ └── src │ │ └── ai │ │ └── obney │ │ └── grain │ │ └── behavior_tree_v2_dspy_extensions │ │ ├── interface.clj │ │ └── core.clj └── mulog-aws-cloudwatch-emf-publisher │ ├── resources │ └── mulog-aws-cloudwatch-emf-publisher │ │ └── .keep │ ├── test │ └── ai │ │ └── obney │ │ └── grain │ │ └── mulog_aws_cloudwatch_emf_publisher │ │ └── .keep │ ├── deps.edn │ └── src │ └── ai │ └── obney │ └── grain │ └── mulog_aws_cloudwatch_emf_publisher │ └── interface.clj ├── projects ├── .keep ├── grain-mulog-aws-cloudwatch-emf-publisher │ └── deps.edn ├── grain-event-store-postgres-v2 │ └── deps.edn ├── grain-dspy-extensions │ └── deps.edn └── grain-core │ └── deps.edn ├── development └── src │ ├── .keep │ └── example_app_demo.clj ├── requirements.txt ├── python.edn ├── logo.png ├── .clj-kondo ├── imports │ ├── metosin │ │ └── malli │ │ │ └── config.edn │ ├── cnuernber │ │ └── dtype-next │ │ │ ├── tech │ │ │ └── v3 │ │ │ │ ├── datatype.clj │ │ │ │ ├── datatype_api.clj │ │ │ │ ├── parallel │ │ │ │ └── for.clj │ │ │ │ ├── datatype │ │ │ │ ├── binary_pred.clj │ │ │ │ ├── gradient.clj │ │ │ │ ├── jvm_map.clj │ │ │ │ ├── ffi.clj │ │ │ │ ├── statistics.clj │ │ │ │ ├── binary_op.clj │ │ │ │ ├── unary_op.clj │ │ │ │ ├── export_symbols.clj │ │ │ │ └── functional_api.clj │ │ │ │ ├── tensor.clj │ │ │ │ └── tensor_api.clj │ │ │ └── config.edn │ ├── com.github.seancorfield │ │ └── next.jdbc │ │ │ ├── config.edn │ │ │ └── hooks │ │ │ └── com │ │ │ └── github │ │ │ └── seancorfield │ │ │ └── next_jdbc.clj_kondo │ ├── io.pedestal │ │ └── pedestal.log │ │ │ ├── config.edn │ │ │ └── hooks │ │ │ └── io │ │ │ └── pedestal │ │ │ └── log.clj_kondo │ └── potemkin │ │ └── potemkin │ │ ├── potemkin │ │ └── namespaces.clj │ │ └── config.edn └── config.edn ├── .vscode └── settings.json ├── workspace.edn ├── .gitignore ├── LICENSE ├── deps.edn ├── readme.md └── scripts └── create_component.bb /bases/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /development/src/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/time/resources/time/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/pubsub/resources/pubsub/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/time/test/ai/obney/grain/time/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dspy==2.6.27 2 | litellm==1.75.3 -------------------------------------------------------------------------------- /bases/example-base/resources/example-base/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/anomalies/resources/anomalies/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/clj-dspy/resources/clj-dspy/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/pubsub/test/ai/obney/grain/pubsub/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/webserver/resources/webserver/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /python.edn: -------------------------------------------------------------------------------- 1 | {:python-executable ".venv/bin/python"} -------------------------------------------------------------------------------- /bases/example-base/test/ai/obney/grain/example_base/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/anomalies/test/ai/obney/grain/anomalies/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/clj-dspy/test/ai/obney/grain/clj_dspy/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/event-model/resources/event-model/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/query-schema/resources/query-schema/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/schema-util/resources/schema-util/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/webserver/test/ai/obney/grain/webserver/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/event-model/test/ai/obney/grain/event_model/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/example-service/resources/example-service/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/periodic-task/resources/periodic-task/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/query-processor/resources/query-processor/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/query-schema/test/ai/obney/grain/query_schema/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/schema-util/test/ai/obney/grain/schema_util/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/todo-processor/resources/todo-processor/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/behavior-tree-v2/resources/behavior-tree-v2/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/command-processor/resources/command-procesor/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/event-store-v2/test/ai/obney/grain/event_store_v2/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/example-service/test/ai/obney/grain/example_service/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/periodic-task/test/ai/obney/grain/periodic_task/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/query-processor/test/ai/obney/grain/query_processor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/todo-processor/test/ai/obney/grain/todo_processor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ObneyAI/grain/HEAD/logo.png -------------------------------------------------------------------------------- /components/behavior-tree-v2/test/ai/obney/grain/behavior_tree_v2/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/command-processor/test/ai/obney/grain/command_processor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/core-async-thread-pool/resources/core-async-thread-pool/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/query-request-handler/resources/query-request-handler/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/command-request-handler/resources/command-request-handler/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/core-async-thread-pool/test/ai/obney/grain/core_async_thread_pool/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/event-store-postgres-v2/resources/event-store-postgres-v2/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/query-request-handler/test/ai/obney/grain/query_request_handler/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/command-request-handler/test/ai/obney/grain/command_request_handler/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/event-store-postgres-v2/test/ai/obney/grain/event_store_postgres_v2/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/behavior-tree-v2-dspy-extensions/resources/behavior-tree-v2-dspy-extensions/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/behavior-tree-v2-dspy-extensions/test/ai/obney/grain/behavior_tree_v2_dspy_extensions/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/mulog-aws-cloudwatch-emf-publisher/resources/mulog-aws-cloudwatch-emf-publisher/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/mulog-aws-cloudwatch-emf-publisher/test/ai/obney/grain/mulog_aws_cloudwatch_emf_publisher/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/event-model/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | -------------------------------------------------------------------------------- /components/query-schema/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | -------------------------------------------------------------------------------- /.clj-kondo/imports/metosin/malli/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {malli.experimental/defn schema.core/defn} 2 | :linters {:unresolved-symbol {:exclude [(malli.core/=>)]}}} 3 | -------------------------------------------------------------------------------- /components/example-service/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | -------------------------------------------------------------------------------- /components/todo-processor/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | -------------------------------------------------------------------------------- /components/time/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {tick/tick {:mvn/version "1.0"}} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | -------------------------------------------------------------------------------- /components/periodic-task/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {jarohen/chime {:mvn/version "0.3.3"}} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | -------------------------------------------------------------------------------- /components/pubsub/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {org.clojure/core.async {:mvn/version "1.7.701"}} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | -------------------------------------------------------------------------------- /components/anomalies/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {com.cognitect/anomalies {:mvn/version "0.1.12"}} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | -------------------------------------------------------------------------------- /components/event-store-v2/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {danlentz/clj-uuid {:mvn/version "0.2.0"}} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | -------------------------------------------------------------------------------- /components/anomalies/src/ai/obney/grain/anomalies/interface.cljc: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.anomalies.interface 2 | (:require [cognitect.anomalies :as anom])) 3 | 4 | (defn anomaly? [x] (when (::anom/category x) x)) -------------------------------------------------------------------------------- /components/event-store-postgres-v2/src/ai/obney/grain/event_store_postgres_v2/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.event-store-postgres-v2.interface 2 | (:require [ai.obney.grain.event-store-postgres-v2.core])) 3 | -------------------------------------------------------------------------------- /components/core-async-thread-pool/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {org.clojure/core.async {:mvn/version "1.7.701"}} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | -------------------------------------------------------------------------------- /components/pubsub/src/ai/obney/grain/pubsub/core/protocol.cljc: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.pubsub.core.protocol) 2 | 3 | (defprotocol PubSub 4 | (start [this]) 5 | (stop [this]) 6 | (pub [this args]) 7 | (sub [this args])) -------------------------------------------------------------------------------- /components/behavior-tree-v2/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {com.brunobonacci/mulog-adv-console {:mvn/version "0.9.0"}} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | 6 | -------------------------------------------------------------------------------- /components/behavior-tree-v2-dspy-extensions/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {clj-python/libpython-clj {:mvn/version "2.025"}} 3 | :aliases {:test {:extra-paths ["test"] 4 | :extra-deps {}}}} 5 | -------------------------------------------------------------------------------- /components/example-service/src/ai/obney/grain/example_service/interface/queries.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-service.interface.queries 2 | (:require [ai.obney.grain.example-service.core.queries :as core])) 3 | 4 | (def queries core/queries) -------------------------------------------------------------------------------- /components/example-service/src/ai/obney/grain/example_service/interface/commands.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-service.interface.commands 2 | (:require [ai.obney.grain.example-service.core.commands :as core])) 3 | 4 | (def commands core/commands) -------------------------------------------------------------------------------- /components/clj-dspy/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {clj-python/libpython-clj {:mvn/version "2.025"} 3 | metosin/malli {:mvn/version "0.16.4"}} 4 | :aliases {:test {:extra-paths ["test"] 5 | :extra-deps {}}}} 6 | -------------------------------------------------------------------------------- /components/schema-util/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {metosin/malli {:mvn/version "0.17.0"} 3 | org.clojure/core.async {:mvn/version "1.7.701"}} 4 | :aliases {:test {:extra-paths ["test"] 5 | :extra-deps {}}}} 6 | -------------------------------------------------------------------------------- /bases/example-base/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {nrepl/nrepl {:mvn/version "1.3.1"} 3 | com.brunobonacci/mulog-adv-console {:mvn/version "0.9.0"}} 4 | :aliases {:test {:extra-paths ["test"] 5 | :extra-deps {}}}} 6 | -------------------------------------------------------------------------------- /components/query-processor/src/ai/obney/grain/query_processor/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.query-processor.interface 2 | (:require [ai.obney.grain.query-processor.core :as core])) 3 | 4 | (defn process-query [context] 5 | (core/process-query context)) -------------------------------------------------------------------------------- /components/command-processor/src/ai/obney/grain/command_processor/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.command-processor.interface 2 | (:require [ai.obney.grain.command-processor.core :as core])) 3 | 4 | (defn process-command [context] 5 | (core/process-command context)) 6 | -------------------------------------------------------------------------------- /components/example-service/src/ai/obney/grain/example_service/interface/read_models.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-service.interface.read-models 2 | (:require [ai.obney.grain.example-service.core.read-models :as core])) 3 | 4 | (defn root 5 | [context] 6 | (core/root context)) -------------------------------------------------------------------------------- /components/mulog-aws-cloudwatch-emf-publisher/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {org.clojure/data.json {:mvn/version "2.5.1"} 3 | com.brunobonacci/mulog {:mvn/version "0.9.0"}} 4 | :aliases {:test {:extra-paths ["test"] 5 | :extra-deps {}}}} 6 | -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/datatype.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.datatype) 2 | 3 | (defmacro make-reader 4 | ([datatype n-elems read-op] 5 | `(let [~'idx ~n-elems] 6 | ~read-op)) 7 | ([reader-datatype adv-datatype n-elems read-op] 8 | `(let [~'idx ~n-elems] 9 | ~read-op))) 10 | -------------------------------------------------------------------------------- /components/periodic-task/src/ai/obney/grain/periodic_task/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.periodic-task.interface 2 | (:require [ai.obney.grain.periodic-task.core :as core])) 3 | 4 | (defn start [config] 5 | (core/start config)) 6 | 7 | (defn stop [periodic-task] 8 | (core/stop periodic-task)) 9 | 10 | -------------------------------------------------------------------------------- /projects/grain-mulog-aws-cloudwatch-emf-publisher/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.12.0"} 2 | poly/grain-mulog-aws-cloudwatch-emf-publisher {:local/root "../../components/mulog-aws-cloudwatch-emf-publisher"}} 3 | 4 | :aliases {:test {:extra-paths [] 5 | :extra-deps {}}}} 6 | -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/datatype_api.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.datatype-api) 2 | 3 | 4 | (defmacro make-reader 5 | ([datatype n-elems read-op] 6 | `(let [~'idx ~n-elems] 7 | ~read-op)) 8 | ([reader-datatype adv-datatype n-elems read-op] 9 | `(let [~'idx ~n-elems] 10 | ~read-op))) 11 | -------------------------------------------------------------------------------- /components/todo-processor/src/ai/obney/grain/todo_processor/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.todo-processor.interface 2 | (:require [ai.obney.grain.todo-processor.core :as core])) 3 | 4 | (defn start 5 | [config] 6 | (core/start config)) 7 | 8 | (defn stop 9 | [todo-processor] 10 | (core/stop todo-processor)) -------------------------------------------------------------------------------- /components/example-service/src/ai/obney/grain/example_service/interface/periodic_tasks.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-service.interface.periodic-tasks 2 | (:require [ai.obney.grain.example-service.core.periodic-tasks :as core])) 3 | 4 | (defn example-periodic-task 5 | [context time] 6 | (core/example-periodic-task context time)) -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/parallel/for.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.parallel.for) 2 | 3 | 4 | (defmacro doiter 5 | [varname iterable & body] 6 | `(let [~varname ~iterable] 7 | ~@body)) 8 | 9 | 10 | (defmacro parallel-for 11 | [idx-var n-elems & body] 12 | `(let [~idx-var ~n-elems] 13 | ~@body)) 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "calva.replConnectSequences": [ 3 | { 4 | "projectType": "deps.edn", 5 | "name": "grain", 6 | "cljsType": "none", 7 | "menuSelections": { 8 | "cljAliases": ["dev", "test", "+default"] 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /components/example-service/src/ai/obney/grain/example_service/interface/todo_processors.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-service.interface.todo-processors 2 | (:require [ai.obney.grain.example-service.core.todo-processors :as core])) 3 | 4 | (defn calculate-average-counter-value 5 | [context] 6 | (core/calculate-average-counter-value context)) -------------------------------------------------------------------------------- /components/query-request-handler/src/ai/obney/grain/query_request_handler/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.query-request-handler.interface 2 | (:require [ai.obney.grain.query-request-handler.core :as core])) 3 | 4 | (defn routes 5 | [config] 6 | (core/routes config)) 7 | 8 | (defn handle-query 9 | [config query] 10 | (core/handle-query config query)) -------------------------------------------------------------------------------- /components/webserver/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {io.pedestal/pedestal.service {:mvn/version "0.7.2"} 3 | io.pedestal/pedestal.jetty {:mvn/version "0.7.1"} 4 | org.clojure/data.json {:mvn/version "2.5.1"} 5 | integrant/integrant {:mvn/version "0.13.1"}} 6 | :aliases {:test {:extra-paths ["test"] 7 | :extra-deps {}}}} 8 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {ai.obney.grain.schema-util.interface/defschemas clojure.core/def 2 | ai.obney.grain.clj-dspy.interface/defsignature clojure.core/def 3 | ai.obney.grain.clj-dspy.interface/defmodel clojure.core/def 4 | ai.obney.grain.clj-dspy.core/defsignature clojure.core/def 5 | ai.obney.grain.clj-dspy.core/defmodel clojure.core/def}} -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/datatype/binary_pred.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.datatype.binary-pred) 2 | 3 | 4 | 5 | (defmacro make-boolean-predicate 6 | [opname op] 7 | `(let [~'x 1 8 | ~'y 2] 9 | ~op)) 10 | 11 | 12 | (defmacro make-numeric-binary-predicate 13 | [opname scalar-op object-op] 14 | `(let [~'x 1 15 | ~'y 2] 16 | ~scalar-op 17 | ~object-op)) 18 | -------------------------------------------------------------------------------- /components/pubsub/src/ai/obney/grain/pubsub/interface.cljc: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.pubsub.interface 2 | (:require [ai.obney.grain.pubsub.core :as core])) 3 | 4 | (defn start 5 | [config] 6 | (core/start config)) 7 | 8 | (defn stop 9 | [pubsub] 10 | (core/stop pubsub)) 11 | 12 | (defn pub 13 | [pubsub args] 14 | (core/pub pubsub args)) 15 | 16 | (defn sub 17 | [pubsub args] 18 | (core/sub pubsub args)) -------------------------------------------------------------------------------- /components/query-processor/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {integrant/integrant {:mvn/version "0.13.1"} 3 | com.brunobonacci/mulog {:mvn/version "0.9.0"} 4 | com.cognitect/anomalies {:mvn/version "0.1.12"} 5 | metosin/malli {:mvn/version "0.17.0"} 6 | org.clojure/core.async {:mvn/version "1.7.701"}} 7 | :aliases {:test {:extra-paths ["test"] 8 | :extra-deps {}}}} 9 | -------------------------------------------------------------------------------- /.clj-kondo/imports/com.github.seancorfield/next.jdbc/config.edn: -------------------------------------------------------------------------------- 1 | {:hooks 2 | {:analyze-call 3 | {next.jdbc/with-transaction 4 | hooks.com.github.seancorfield.next-jdbc/with-transaction 5 | next.jdbc/with-transaction+options 6 | hooks.com.github.seancorfield.next-jdbc/with-transaction+options}} 7 | :lint-as {next.jdbc/on-connection clojure.core/with-open 8 | next.jdbc/on-connection+options clojure.core/with-open}} 9 | -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/datatype/gradient.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.datatype.gradient) 2 | 3 | 4 | (defmacro append-diff 5 | [rtype n-elems read-fn cast-fn append reader] 6 | `(. ~read-fn ~reader)) 7 | 8 | (defmacro prepend-diff 9 | [rtype n-elems read-fn cast-fn append reader] 10 | `(. ~read-fn ~reader)) 11 | 12 | (defmacro basic-diff 13 | [rtype n-elems read-fn reader] 14 | `(. ~read-fn ~reader)) 15 | -------------------------------------------------------------------------------- /components/command-request-handler/src/ai/obney/grain/command_request_handler/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.command-request-handler.interface 2 | (:require [ai.obney.grain.command-request-handler.core :as core])) 3 | 4 | (defn routes 5 | [config] 6 | (core/routes config)) 7 | 8 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 9 | (defn handle-command 10 | [config command] 11 | (core/handle-command config command)) -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/datatype/jvm_map.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.datatype.jvm-map) 2 | 3 | 4 | (defmacro bi-consumer 5 | [karg varg code] 6 | `(let [~karg 1 7 | ~varg 2] 8 | ~@code)) 9 | 10 | 11 | (defmacro bi-function 12 | [karg varg code] 13 | `(let [~karg 1 14 | ~varg 2] 15 | ~@code)) 16 | 17 | 18 | (defmacro function 19 | [karg code] 20 | `(let [~karg 1] 21 | ~@code)) 22 | -------------------------------------------------------------------------------- /components/query-request-handler/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {com.brunobonacci/mulog {:mvn/version "0.9.0"} 3 | com.cognitect/anomalies {:mvn/version "0.1.12"} 4 | org.clojure/core.async {:mvn/version "1.7.701"} 5 | io.pedestal/pedestal.service {:mvn/version "0.7.2"} 6 | com.cognitect/transit-clj {:mvn/version "1.0.333"}} 7 | :aliases {:test {:extra-paths ["test"] 8 | :extra-deps {}}}} 9 | -------------------------------------------------------------------------------- /components/command-request-handler/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {com.brunobonacci/mulog {:mvn/version "0.9.0"} 3 | com.cognitect/anomalies {:mvn/version "0.1.12"} 4 | org.clojure/core.async {:mvn/version "1.7.701"} 5 | io.pedestal/pedestal.service {:mvn/version "0.7.2"} 6 | com.cognitect/transit-clj {:mvn/version "1.0.333"}} 7 | :aliases {:test {:extra-paths ["test"] 8 | :extra-deps {}}}} 9 | -------------------------------------------------------------------------------- /components/behavior-tree-v2-dspy-extensions/src/ai/obney/grain/behavior_tree_v2_dspy_extensions/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.behavior-tree-v2-dspy-extensions.interface 2 | (:require [ai.obney.grain.behavior-tree-v2-dspy-extensions.core :as core])) 3 | 4 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 5 | (defn dspy 6 | [{{:keys [_id _signature _operation]} :opts 7 | :keys [_st-memory] 8 | :as context}] 9 | (core/dspy context)) 10 | 11 | 12 | -------------------------------------------------------------------------------- /components/command-processor/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {integrant/integrant {:mvn/version "0.13.1"} 3 | com.brunobonacci/mulog {:mvn/version "0.9.0"} 4 | com.cognitect/anomalies {:mvn/version "0.1.12"} 5 | metosin/malli {:mvn/version "0.17.0"} 6 | org.clojure/core.async {:mvn/version "1.7.701"} 7 | danlentz/clj-uuid {:mvn/version "0.2.0"}} 8 | :aliases {:test {:extra-paths ["test"] 9 | :extra-deps {}}}} 10 | -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/datatype/ffi.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.datatype.ffi) 2 | 3 | 4 | (defmacro define-library! 5 | [lib-varname lib-fns _lib-symbols _error-checker] 6 | (let [fn-defs (second lib-fns)] 7 | `(do 8 | (def ~lib-varname :ok) 9 | ~@(map (fn [[fn-name fn-data]] 10 | (let [argvec (mapv first (:argtypes fn-data))] 11 | `(defn ~(symbol (name fn-name)) 12 | ~argvec 13 | (apply + ~argvec)))) 14 | fn-defs)))) 15 | -------------------------------------------------------------------------------- /components/query-schema/src/ai/obney/grain/query_schema/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.query-schema.interface 2 | (:require [ai.obney.grain.schema-util.interface :refer [defschemas]])) 3 | 4 | 5 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 6 | (defschemas schemas 7 | {::query-id :uuid 8 | ::query-name :qualified-keyword 9 | ::query-timestamp :time/offset-date-time 10 | ::query [:map 11 | [:query/name ::query-name] 12 | [:query/id ::query-id] 13 | [:query/timestamp ::query-timestamp]]}) 14 | -------------------------------------------------------------------------------- /components/command-processor/src/ai/obney/grain/command_processor/interface/schemas.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.command-processor.interface.schemas 2 | (:require [ai.obney.grain.schema-util.interface :refer [defschemas]])) 3 | 4 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 5 | (defschemas schemas 6 | {::command-id :uuid 7 | ::command-name :qualified-keyword 8 | ::command-timestamp :time/offset-date-time 9 | ::command [:map 10 | [:command/name ::command-name] 11 | [:command/id ::command-id] 12 | [:command/timestamp ::command-timestamp]]}) -------------------------------------------------------------------------------- /components/event-store-postgres-v2/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {org.clojure/data.json {:mvn/version "2.5.1"} 3 | com.github.seancorfield/next.jdbc {:mvn/version "1.3.955"} 4 | org.postgresql/postgresql {:mvn/version "42.7.4"} 5 | hikari-cp/hikari-cp {:mvn/version "3.2.0"} 6 | integrant/integrant {:mvn/version "0.13.1"} 7 | com.brunobonacci/mulog {:mvn/version "0.9.0"} 8 | com.cognitect/anomalies {:mvn/version "0.1.12"} 9 | danlentz/clj-uuid {:mvn/version "0.2.0"}} 10 | :aliases {:test {:extra-paths ["test"] 11 | :extra-deps {}}}} 12 | -------------------------------------------------------------------------------- /workspace.edn: -------------------------------------------------------------------------------- 1 | {:top-namespace "ai.obney.grain" 2 | :interface-ns "interface" 3 | :default-profile-name "default" 4 | :compact-views #{} 5 | :vcs {:name "git" 6 | :auto-add false} 7 | :tag-patterns {:stable "stable-*" 8 | :release "v[0-9]*"} 9 | :projects {"development" {:alias "dev"} 10 | "grain-core" {:alias "core"} 11 | "grain-dspy-extensions" {:alias "gde"} 12 | "grain-event-store-postgres" {:alias "es-pg"} 13 | "grain-event-store-postgres-v2" {:alias "es-pg-v2"} 14 | "grain-mulog-aws-cloudwatch-emf-publisher" {:alias "mulog-aws-emf-pub"}}} 15 | -------------------------------------------------------------------------------- /projects/grain-event-store-postgres-v2/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.12.0"} 2 | pgesv2/event-store-postgres-v2 {:local/root "../../components/event-store-postgres-v2"} 3 | pgesv2/event-store-v2 {:local/root "../../components/event-store-v2"} 4 | pgesv2/pubsub {:local/root "../../components/pubsub"} 5 | pgesv2/anomalies {:local/root "../../components/anomalies"} 6 | pgesv2/schema-util {:local/root "../../components/schema-util"} 7 | pgesv2/time {:local/root "../../components/time"}} 8 | 9 | :aliases {:test {:extra-paths [] 10 | :extra-deps {}}}} 11 | -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/datatype/statistics.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.datatype.statistics) 2 | 3 | 4 | (defmacro define-descriptive-stats 5 | [] 6 | `(do 7 | ~@(->> [:skew :variance :standard-deviation :moment-3 :kurtosis :moment-4 :moment-2] 8 | (map (fn [tower-key] 9 | (let [fn-symbol (symbol (name tower-key))] 10 | `(defn ~fn-symbol 11 | ([~'data ~'options] 12 | (apply + ~'data ~'options) 13 | ~'data) 14 | ([~'data] 15 | (apply + ~'data))))))))) 16 | -------------------------------------------------------------------------------- /components/core-async-thread-pool/src/ai/obney/grain/core_async_thread_pool/core.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.core-async-thread-pool.core 2 | (:require [clojure.core.async :as async])) 3 | 4 | (defn start 5 | [{:keys [thread-count execution-fn error-fn in-chan]}] 6 | 7 | (dotimes [_ thread-count] 8 | (async/go-loop [] 9 | (when-let [job (async/CoreAsyncPubSub config))) 10 | 11 | (defmethod start-pubsub :default 12 | [config] 13 | (throw (ex-info "Unknown PubSub type" {:type (:type config)}))) 14 | 15 | (defn start 16 | [config] 17 | (start-pubsub config)) 18 | 19 | (defn stop 20 | [pubsub] 21 | (p/stop pubsub)) 22 | 23 | (defn pub 24 | [pubsub args] 25 | (p/pub pubsub args)) 26 | 27 | (defn sub 28 | [pubsub args] 29 | (p/sub pubsub args)) -------------------------------------------------------------------------------- /components/event-store-v2/src/ai/obney/grain/event_store_v2/interface.cljc: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.event-store-v2.interface 2 | (:refer-clojure :exclude [read]) 3 | (:require [ai.obney.grain.event-store-v2.interface.schemas] 4 | [ai.obney.grain.event-store-v2.core :as core] 5 | [ai.obney.grain.event-store-v2.core.in-memory])) 6 | 7 | (defn ->event 8 | [{:keys [_type _body _tags] :as args}] 9 | (core/->event args)) 10 | 11 | (defn start 12 | [config] 13 | (core/start config)) 14 | 15 | (defn stop 16 | [event-store] 17 | (core/stop event-store)) 18 | 19 | (defn append 20 | [event-store args] 21 | (core/append event-store args)) 22 | 23 | (defn read 24 | [event-store args] 25 | (core/read event-store args)) 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /projects/grain-dspy-extensions/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.12.0"} 2 | gde/anomalies {:local/root "../../components/anomalies"} 3 | gde/time {:local/root "../../components/time"} 4 | gde/clj-dspy {:local/root "../../components/clj-dspy"} 5 | gde/schema-util {:local/root "../../components/schema-util"} 6 | gde/pubsub {:local/root "../../components/pubsub"} 7 | gde/event-store-v2 {:local/root "../../components/event-store-v2"} 8 | gde/behavior-tree-v2 {:local/root "../../components/behavior-tree-v2"} 9 | gde/behavior-tree-v2-dspy-extensions {:local/root "../../components/behavior-tree-v2-dspy-extensions"}} 10 | 11 | :aliases {:test {:extra-paths [] 12 | :extra-deps {}}}} 13 | 14 | -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/datatype/unary_op.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.datatype.unary-op) 2 | 3 | 4 | (defmacro make-double-unary-op 5 | [_opname opcode] 6 | `(let [~'x 1.0] 7 | ~opcode)) 8 | 9 | 10 | (defmacro make-numeric-object-unary-op 11 | [_opname opcode] 12 | `(let [~'x 1.0] 13 | ~opcode)) 14 | 15 | 16 | (defmacro make-float-double-unary-op 17 | [_opname opcode] 18 | `(let [~'x 1.0] 19 | ~opcode)) 20 | 21 | 22 | (defmacro make-numeric-unary-op 23 | [_opname opcode] 24 | `(let [~'x 1.0] 25 | ~opcode)) 26 | 27 | 28 | (defmacro make-long-unary-op 29 | [_opname opcode] 30 | `(let [~'x 1] 31 | ~opcode)) 32 | 33 | 34 | (defmacro make-all-datatype-unary-op 35 | [_opname opcode] 36 | `(let [~'x 1] 37 | ~opcode)) 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/classes 2 | **/target 3 | **/.artifacts 4 | **/.cpcache 5 | **/.DS_Store 6 | **/.gradle 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/shelf 13 | .idea/**/statistic.xml 14 | .idea/dictionaries/** 15 | .idea/libraries/** 16 | 17 | # File-based project format 18 | *.iws 19 | *.ipr 20 | 21 | # Cursive Clojure plugin 22 | .idea/replstate.xml 23 | *.iml 24 | 25 | /example/example/** 26 | artifacts 27 | projects/**/pom.xml 28 | 29 | # nrepl 30 | .nrepl-port 31 | 32 | # clojure-lsp 33 | .lsp/.cache 34 | 35 | # clj-kondo 36 | .clj-kondo/.cache 37 | 38 | # Calva VS Code Extension 39 | .calva/output-window/output.calva-repl 40 | 41 | **/.portal/* 42 | 43 | **/.calva/output-window/output.calva-repl -------------------------------------------------------------------------------- /components/example-service/src/ai/obney/grain/example_service/core/periodic_tasks.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-service.core.periodic-tasks 2 | "The periodic tasks namespace in a grain service component is where 3 | periodic task functions are defined. These functions accept a context, 4 | which is wired up in the base for the grain app, and the time, provided 5 | by the periodic-task component implementation. 6 | 7 | Periodic tasks are less rigid than commands and todo-processors and generally 8 | do not have a specific return value. So they use the various dependencies in the context 9 | in order to perform their work with discretion." 10 | (:require [com.brunobonacci.mulog :as u])) 11 | 12 | (defn example-periodic-task 13 | [_context _time] 14 | (u/trace ::example [])) -------------------------------------------------------------------------------- /.clj-kondo/imports/io.pedestal/pedestal.log/config.edn: -------------------------------------------------------------------------------- 1 | ; Copyright 2024 Nubank NA 2 | 3 | ; The use and distribution terms for this software are covered by the 4 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0) 5 | ; which can be found in the file epl-v10.html at the root of this distribution. 6 | ; 7 | ; By using this software in any fashion, you are agreeing to be bound by 8 | ; the terms of this license. 9 | ; 10 | ; You must not remove this notice, or any other, from this software. 11 | 12 | {:hooks 13 | {:analyze-call 14 | {io.pedestal.log/trace hooks.io.pedestal.log/log-expr 15 | io.pedestal.log/debug hooks.io.pedestal.log/log-expr 16 | io.pedestal.log/info hooks.io.pedestal.log/log-expr 17 | io.pedestal.log/warn hooks.io.pedestal.log/log-expr 18 | io.pedestal.log/error hooks.io.pedestal.log/log-expr}}} 19 | -------------------------------------------------------------------------------- /components/behavior-tree-v2/src/ai/obney/grain/behavior_tree_v2/interface/protocol.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.behavior-tree-v2.interface.protocol) 2 | 3 | (def success :success) 4 | (def failure :failure) 5 | (def running :running) 6 | 7 | (defmulti tick 8 | "Execute the node and return success, failure, or running." 9 | (fn [node _context] (:type node))) 10 | 11 | (defmulti build 12 | "Build a behavior tree node based on its type." 13 | (fn [type _args] type)) 14 | 15 | (defn opts+children 16 | "Extract options and children from the config vector." 17 | [args] 18 | (if (and (seq args) (map? (first args))) 19 | [(first args) (rest args)] 20 | [{} args])) 21 | 22 | (defprotocol LongTermMemory 23 | "Protocol for providing long-term memory access." 24 | (latest [this] 25 | "Return the latest long-term memory read model as a map.")) -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/tensor.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.tensor) 2 | 3 | (defn args-expansion 4 | [op-code-args] 5 | (if (symbol? op-code-args) 6 | [op-code-args [:a :b :c]] 7 | (->> op-code-args 8 | (mapcat (fn [argname] 9 | [argname 1.0])) 10 | (vec)))) 11 | 12 | 13 | (defmacro typed-compute-tensor 14 | ([datatype advertised-datatype rank shape op-code-args op-code] 15 | (let [args-expansion (args-expansion op-code-args)] 16 | `(let ~args-expansion 17 | ~op-code))) 18 | ([advertised-datatype rank shape op-code-args op-code] 19 | (let [args-expansion (args-expansion op-code-args)] 20 | `(let ~args-expansion 21 | ~op-code))) 22 | ([_advertised-datatype _shape op-code-args op-code] 23 | (let [args-expansion (args-expansion op-code-args)] 24 | `(let ~args-expansion 25 | ~op-code)))) 26 | -------------------------------------------------------------------------------- /components/schema-util/src/ai/obney/grain/schema_util/interface.cljc: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.schema-util.interface 2 | (:require [malli.core :as m] 3 | [malli.registry :as mr] 4 | [malli.experimental.time :as mt] 5 | #?@(:clj [[clojure.core.async.impl.protocols :refer [Channel]]]))) 6 | 7 | (def registry* (atom (merge (mt/schemas) (m/default-schemas)))) 8 | 9 | (defn register! 10 | [schema-map] 11 | (swap! registry* merge schema-map)) 12 | 13 | #?(:clj (defmacro defschemas 14 | [symbol schema-map] 15 | `(do 16 | (#'register! ~schema-map) 17 | (def ~symbol ~schema-map)))) 18 | 19 | (mr/set-default-registry! 20 | (mr/mutable-registry registry*)) 21 | 22 | ;; 23 | ;; Default Schemas 24 | ;; 25 | 26 | #?(:clj (defschemas schemas 27 | {::channel [:fn #(satisfies? Channel %)]}) 28 | :cljs (register! {})) 29 | 30 | 31 | -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/tensor_api.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.tensor-api) 2 | 3 | 4 | (defn args-expansion 5 | [op-code-args] 6 | (if (symbol? op-code-args) 7 | [op-code-args [:a :b :c]] 8 | (->> op-code-args 9 | (mapcat (fn [argname] 10 | [argname 1.0])) 11 | (vec)))) 12 | 13 | 14 | (defmacro typed-compute-tensor 15 | ([datatype advertised-datatype rank shape op-code-args op-code] 16 | (let [args-expansion (args-expansion op-code-args)] 17 | `(let ~args-expansion 18 | ~op-code))) 19 | ([advertised-datatype rank shape op-code-args op-code] 20 | (let [args-expansion (args-expansion op-code-args)] 21 | `(let ~args-expansion 22 | ~op-code))) 23 | ([_advertised-datatype _shape op-code-args op-code] 24 | (let [args-expansion (args-expansion op-code-args)] 25 | `(let ~args-expansion 26 | ~op-code)))) 27 | -------------------------------------------------------------------------------- /components/core-async-thread-pool/src/ai/obney/grain/core_async_thread_pool/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.core-async-thread-pool.interface 2 | (:require [ai.obney.grain.core-async-thread-pool.core :as core] 3 | [ai.obney.grain.schema-util.interface :as schemas :refer [defschemas]])) 4 | 5 | (defschemas schemas 6 | {::thread-count :int 7 | ::execution-fn :function 8 | ::error-fn :function 9 | ::in-chan ::schemas/channel 10 | 11 | ::start-args [:map 12 | [:thread-count ::thread-count] 13 | [:execution-fn ::execution-fn] 14 | [:error-fn ::error-fn] 15 | [:in-chan ::in-chan]] 16 | 17 | 18 | ::stop-fn :function 19 | 20 | ::start-output [:map 21 | [:stop-fn ::stop-fn]]}) 22 | 23 | (defn start [config] 24 | (core/start config)) 25 | 26 | (defn stop [thread-pool] 27 | (core/stop thread-pool)) 28 | 29 | -------------------------------------------------------------------------------- /components/pubsub/src/ai/obney/grain/pubsub/core/core_async.cljc: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.pubsub.core.core-async 2 | (:require [clojure.core.async :as async] 3 | [ai.obney.grain.pubsub.core.protocol :as protocol])) 4 | 5 | (defn start 6 | [{{:keys [topic-fn]} :config}] 7 | (let [chan (async/chan 1024) 8 | pub (async/pub chan topic-fn)] 9 | {:chan chan 10 | :pub pub})) 11 | 12 | (defn stop 13 | [{{:keys [chan]} :state}] 14 | (async/close! chan)) 15 | 16 | (defn pub 17 | [{{:keys [chan]} :state :as _pubsub} 18 | {:keys [message] :as _args}] 19 | (when message 20 | (async/put! chan message))) 21 | 22 | (defn sub 23 | [{{:keys [pub]} :state :as _pubsub} 24 | {:keys [topic sub-chan] :as _args}] 25 | (async/sub pub topic sub-chan)) 26 | 27 | 28 | (defrecord CoreAsyncPubSub [config] 29 | protocol/PubSub 30 | (start [this] 31 | (assoc this :state (start this))) 32 | (stop [this] 33 | (stop this)) 34 | (pub [this args] 35 | (pub this args)) 36 | (sub [this args] 37 | (sub this args))) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copyright 2025 ObneyAI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /.clj-kondo/imports/io.pedestal/pedestal.log/hooks/io/pedestal/log.clj_kondo: -------------------------------------------------------------------------------- 1 | ; Copyright 2024 Nubank NA 2 | 3 | ; The use and distribution terms for this software are covered by the 4 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0) 5 | ; which can be found in the file epl-v10.html at the root of this distribution. 6 | ; 7 | ; By using this software in any fashion, you are agreeing to be bound by 8 | ; the terms of this license. 9 | ; 10 | ; You must not remove this notice, or any other, from this software. 11 | 12 | (ns hooks.io.pedestal.log 13 | (:require [clj-kondo.hooks-api :as api])) 14 | 15 | (defn log-expr 16 | "Expands (log-expr :a :A :b :B ... ) 17 | to (hash-map :a :A :b :B ... ) per clj-kondo examples." 18 | [{:keys [:node]}] 19 | (let [[k v & _kvs] (rest (:children node))] 20 | (when-not (and k v) 21 | (throw (ex-info "No kv pair provided" {}))) 22 | (let [new-node (api/list-node 23 | (list* 24 | (api/token-node 'hash-map) 25 | (rest (:children node))))] 26 | {:node (vary-meta new-node assoc :clj-kondo/ignore [:unused-value])}))) 27 | -------------------------------------------------------------------------------- /components/periodic-task/src/ai/obney/grain/periodic_task/core.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.periodic-task.core 2 | (:require [chime.core :as chime] 3 | [com.brunobonacci.mulog :as u]) 4 | (:import [java.time Instant Duration])) 5 | 6 | (defn start [{{:keys [every duration]} :schedule 7 | :keys [handler-fn _task-name _schedule] :as args}] 8 | (u/log ::starting-periodic-task ::args args) 9 | (let [now (Instant/now) 10 | schedule-seq (chime/periodic-seq 11 | now 12 | (case duration 13 | :seconds (Duration/ofSeconds every) 14 | :minutes (Duration/ofMinutes every) 15 | :hours (Duration/ofHours every)))] 16 | {::task (chime/chime-at schedule-seq handler-fn) 17 | ::args args})) 18 | 19 | (defn stop [{::keys [task args]}] 20 | (u/log ::stopping-periodic-task ::args args) 21 | (.close task)) 22 | 23 | 24 | (comment 25 | 26 | (def task 27 | (start 28 | {:schedule {:every 1 :duration :seconds} 29 | :handler-fn (fn [_time] (println "HELLO")) 30 | :task-name ::hello-world-task})) 31 | 32 | (stop task) 33 | 34 | "" 35 | ) 36 | 37 | -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/datatype/export_symbols.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.datatype.export-symbols 2 | (:require [clj-kondo.hooks-api :as hooks-api])) 3 | 4 | (defmacro export-symbols 5 | [src-ns & symbol-list] 6 | (let [analysis (:clj (hooks-api/ns-analysis src-ns))] 7 | `(do 8 | ~@(->> symbol-list 9 | (mapv 10 | (fn [sym-name] 11 | (when-let [fn-data (get analysis sym-name)] 12 | (if-let [arities (get fn-data :fixed-arities)] 13 | `(defn ~sym-name 14 | ~@(->> arities 15 | (map (fn [arity] 16 | (let [argvec (mapv 17 | #(symbol (str "arg-" %)) 18 | (range arity))] 19 | `(~argvec (apply + ~argvec) 20 | ;;This line is to disable the type detection of clj-kondo 21 | ~(if-not (= 0 arity) 22 | (first argvec) 23 | :ok))))))) 24 | `(def ~sym-name :defined))))))))) 25 | -------------------------------------------------------------------------------- /projects/grain-core/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.12.0"} 2 | poly/command-processor {:local/root "../../components/command-processor"} 3 | poly/command-request-handler {:local/root "../../components/command-request-handler"} 4 | poly/anomalies {:local/root "../../components/anomalies"} 5 | poly/time {:local/root "../../components/time"} 6 | poly/schema-util {:local/root "../../components/schema-util"} 7 | poly/core-async-thread-pool {:local/root "../../components/core-async-thread-pool"} 8 | poly/webserver {:local/root "../../components/webserver"} 9 | poly/query-processor {:local/root "../../components/query-processor"} 10 | poly/query-request-handler {:local/root "../../components/query-request-handler"} 11 | poly/query-schema {:local/root "../../components/query-schema"} 12 | poly/event-store-v2 {:local/root "../../components/event-store-v2"} 13 | poly/pubsub {:local/root "../../components/pubsub"} 14 | poly/todo-processor {:local/root "../../components/todo-processor"} 15 | poly/periodic-task {:local/root "../../components/periodic-task"} 16 | poly/behavior-tree-v2 {:local/root "../../components/behavior-tree-v2"}} 17 | 18 | :aliases {:test {:extra-paths [] 19 | :extra-deps {}}}} 20 | -------------------------------------------------------------------------------- /components/example-service/src/ai/obney/grain/example_service/core/todo_processors.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-service.core.todo-processors 2 | "The core todo-processors namespace in a grain service is where todo-processor handler functions are defined. 3 | These functions receive a context and have a specific return signature. They can return a cognitect anomaly, 4 | a map with a `:result/events` key containing a sequence of valid events per the event-store event 5 | schema, or an empty map. Sometimes the todo-processor will just call a command through the commant-processor. 6 | The wiring up of the context and the function occurs in the grain app base. The todo-processor subscribes to 7 | one or more events via pubsub and only ever processes a single event at a time, which is included in the context." 8 | (:require [ai.obney.grain.command-processor.interface :as command-processor] 9 | [ai.obney.grain.time.interface :as time])) 10 | 11 | (defn calculate-average-counter-value 12 | "Calculates the average value of the counter from the given todo items." 13 | [{:keys [_event] :as context}] 14 | (command-processor/process-command 15 | (assoc context 16 | :command 17 | {:command/id (random-uuid) 18 | :command/timestamp (time/now) 19 | :command/name :example/calculate-average-counter-value}))) 20 | -------------------------------------------------------------------------------- /components/webserver/src/ai/obney/grain/webserver/core.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.webserver.core 2 | (:require [integrant.core :as ig] 3 | [io.pedestal.http :as http] 4 | [io.pedestal.http.route :as route] 5 | [clojure.set :as set])) 6 | 7 | (def ^:private system 8 | {::server {::http/port 8080 9 | ::http/host "0.0.0.0" 10 | ::http/type :jetty 11 | ::http/routes #(route/expand-routes #{["/" :get (fn [_req] {:status 200 :body "Hello, world!"}) :route-name :default]}) 12 | ::http/join? false}}) 13 | 14 | (defmethod ig/init-key ::server [_ config] 15 | (http/start (http/create-server config))) 16 | 17 | (defmethod ig/halt-key! ::server [_ server] 18 | (http/stop server)) 19 | 20 | (defn start 21 | [{:http/keys [routes] :as config}] 22 | (ig/init 23 | (cond-> (update system 24 | ::server 25 | merge 26 | (set/rename-keys 27 | config 28 | {:http/port ::http/port 29 | :https/host ::http/host 30 | :http/join? ::http/join? 31 | :http/routes ::http/routes})) 32 | routes 33 | (assoc-in [::server ::http/routes] #(route/expand-routes routes))))) 34 | 35 | (defn stop 36 | [webserver] 37 | (ig/halt! webserver)) 38 | 39 | 40 | -------------------------------------------------------------------------------- /.clj-kondo/imports/com.github.seancorfield/next.jdbc/hooks/com/github/seancorfield/next_jdbc.clj_kondo: -------------------------------------------------------------------------------- 1 | (ns hooks.com.github.seancorfield.next-jdbc 2 | (:require [clj-kondo.hooks-api :as api])) 3 | 4 | (defn with-transaction 5 | "Expands (with-transaction [tx expr opts] body) 6 | to (let [tx expr] opts body) per clj-kondo examples." 7 | [{:keys [:node]}] 8 | (let [[binding-vec & body] (rest (:children node)) 9 | [sym val opts] (:children binding-vec)] 10 | (when-not (and sym val) 11 | (throw (ex-info "No sym and val provided" {}))) 12 | (let [new-node (api/list-node 13 | (list* 14 | (api/token-node 'let) 15 | (api/vector-node [sym val]) 16 | opts 17 | body))] 18 | {:node new-node}))) 19 | 20 | (defn with-transaction+options 21 | "Expands (with-transaction+options [tx expr opts] body) 22 | to (let [tx expr] opts body) per clj-kondo examples." 23 | [{:keys [:node]}] 24 | (let [[binding-vec & body] (rest (:children node)) 25 | [sym val opts] (:children binding-vec)] 26 | (when-not (and sym val) 27 | (throw (ex-info "No sym and val provided" {}))) 28 | (let [new-node (api/list-node 29 | (list* 30 | (api/token-node 'let) 31 | (api/vector-node [sym val]) 32 | opts 33 | body))] 34 | {:node new-node}))) 35 | -------------------------------------------------------------------------------- /components/example-service/src/ai/obney/grain/example_service/core/queries.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-service.core.queries 2 | "The core queries namespace in a grain service component implements 3 | the query handlers and defines the query registry. Query functions 4 | take a context that includes any necessary dependencies, to be injected 5 | in the base for the service. Usually a query-request-handler or another 6 | type of adapter will call the query processor, which will access the query 7 | registry for the entire application in the context. Queries either return a cognitect 8 | anomaly or a map that optionally has a :query/result which is some 9 | data that is meant to be returned to the caller, see query-request-handler for example." 10 | (:require [ai.obney.grain.example-service.interface.read-models :as read-models] 11 | [cognitect.anomalies :as anom])) 12 | 13 | (defn counters 14 | [context] 15 | (let [counters (->> (read-models/root context) 16 | vals)] 17 | {:query/result counters})) 18 | 19 | (defn counter 20 | [{{:keys [counter-id]} :query :as context}] 21 | (let [counter (-> (read-models/root context) 22 | (get counter-id))] 23 | (if counter 24 | {:query/result counter} 25 | {::anom/category ::anom/not-found 26 | ::anom/message (format "Counter with ID '%s' not found." counter-id)}))) 27 | 28 | (def queries 29 | {:example/counters {:handler-fn #'counters} 30 | :example/counter {:handler-fn #'counter}}) -------------------------------------------------------------------------------- /components/example-service/src/ai/obney/grain/example_service/interface/schemas.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-service.interface.schemas 2 | "The schemas ns in a grain service component defines the schemas for commands, events, queries, etc. 3 | 4 | It uses the `defschemas` macro to register the schemas centrally for the rest of 5 | the system to use. 6 | 7 | Schemas are validated in places such as the command-processor 8 | and event-store." 9 | (:require [ai.obney.grain.schema-util.interface :refer [defschemas]])) 10 | 11 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 12 | (defschemas commands 13 | {:example/create-counter 14 | [:map 15 | [:name :string]] 16 | 17 | :example/increment-counter 18 | [:map 19 | [:counter-id :uuid]] 20 | 21 | :example/decrement-counter 22 | [:map 23 | [:counter-id :uuid]] 24 | 25 | :example/calculate-average-counter-value 26 | [:map]}) 27 | 28 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 29 | (defschemas events 30 | {:example/counter-created 31 | [:map 32 | [:counter-id :uuid] 33 | [:name :string]] 34 | 35 | :example/counter-incremented 36 | [:map 37 | [:counter-id :uuid]] 38 | 39 | :example/counter-decremented 40 | [:map 41 | [:counter-id :uuid]] 42 | 43 | :example/average-calculated 44 | [:map 45 | [:value :double]]}) 46 | 47 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 48 | (defschemas queries 49 | {:example/counters 50 | [:map] 51 | :example/counter 52 | [:map 53 | [:counter-id :uuid]]}) -------------------------------------------------------------------------------- /components/query-processor/src/ai/obney/grain/query_processor/core.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.query-processor.core 2 | (:require 3 | [ai.obney.grain.query-schema.interface :as query-schema] 4 | [com.brunobonacci.mulog :as u] 5 | [cognitect.anomalies :as anom] 6 | [malli.core :as mc] 7 | [malli.error :as me])) 8 | 9 | (defn process-query [{:keys [query query-registry] :as context}] 10 | (u/trace 11 | ::process-query 12 | [::query query] 13 | (let [query-name (:query/name query) 14 | handler (get-in query-registry [query-name :handler-fn])] 15 | (if handler 16 | (if-let [_ (and (mc/validate query-name query) 17 | (mc/validate ::query-schema/query query))] 18 | (let [result (try 19 | (handler context) 20 | (catch Exception e 21 | (u/log ::query-handler-exception 22 | :error e 23 | :query (get-in context [:query :query/name])) 24 | {::anom/category ::anom/fault 25 | ::anom/message (format "Error executing query handler: %s" (.getMessage e))}))] 26 | (if (nil? result) 27 | {::anom/category ::anom/fault 28 | ::anom/message (format "Query handler returned nil: %s" 29 | (get-in query [:query :query/name]))} 30 | result)) 31 | {::anom/category ::anom/incorrect 32 | ::anom/message "Invalid Query: Failed Schema Validation" 33 | :error/explain (me/humanize (or (mc/explain query-name query) 34 | (mc/explain ::query-schema/query query)))}) 35 | {::anom/category ::anom/not-found 36 | ::anom/message "Unknown Query"})))) 37 | 38 | -------------------------------------------------------------------------------- /.clj-kondo/imports/potemkin/potemkin/potemkin/namespaces.clj: -------------------------------------------------------------------------------- 1 | (ns potemkin.namespaces 2 | (:require [clj-kondo.hooks-api :as api])) 3 | 4 | (defn import-macro* 5 | ([sym] 6 | `(def ~(-> sym name symbol) ~sym)) 7 | ([sym name] 8 | `(def ~name ~sym))) 9 | 10 | (defmacro import-fn 11 | ([sym] 12 | (import-macro* sym)) 13 | ([sym name] 14 | (import-macro* sym name))) 15 | 16 | (defmacro import-macro 17 | ([sym] 18 | (import-macro* sym)) 19 | ([sym name] 20 | (import-macro* sym name))) 21 | 22 | (defmacro import-def 23 | ([sym] 24 | (import-macro* sym)) 25 | ([sym name] 26 | (import-macro* sym name))) 27 | 28 | #_ 29 | (defmacro import-vars 30 | "Imports a list of vars from other namespaces." 31 | [& syms] 32 | (let [unravel (fn unravel [x] 33 | (if (sequential? x) 34 | (->> x 35 | rest 36 | (mapcat unravel) 37 | (map 38 | #(symbol 39 | (str (first x) 40 | (when-let [n (namespace %)] 41 | (str "." n))) 42 | (name %)))) 43 | [x])) 44 | syms (mapcat unravel syms) 45 | result `(do 46 | ~@(map 47 | (fn [sym] 48 | (let [vr (resolve sym) 49 | m (meta vr)] 50 | (cond 51 | (nil? vr) `(throw (ex-info (format "`%s` does not exist" '~sym) {})) 52 | (:macro m) `(def ~(-> sym name symbol) ~sym) 53 | (:arglists m) `(def ~(-> sym name symbol) ~sym) 54 | :else `(def ~(-> sym name symbol) ~sym)))) 55 | syms))] 56 | result)) 57 | -------------------------------------------------------------------------------- /components/mulog-aws-cloudwatch-emf-publisher/src/ai/obney/grain/mulog_aws_cloudwatch_emf_publisher/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.mulog-aws-cloudwatch-emf-publisher.interface 2 | (:require [com.brunobonacci.mulog.buffer :as mb] 3 | [clojure.data.json :as json])) 4 | 5 | (defn- build-emf-event 6 | "Constructs an EMF JSON string from a μ/log event." 7 | [{metric-name :metric/name 8 | metric-value :metric/value 9 | duration-ns :mulog/duration 10 | timestamp :mulog/timestamp 11 | resolution :metric/resolution 12 | :keys [app-name env] 13 | :as _event}] 14 | (let [unit (cond 15 | (and metric-name metric-value) "Count" 16 | (and duration-ns metric-name) "Milliseconds" 17 | :else "None") 18 | value (or metric-value (/ duration-ns 1e6))] 19 | (json/write-str 20 | {:_aws {:Timestamp timestamp 21 | :CloudWatchMetrics 22 | [{:Namespace "ObneyAI/InfoSystem" 23 | :Dimensions [["app-name" "env"]] 24 | :Metrics [{:Name metric-name 25 | :Unit unit 26 | :StorageResolution 27 | (case resolution 28 | :high 1 29 | :low 60 30 | 1)}]}]} 31 | :app-name app-name 32 | :env env 33 | (keyword metric-name) value}))) 34 | 35 | (deftype CloudWatchEMFPublisher [buffer] 36 | com.brunobonacci.mulog.publisher.PPublisher 37 | (agent-buffer [_] buffer) 38 | (publish-delay [_] 200) 39 | (publish [_ buffer] 40 | (doseq [event (map second (mb/items buffer))] 41 | (when (or (and (:metric/name event) (:metric/value event)) 42 | (and (:metric/name event) (:mulog/duration event))) 43 | (println (build-emf-event event)))) 44 | (mb/clear buffer))) 45 | 46 | (defn cloudwatch-emf-publisher 47 | "Creates an instance of the CloudWatch EMF Publisher." 48 | [_config] 49 | (CloudWatchEMFPublisher. (mb/agent-buffer 10000))) -------------------------------------------------------------------------------- /components/event-store-v2/src/ai/obney/grain/event_store_v2/interface/schemas.cljc: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.event-store-v2.interface.schemas 2 | (:require [ai.obney.grain.schema-util.interface :refer [register!]] 3 | #?@(:clj [[clj-uuid :as uuid]] 4 | :cljs [["uuid" :as uuid*]]))) 5 | 6 | (defn- as-of-or-after [x] (not (and (:as-of x) (:after x)))) 7 | 8 | #?(:clj (defn- uuid-v7? [x] (and (uuid? x) (= 7 (uuid/get-version x)))) 9 | :cljs (defn uuid-v7? [x] 10 | (let [x* (str x)] 11 | (boolean 12 | (and (uuid*/validate x*) 13 | (= (uuid*/version x*) 7)))))) 14 | 15 | (register! 16 | {::entity-type :keyword 17 | ::entity-id :uuid 18 | ::tag [:tuple ::entity-type ::entity-id] 19 | ::tags [:set ::tag] 20 | ::type :keyword 21 | ::types [:set ::type] 22 | ::uuid-v7 [:fn 23 | {:error/message "Must be UUID v7"} 24 | uuid-v7?] 25 | ::id ::uuid-v7 26 | ::timestamp [:time/offset-date-time] 27 | 28 | ::event [:map 29 | [:event/id ::id] 30 | [:event/timestamp ::timestamp] 31 | [:event/tags ::tags] 32 | [:event/type ::type]] 33 | 34 | ::as-of-or-after 35 | [:fn {:error/message "Cannot supply both :as-of and :after"} as-of-or-after] 36 | 37 | ::read-args 38 | [:and 39 | ::as-of-or-after 40 | [:map 41 | [:tags {:optional true} ::tags] 42 | [:types {:optional true} ::types] 43 | [:as-of {:optional true} ::id] 44 | [:after {:optional true} ::id]]] 45 | 46 | ::cas 47 | [:and 48 | ::as-of-or-after 49 | [:map 50 | [:tags {:optional true} ::tags] 51 | [:types {:optional true} ::types] 52 | [:as-of {:optional true} ::id] 53 | [:after {:optional true} ::id] 54 | [:predicate-fn fn?]]] 55 | 56 | ::append-args 57 | [:map 58 | [:events [:vector ::event]] 59 | [:tx-metadata {:optional true} [:map]] 60 | [:cas {:optional true} ::cas]] 61 | 62 | ::->event-args 63 | [:map 64 | [:type ::type] 65 | [:tags {:optional true} ::tags] 66 | [:body {:optional true} [:map]]] 67 | 68 | :grain/tx 69 | [:map 70 | [:event-ids [:set ::id]] 71 | [:metadata {:optional true} [:map]]]}) 72 | -------------------------------------------------------------------------------- /components/example-service/src/ai/obney/grain/example_service/core/read_models.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-service.core.read-models 2 | "The core read-models namespace in a grain app is where projections are created from events. 3 | Events are retrieved using the event-store and the read model is built through reducing usually. 4 | These tend to be used by the other components of the grain app, such as commands, queries, periodic tasks, 5 | and todo-processors." 6 | (:require [ai.obney.grain.event-store-v2.interface :as event-store] 7 | [com.brunobonacci.mulog :as u])) 8 | 9 | (defmulti apply-event 10 | "Apply an event to the read model." 11 | (fn [_state event] 12 | (:event/type event))) 13 | 14 | (defmethod apply-event :example/counter-created 15 | [state {:keys [counter-id name]}] 16 | (assoc state counter-id 17 | {:counter/id counter-id 18 | :counter/name name})) 19 | 20 | (defmethod apply-event :example/counter-incremented 21 | [state {:keys [counter-id]}] 22 | (update state counter-id update :counter/value (fnil inc 0))) 23 | 24 | (defmethod apply-event :example/counter-decremented 25 | [state {:keys [counter-id]}] 26 | (update state counter-id update :counter/value (fnil dec 0))) 27 | 28 | (defmethod apply-event :default 29 | [state _event] 30 | ;; If the event is not recognized, return the state unchanged. 31 | state) 32 | 33 | (defn apply-events 34 | "Applies a sequence of events to the read model state." 35 | [events] 36 | (let [result (reduce 37 | (fn [state event] 38 | (apply-event state event)) 39 | {} 40 | events)] 41 | (when (seq result) 42 | result))) 43 | 44 | (defn root 45 | "Returns the root entity of the read model." 46 | [{:keys [event-store] :as _context}] 47 | (let [events (event-store/read 48 | event-store 49 | {:types #{:example/counter-created 50 | :example/counter-incremented 51 | :example/counter-decremented}}) 52 | state (u/trace 53 | ::read-model-root 54 | [:metric/name "ReadModelExampleRoot"] 55 | (apply-events events))] 56 | state)) -------------------------------------------------------------------------------- /components/event-store-v2/src/ai/obney/grain/event_store_v2/interface/protocol.cljc: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.event-store-v2.interface.protocol 2 | (:refer-clojure :exclude [read])) 3 | 4 | (defmulti start-event-store #(get-in % [:conn :type])) 5 | 6 | (defprotocol EventStore 7 | (start [this]) 8 | (stop [this]) 9 | 10 | (append [this args] 11 | "Append a series of events to the event store. 12 | 13 | args: 14 | 15 | A map with the following keys: 16 | 17 | :events - A vector of events to append to the event store. 18 | 19 | :tx-metadata - An optional map of metadata to associate with the transaction. 20 | 21 | :cas - An optional map with the following keys: 22 | 23 | :tags - A set of tags to filter events by. Each tag is a tuple of entity type and entity ID. 24 | 25 | :types - A set of event types to filter events by. 26 | 27 | :as-of - A UUID v7 event id to filter events that occurred before or at this time. 28 | 29 | :after - A UUID v7 event id to filter events that occurred after this time. 30 | 31 | :predicate-fn - A function with signature [events] that returns true or false, deciding whether the events will be stored or not.") 32 | 33 | (read [this args] 34 | "Read an ordered stream of events from the event store. 35 | 36 | Returns a reducible (IReduceInit + IReduce) that streams events without eagerly loading them all into memory. 37 | 38 | If no tags or types are provided, all events are returned. 39 | 40 | Cannot supply both :as-of and :after at the same time. 41 | 42 | May return a cognitect anomaly. 43 | 44 | Usage: 45 | - (reduce f init (read store query)) ; Direct reduction 46 | - (transduce xf f init (read store query)) ; Transducer pipeline 47 | - (into [] (take 10) (read store query)) ; Collect with limit 48 | 49 | args: 50 | 51 | A map with the following optional keys: 52 | 53 | :tags - A set of tags to filter events by. Each tag is a tuple of entity type and entity ID. 54 | 55 | :types - A set of event types to filter events by. 56 | 57 | :as-of - A UUID v7 event id to filter events that occurred before or at this time. 58 | 59 | :after - A UUID v7 event id to filter events that occurred after this time.")) -------------------------------------------------------------------------------- /components/behavior-tree-v2/src/ai/obney/grain/behavior_tree_v2/core/engine.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.behavior-tree-v2.core.engine 2 | (:require [ai.obney.grain.behavior-tree-v2.interface.protocol :as p] 3 | [ai.obney.grain.behavior-tree-v2.core.long-term-memory :as ltm] 4 | [ai.obney.grain.behavior-tree-v2.core.nodes] 5 | [malli.core :as m])) 6 | 7 | (defn build 8 | "Build a behavior tree from a config vector." 9 | [[node-type & args :as _config] 10 | {:keys [event-store queries read-model-fn st-memory] :as context}] 11 | {:tree (p/build node-type args) 12 | :context (cond-> context 13 | :always (assoc :st-memory (atom (or st-memory {}))) 14 | (and event-store queries read-model-fn) 15 | (assoc :lt-memory (ltm/->long-term-memory context)))}) 16 | 17 | (defn run 18 | "Run the behavior tree with the given context." 19 | [{:keys [tree context] :as _bt}] 20 | (p/tick tree context)) 21 | 22 | ;; ## Custom Behavior Tree Conditions 23 | 24 | (defn st-memory-has-value? 25 | [{{:keys [path schema]} :opts 26 | :keys [st-memory]}] 27 | (let [st-memory-state @st-memory] 28 | (m/validate 29 | schema 30 | (if path 31 | (get-in st-memory-state path) 32 | st-memory-state)))) 33 | 34 | (defn lt-memory-has-value? 35 | [{{:keys [path schema]} :opts 36 | :keys [lt-memory]}] 37 | (let [lt-memory-state (p/latest lt-memory)] 38 | (m/validate 39 | schema 40 | (if path 41 | (get-in lt-memory-state path) 42 | lt-memory-state)))) 43 | 44 | (comment 45 | 46 | (def running-seq 47 | [:sequence 48 | [:action (fn [_] p/running)] 49 | [:action (fn [_] p/success)] 50 | [:action (fn [_] p/failure)]]) 51 | 52 | (def success-seq 53 | [:sequence 54 | [:action (fn [_] p/success)] 55 | [:action (fn [_] p/success)]]) 56 | 57 | (def failure-seq 58 | [:sequence 59 | [:action (fn [_] p/failure)] 60 | [:action (fn [_] p/success)]]) 61 | 62 | (def running-fallback 63 | [:fallback 64 | [:action (fn [_] p/running)] 65 | [:action (fn [_] p/success)] 66 | [:action (fn [_] p/failure)]]) 67 | 68 | (def success-fallback 69 | [:fallback 70 | [:action (fn [_] p/success)] 71 | [:action (fn [_] p/failure)]]) 72 | 73 | (def failure-fallback 74 | [:fallback 75 | [:action (fn [_] p/failure)] 76 | [:action (fn [_] p/failure)]]) 77 | 78 | 79 | 80 | (require '[ai.obney.grain.event-store-v2.interface :as es]) 81 | 82 | (def event-store 83 | (es/start {:conn {:type :in-memory}})) 84 | 85 | 86 | "") -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:aliases {:dev {:extra-paths ["development/src"] 2 | 3 | :extra-deps {org.clojure/clojure {:mvn/version "1.12.0"} 4 | clj-http/clj-http {:mvn/version "3.13.0"} 5 | clj-python/libpython-clj {:mvn/version "2.025"} 6 | 7 | ;; Components 8 | poly/command-processor {:local/root "components/command-processor"} 9 | poly/command-request-handler {:local/root "components/command-request-handler"} 10 | poly/anomalies {:local/root "components/anomalies"} 11 | poly/time {:local/root "components/time"} 12 | poly/schema-util {:local/root "components/schema-util"} 13 | poly/core-async-thread-pool {:local/root "components/core-async-thread-pool"} 14 | poly/webserver {:local/root "components/webserver"} 15 | poly/query-processor {:local/root "components/query-processor"} 16 | poly/query-request-handler {:local/root "components/query-request-handler"} 17 | poly/query-schema {:local/root "components/query-schema"} 18 | poly/event-store-v2 {:local/root "components/event-store-v2"} 19 | poly/event-store-postgres-v2 {:local/root "components/event-store-postgres-v2"} 20 | poly/pubsub {:local/root "components/pubsub"} 21 | poly/todo-processor {:local/root "components/todo-processor"} 22 | poly/periodic-task {:local/root "components/periodic-task"} 23 | poly/mulog-aws-cloudwatch-emf-publisher {:local/root "components/mulog-aws-cloudwatch-emf-publisher"} 24 | poly/event-model {:local/root "components/event-model"} 25 | poly/behavior-tree-v2 {:local/root "components/behavior-tree-v2"} 26 | poly/behavior-tree-v2-dspy-extensions {:local/root "components/behavior-tree-v2-dspy-extensions"} 27 | poly/clj-dspy {:local/root "components/clj-dspy"} 28 | 29 | ;; Examples 30 | poly/example-base {:local/root "bases/example-base"} 31 | poly/example-service {:local/root "components/example-service"}}} 32 | 33 | :test {:extra-paths []} 34 | 35 | :poly {:main-opts ["-m" "polylith.clj.core.poly-cli.core"] 36 | :extra-deps {polylith/clj-poly {:mvn/version "0.2.21"}}}}} 37 | -------------------------------------------------------------------------------- /components/command-processor/src/ai/obney/grain/command_processor/core.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.command-processor.core 2 | (:require 3 | [ai.obney.grain.event-store-v2.interface :as event-store] 4 | [ai.obney.grain.command-processor.interface.schemas :as command-schema] 5 | [ai.obney.grain.anomalies.interface :refer [anomaly?]] 6 | [com.brunobonacci.mulog :as u] 7 | [cognitect.anomalies :as anom] 8 | [malli.core :as mc] 9 | [malli.error :as me])) 10 | 11 | (defn execute-command 12 | [handler {:keys [event-store] :as context}] 13 | (let [result (try 14 | (or (handler context) 15 | {::anom/category ::anom/fault 16 | ::anom/message (format "Command handler returned nil: %s" 17 | (get-in context [:command :command/name]))}) 18 | (catch Exception e 19 | (u/log ::command-handler-exception 20 | :error e 21 | :command (get-in context [:command :command/name])) 22 | {::anom/category ::anom/fault 23 | ::anom/message (format "Error executing command handler: %s" (.getMessage e))}))] 24 | (when (anomaly? result) 25 | (u/log ::error-executing-command ::anomaly result)) 26 | (if-let [events (:command-result/events result)] 27 | (let [event-store-result (event-store/append event-store {:events events})] 28 | (if-not (anomaly? event-store-result) 29 | result 30 | (do 31 | (u/log ::error-storing-events) 32 | {::anom/category ::anom/fault 33 | ::anom/message "Error storing events."}))) 34 | result))) 35 | 36 | (defn process-command [{:keys [command command-registry] :as context}] 37 | (u/trace 38 | ::process-command 39 | [::command command :metric/name "CommandProcessed" :metric/resolution :high] 40 | (let [command-name (:command/name command) 41 | handler (get-in command-registry [command-name :handler-fn])] 42 | (if handler 43 | (if-let [_ (and (mc/validate command-name command) 44 | (mc/validate ::command-schema/command command))] 45 | (let [_ (u/log ::command-started :metric/name "CommandStarted" :metric/value 1 :metric/resolution :high) 46 | result (execute-command handler context) 47 | _ (u/log ::command-finished :metric/name "CommandFinished" :metric/value 1 :metric/resolution :high)] 48 | result) 49 | {::anom/category ::anom/incorrect 50 | ::anom/message "Invalid Command: Failed Schema Validation" 51 | :error/explain (me/humanize (or (mc/explain command-name command) 52 | (mc/explain ::command-schema/command command)))}) 53 | {::anom/category ::anom/not-found 54 | ::anom/message "Unknown Command"})))) 55 | 56 | -------------------------------------------------------------------------------- /components/behavior-tree-v2/src/ai/obney/grain/behavior_tree_v2/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.behavior-tree-v2.interface 2 | (:require [ai.obney.grain.behavior-tree-v2.core.engine :as core] 3 | [ai.obney.grain.behavior-tree-v2.interface.protocol :as p])) 4 | 5 | (def success p/success) 6 | (def failure p/failure) 7 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 8 | (def running p/running) 9 | 10 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 11 | (defn build 12 | "Given a Behavior Tree `config` and a `context`, returns 13 | a built Behavior Tree that can be executed with `run`. 14 | 15 | `context` is a map that can include both user defined and special 16 | keys. 17 | 18 | User defined keys can be used as needed in custom condition and action nodes. 19 | 20 | Special keys: 21 | 22 | - `:event-store` - A reified implementation of `ai.obney.grain.event-store-v2.interface.protocol/EventStore` 23 | - `:st-memory` - A map representing the initial short-term memory of the Behavior Tree. Will be wrapped in 24 | an atom internally. 25 | 26 | The following must be supplied together with `:event-store` to be effective, these are for utilizing 27 | long-term memory (e.g. domain events from the event store): 28 | 29 | - `:queries` - A vector of Grain event-store queries as defined by the `ai.obney.grain.event-store-v2.interface.protocol/EventStore` protocol. 30 | - `:read-model-fn` - A function of [initial-state event] that returns a map created by applying an event to the initial state, producing the latest state." 31 | [config context] 32 | (core/build config context)) 33 | 34 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 35 | (defn run 36 | "Run or 'Tick' a behavior tree created with `build`. 37 | 38 | Unless there is an uncaught exception, the result will always be one of: 39 | 40 | - `:success` - The tree has finished executing. 41 | - `:running` - Part of the tree has not finished executing and the tree should be run again. 42 | - `:failure` - There was an anticipated or unanticipated failure within the tree." 43 | [bt] 44 | (core/run bt)) 45 | 46 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 47 | (defn st-memory-has-value? 48 | "Validate a specific location given a malli schema. 49 | 50 | args: 51 | - `:path` - Optional get-in path to a specific location in short-term memory. 52 | - `:schema` - Required malli schema to validate either the entire short-term memory map or the given path." 53 | [{{:keys [_path _schema]} :opts 54 | :keys [_st-memory] 55 | :as args}] 56 | (core/st-memory-has-value? args)) 57 | 58 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 59 | (defn lt-memory-has-value? 60 | "Validate a specific location given a malli schema. 61 | 62 | args: 63 | - `:path` - Optional get-in path to a specific location in short-term memory. 64 | - `:schema` - Required malli schema to validate either the entire short-term memory map or the given path." 65 | [{{:keys [_path _schema]} :opts 66 | :keys [_lt-memory] :as args}] 67 | (core/lt-memory-has-value? args)) -------------------------------------------------------------------------------- /components/event-store-v2/src/ai/obney/grain/event_store_v2/core.cljc: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.event-store-v2.core 2 | (:refer-clojure :exclude [read]) 3 | (:require [ai.obney.grain.event-store-v2.interface.schemas :as schemas] 4 | [ai.obney.grain.event-store-v2.interface.protocol :as p :refer [start-event-store]] 5 | [ai.obney.grain.anomalies.interface :refer [anomaly?]] 6 | [ai.obney.grain.pubsub.interface :as pubsub] 7 | [ai.obney.grain.time.interface :as time] 8 | [malli.core :as mc] 9 | [cognitect.anomalies :as anom] 10 | #?@(:clj [[clj-uuid :as uuid] 11 | [com.brunobonacci.mulog :as u]] 12 | :cljs [[cljs.core :refer [ExceptionInfo]] 13 | ["uuid" :refer [v7]]])) 14 | #?(:clj (:import [clojure.lang ExceptionInfo]))) 15 | 16 | (defmethod start-event-store :default 17 | [{{:keys [type]} :conn}] 18 | (throw (ex-info (str "Unsupported event store type: " type) {:type type}))) 19 | 20 | (defn start 21 | [config] 22 | (assoc-in (start-event-store config) 23 | [:config :event-pubsub] 24 | (:event-pubsub config))) 25 | 26 | (defn stop 27 | [event-store] 28 | (p/stop event-store)) 29 | 30 | (defn append 31 | [{{:keys [event-pubsub]} :config 32 | :as event-store} 33 | {:keys [events] :as args}] 34 | (let [validation-errors 35 | (or 36 | 37 | ;; Invalid arguments 38 | (when-let [validation-error (mc/explain ::schemas/append-args args)] 39 | {::anom/category ::anom/incorrect 40 | ::anom/message "Invalid arguments" 41 | :explain/data validation-error}) 42 | 43 | ;; Schema validation issues 44 | (try (->> events 45 | (mapv #(mc/explain [:and ::schemas/event (:event/type %)] %)) 46 | (filterv (complement nil?))) 47 | (catch ExceptionInfo _ 48 | {::anom/category ::anom/fault 49 | ::anom/message "One or more event schemas are not defined for :event/type" 50 | ::event-names (set (map :event/name events))})))] 51 | (cond 52 | (anomaly? validation-errors) 53 | validation-errors 54 | 55 | (seq validation-errors) 56 | (do 57 | #?(:clj (u/log ::validation-errors :validation-errors validation-errors)) 58 | {::anom/category ::anom/fault 59 | ::anom/message "Invalid Event(s): Failed Schema Validation" 60 | :error/explain validation-errors}) 61 | 62 | :else 63 | (let [result (p/append event-store args)] 64 | (if (anomaly? result) 65 | result 66 | (when event-pubsub 67 | (run! #(pubsub/pub event-pubsub {:message %}) events))))))) 68 | 69 | (defn read 70 | [event-store args] 71 | (if-let [validation-error (mc/explain ::schemas/read-args args)] 72 | {::anom/category ::anom/incorrect 73 | ::anom/message "Invalid arguments" 74 | :explain/data validation-error} 75 | (p/read event-store args))) 76 | 77 | (defn ->event 78 | [{:keys [type body tags] :or {tags #{}} :as args}] 79 | (if-let [validation-error (mc/explain ::schemas/->event-args args)] 80 | {::anom/category ::anom/incorrect 81 | ::anom/message "Invalid arguments" 82 | :explain/data validation-error} 83 | (merge 84 | {:event/id #?(:clj (uuid/v7) 85 | :cljs (uuid (v7))) 86 | :event/timestamp (time/now) 87 | :event/type type 88 | :event/tags tags} 89 | body))) 90 | 91 | (comment 92 | 93 | 94 | 95 | 96 | "") -------------------------------------------------------------------------------- /components/behavior-tree-v2/src/ai/obney/grain/behavior_tree_v2/core/nodes.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.behavior-tree-v2.core.nodes 2 | (:require [ai.obney.grain.behavior-tree-v2.interface.protocol :as p :refer [opts+children]])) 3 | 4 | (defmethod p/tick :default 5 | [node _context] 6 | (throw (ex-info "Node type not implemented" {:node node}))) 7 | 8 | ;; 9 | ;; Sequence 10 | ;; 11 | 12 | (defmethod p/tick :sequence 13 | [node context] 14 | (loop [[child-node :as children] (:children node)] 15 | (if-not child-node 16 | p/success 17 | (let [result (p/tick child-node context)] 18 | (case result 19 | :success (recur (rest children)) 20 | :failure p/failure 21 | :running p/running))))) 22 | 23 | (defmethod p/build :sequence 24 | [node-type args] 25 | (let [[opts children] (opts+children args)] 26 | (assoc opts 27 | :type node-type 28 | :children (mapv #(p/build (first %) (rest %)) children)))) 29 | 30 | ;; 31 | ;; Fallback 32 | ;; 33 | 34 | (defmethod p/tick :fallback 35 | [node context] 36 | (loop [[child-node :as children] (:children node)] 37 | (if-not child-node 38 | p/failure 39 | (let [result (p/tick child-node context)] 40 | (case result 41 | :success p/success 42 | :failure (recur (rest children)) 43 | :running p/running))))) 44 | 45 | (defmethod p/build :fallback 46 | [node-type args] 47 | (let [[opts children] (opts+children args)] 48 | (assoc opts 49 | :type node-type 50 | :children (mapv #(p/build (first %) (rest %)) children)))) 51 | 52 | ;; 53 | ;; Parallel 54 | ;; 55 | 56 | (defmethod p/tick :parallel 57 | [{:keys [success-threshold children] :as _node} context] 58 | (let [success-threshold (or success-threshold (count children)) 59 | futures (mapv #(future (p/tick % context)) children) 60 | results (mapv deref futures) 61 | success-count (count (filter #(= % p/success) results)) 62 | failure-count (count (filter #(= % p/failure) results))] 63 | (cond 64 | (>= success-count success-threshold) p/success 65 | (> failure-count (- (count children) success-threshold)) p/failure 66 | :else p/running))) 67 | 68 | (defmethod p/build :parallel 69 | [node-type args] 70 | (let [[opts children] (opts+children args)] 71 | (assoc opts 72 | :type node-type 73 | :children (mapv #(p/build (first %) (rest %)) children)))) 74 | 75 | ;; 76 | ;; Condition 77 | ;; 78 | 79 | (defmethod p/tick :condition 80 | [{:keys [condition-fn opts] :as _node} context] 81 | (if (condition-fn (assoc context :opts opts)) 82 | p/success 83 | p/failure)) 84 | 85 | (defmethod p/build :condition 86 | [node-type args] 87 | (let [[opts children] (opts+children args)] 88 | {:type node-type 89 | :opts opts 90 | :condition-fn (first children)})) 91 | 92 | ;; 93 | ;; Action 94 | ;; 95 | 96 | (defmethod p/tick :action 97 | [{:keys [action-fn opts] :as _node} context] 98 | (action-fn (assoc context :opts opts))) 99 | 100 | (defmethod p/build :action 101 | [node-type args] 102 | (let [[opts children] (opts+children args)] 103 | {:type node-type 104 | :opts opts 105 | :action-fn (first children)})) 106 | 107 | 108 | 109 | (comment 110 | 111 | (p/tick 112 | {:type :action 113 | :opts {:hello "World"} 114 | :action-fn (fn [context] 115 | (println (:opts context)) 116 | p/success)} 117 | {}) 118 | 119 | 120 | 121 | "") -------------------------------------------------------------------------------- /components/query-request-handler/src/ai/obney/grain/query_request_handler/core.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.query-request-handler.core 2 | (:require [ai.obney.grain.anomalies.interface :refer [anomaly?]] 3 | [ai.obney.grain.query-schema.interface :as query-schema] 4 | [ai.obney.grain.time.interface :as time] 5 | [clojure.core.async :as async] 6 | [com.brunobonacci.mulog :as u] 7 | [cognitect.anomalies :as anom] 8 | [malli.core :as mc] 9 | [malli.error :as me] 10 | [io.pedestal.http.body-params :as body-params] 11 | [ai.obney.grain.query-processor.interface :as qp] 12 | [cognitect.transit :as transit])) 13 | 14 | (defn process-query-result-dispatch 15 | [result] 16 | (::anom/category result)) 17 | 18 | (defmulti process-query-result process-query-result-dispatch) 19 | 20 | (defmethod process-query-result ::anom/incorrect 21 | [{:keys [::anom/message error/explain]}] 22 | {:status 400 23 | :body {:message message 24 | :explain explain}}) 25 | 26 | (defmethod process-query-result ::anom/not-found 27 | [{:keys [::anom/message]}] 28 | {:status 404 29 | :body {:message message}}) 30 | 31 | (defmethod process-query-result ::anom/forbidden 32 | [{:keys [::anom/message]}] 33 | {:status 403 34 | :body {:message message}}) 35 | 36 | (defmethod process-query-result ::anom/conflict 37 | [{:keys [::anom/message]}] 38 | {:status 409 39 | :body {:message message}}) 40 | 41 | (defmethod process-query-result :default 42 | [{:keys [::anom/message]}] 43 | {:status 500 44 | :body {:message message}}) 45 | 46 | (defmethod process-query-result nil 47 | [result] 48 | {:status 200 49 | :body (or (:query/result result) "OK")}) 50 | 51 | (defn decode-query 52 | [query] 53 | (-> query 54 | (assoc :query/id (random-uuid)) 55 | (assoc :query/timestamp (time/now)))) 56 | 57 | (defn prep-response 58 | [response] 59 | (-> response 60 | (assoc-in [:headers "Content-Type"] "application/transit+json") 61 | (update :body (fn [data] 62 | (let [out (java.io.ByteArrayOutputStream.)] 63 | (transit/write (transit/writer out :json) data) 64 | (.toString out)))))) 65 | 66 | (defn handle-query [config {:keys [request] :as context}] 67 | (async/go 68 | (u/trace 69 | ::handle-query 70 | [::request request] 71 | (try 72 | (let [query (decode-query (get-in request [:transit-params :query]))] 73 | (if-let [error (me/humanize (mc/explain ::query-schema/query query))] 74 | (assoc context :response 75 | (prep-response 76 | (process-query-result 77 | {::anom/category ::anom/incorrect 78 | ::anom/message "Invalid Query" 79 | :error/explain error}))) 80 | (let [result (async/ command 54 | (assoc :command/id (random-uuid)) 55 | (assoc :command/timestamp (time/now)))) 56 | 57 | (defn prep-response 58 | [response] 59 | (-> response 60 | (assoc-in [:headers "Content-Type"] "application/transit+json") 61 | (update :body (fn [data] 62 | (let [out (java.io.ByteArrayOutputStream.)] 63 | (transit/write (transit/writer out :json) data) 64 | (.toString out)))))) 65 | 66 | (defn handle-command [grain-context {:keys [request] :as http-context}] 67 | (async/go 68 | (u/trace 69 | ::handle-command 70 | [::request request] 71 | (try 72 | (let [command (decode-command (get-in request [:transit-params :command]))] 73 | (if-let [error (me/humanize (mc/explain ::command-schema/command command))] 74 | (assoc http-context :response 75 | (prep-response 76 | (process-command-result 77 | {::anom/category ::anom/incorrect 78 | ::anom/message "Invalid Command" 79 | :error/explain error}))) 80 | (let [result (async/ result process-command-result prep-response) 87 | :grain/command command 88 | :grain/command-result result)))) 89 | (catch Exception e (u/log ::error :error e)))))) 90 | 91 | (defn interceptor 92 | [config] 93 | {:name ::command-request-handler 94 | :enter (partial #'handle-command config)}) 95 | 96 | (defn routes 97 | [{:keys [_event-store] :as config}] 98 | #{["/command" :post [(body-params/body-params) (interceptor config)] :route-name :command]}) 99 | -------------------------------------------------------------------------------- /components/example-service/src/ai/obney/grain/example_service/core/commands.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-service.core.commands 2 | "The core commands namespace in a grain service component implements 3 | the command handlers and defines the command registry. Command functions 4 | take a context that includes any necessary dependencies, to be injected 5 | in the base for the service. Usually a command-request-handler or another 6 | type of adapter will call the command processor, which will access the command 7 | registry for the entire application in the context. Commands either return a cognitect 8 | anomaly or a map that optionally has a :command-result/events key containing a sequence of 9 | valid events per the event-store event schema and optionally :command/result which is some 10 | data that is meant to be returned to the caller, see command-request-handler for example." 11 | (:require [ai.obney.grain.example-service.interface.read-models :as read-models] 12 | [ai.obney.grain.event-store-v2.interface :refer [->event]] 13 | [cognitect.anomalies :as anom])) 14 | 15 | (defn create-counter 16 | "Creates a new counter. Counter name must be unique." 17 | [context] 18 | (let [counter-name (get-in context [:command :name]) 19 | counter-id (random-uuid) 20 | unique-counter-names (->> (read-models/root context) 21 | vals 22 | (map :counter/name) 23 | set)] 24 | (if (contains? unique-counter-names counter-name) 25 | {::anom/category ::anom/conflict 26 | ::anom/message (format "Counter with name '%s' already exists." counter-name)} 27 | {:command-result/events 28 | [(->event {:type :example/counter-created 29 | :tags #{[:counter counter-id]} 30 | :body {:counter-id counter-id 31 | :name counter-name}})]}))) 32 | 33 | (defn increment-counter 34 | "Increments an existing counter by 1." 35 | [{{:keys [counter-id]} :command :as context}] 36 | (let [state (read-models/root context)] 37 | (if (get state counter-id) 38 | {:command-result/events 39 | [(->event {:type :example/counter-incremented 40 | :tags #{[:counter counter-id]} 41 | :body {:counter-id counter-id}})]} 42 | {::anom/category ::anom/not-found 43 | ::anom/message (format "Counter with ID '%s' not found." counter-id)}))) 44 | 45 | (defn decrement-counter 46 | "Decrements an existing counter by 1." 47 | [{{:keys [counter-id]} :command :as context}] 48 | (let [state (read-models/root context)] 49 | (if (get state counter-id) 50 | {:command-result/events 51 | [(->event {:type :example/counter-decremented 52 | :tags #{[:counter counter-id]} 53 | :body {:counter-id counter-id}})]} 54 | {::anom/category ::anom/not-found 55 | ::anom/message (format "Counter with ID '%s' not found." counter-id)}))) 56 | 57 | (defn calculate-average-counter-value 58 | "Calculates the average value of all initialized counters." 59 | [context] 60 | (let [state (->> (read-models/root context) 61 | (filter (fn [[_ v]] (:counter/value v))) 62 | (into {}))] 63 | {:command-result/events 64 | [(->event 65 | {:type :example/average-calculated 66 | :body {:value (/ (double (->> state 67 | vals 68 | (map :counter/value) 69 | (reduce + 0))) 70 | (double (count state)))}})]})) 71 | 72 | (def commands 73 | {:example/create-counter {:handler-fn #'create-counter} 74 | :example/increment-counter {:handler-fn #'increment-counter} 75 | :example/decrement-counter {:handler-fn #'decrement-counter} 76 | :example/calculate-average-counter-value {:handler-fn #'calculate-average-counter-value}}) -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/config.edn: -------------------------------------------------------------------------------- 1 | {:hooks 2 | {:macroexpand {tech.v3.datatype/make-reader 3 | tech.v3.datatype/make-reader 4 | tech.v3.datatype-api/make-reader 5 | tech.v3.datatype-api/make-reader 6 | tech.v3.datatype.export-symbols/export-symbols 7 | tech.v3.datatype.export-symbols/export-symbols 8 | tech.v3.datatype.binary-op/make-numeric-object-binary-op 9 | tech.v3.datatype.binary-op/make-numeric-object-binary-op 10 | tech.v3.datatype.binary-op/make-float-double-binary-op 11 | tech.v3.datatype.binary-op/make-float-double-binary-op 12 | tech.v3.datatype.binary-op/make-int-long-binary-op 13 | tech.v3.datatype.binary-op/make-int-long-binary-op 14 | tech.v3.datatype.binary-pred/make-boolean-predicate 15 | tech.v3.datatype.binary-pred/make-boolean-predicate 16 | tech.v3.datatype.binary-pred/make-numeric-binary-predicate 17 | tech.v3.datatype.binary-pred/make-numeric-binary-predicate 18 | tech.v3.parallel.for/doiter tech.v3.parallel.for/doiter 19 | tech.v3.parallel.for/parallel-for tech.v3.parallel.for/parallel-for 20 | tech.v3.datatype.ffi/define-library! 21 | tech.v3.datatype.ffi/define-library! 22 | tech.v3.datatype.statistics/define-descriptive-stats 23 | tech.v3.datatype.statistics/define-descriptive-stats 24 | tech.v3.datatype.functional-api/implement-arithmetic-operations 25 | tech.v3.datatype.functional-api/implement-arithmetic-operations 26 | tech.v3.datatype.functional-api/implement-unary-predicates 27 | tech.v3.datatype.functional-api/implement-unary-predicates 28 | tech.v3.datatype.functional-api/implement-binary-predicates 29 | tech.v3.datatype.functional-api/implement-binary-predicates 30 | tech.v3.datatype.functional-api/implement-compare-predicates 31 | tech.v3.datatype.functional-api/implement-compare-predicates 32 | tech.v3.datatype.gradient/append-diff 33 | tech.v3.datatype.gradient/append-diff 34 | tech.v3.datatype.gradient/prepend-diff 35 | tech.v3.datatype.gradient/prepend-diff 36 | tech.v3.datatype.gradient/basic-diff 37 | tech.v3.datatype.gradient/basic-diff 38 | tech.v3.datatype.jvm-map/bi-consumer 39 | tech.v3.datatype.jvm-map/bi-consumer 40 | tech.v3.datatype.jvm-map/bi-function 41 | tech.v3.datatype.jvm-map/bi-function 42 | tech.v3.datatype.jvm-map/function 43 | tech.v3.datatype.jvm-map/function 44 | taoensso.nippy/extend-freeze 45 | taoensso.nippy/extend-freeze 46 | taoensso.nippy/extend-thaw 47 | taoensso.nippy/extend-thaw 48 | tech.v3.datatype.unary-op/make-double-unary-op 49 | tech.v3.datatype.unary-op/make-double-unary-op 50 | tech.v3.datatype.unary-op/make-numeric-object-unary-op 51 | tech.v3.datatype.unary-op/make-numeric-object-unary-op 52 | tech.v3.datatype.unary-op/make-float-double-unary-op 53 | tech.v3.datatype.unary-op/make-float-double-unary-op 54 | tech.v3.datatype.unary-op/make-numeric-unary-op 55 | tech.v3.datatype.unary-op/make-numeric-unary-op 56 | tech.v3.datatype.unary-op/make-long-unary-op 57 | tech.v3.datatype.unary-op/make-long-unary-op 58 | tech.v3.datatype.unary-op/make-all-datatype-unary-op 59 | tech.v3.datatype.unary-op/make-all-datatype-unary-op 60 | tech.v3.tensor-api/typed-compute-tensor 61 | tech.v3.tensor-api/typed-compute-tensor 62 | tech.v3.tensor/typed-compute-tensor 63 | tech.v3.tensor/typed-compute-tensor 64 | }}} 65 | -------------------------------------------------------------------------------- /development/src/example_app_demo.clj: -------------------------------------------------------------------------------- 1 | (ns example-app-demo 2 | (:require [ai.obney.grain.example-base.core :as service] 3 | [ai.obney.grain.command-processor.interface :as cp] 4 | [ai.obney.grain.query-processor.interface :as qp] 5 | [ai.obney.grain.event-store-v2.interface :as es] 6 | [ai.obney.grain.event-store-postgres-v2.interface] 7 | [ai.obney.grain.example-service.interface.read-models :as rm] 8 | [ai.obney.grain.time.interface :as time] 9 | [clj-http.client :as http])) 10 | 11 | (comment 12 | 13 | ;; 14 | ;; Start Service 15 | ;; 16 | (do 17 | (def service (service/start)) 18 | (def context (::service/context service)) 19 | (def event-store (:event-store context))) 20 | 21 | 22 | ;; 23 | ;; Stop Service ;; 24 | ;; 25 | (service/stop service) 26 | 27 | "" 28 | ) 29 | 30 | 31 | (comment 32 | 33 | ;; Interact internally in the REPL with out HTTP 34 | 35 | (try 36 | (cp/process-command 37 | (assoc context 38 | :command {:command/name :example/create-counter 39 | :command/timestamp (time/now) 40 | :command/id (random-uuid) 41 | :name "Counter A"})) 42 | (catch Exception e (ex-data e))) 43 | 44 | (into [] (es/read event-store {})) 45 | 46 | (def counters 47 | (->> (qp/process-query 48 | (assoc context 49 | :query {:query/name :example/counters 50 | :query/timestamp (time/now) 51 | :query/id (random-uuid)})) 52 | :query/result)) 53 | 54 | 55 | (def counter 56 | (->> (qp/process-query 57 | (assoc context 58 | :query {:query/name :example/counter 59 | :query/timestamp (time/now) 60 | :query/id (random-uuid) 61 | :counter-id (:counter/id (first counters))})) 62 | :query/result)) 63 | 64 | 65 | 66 | (cp/process-command 67 | (assoc context 68 | :command {:command/name :example/increment-counter 69 | :command/timestamp (time/now) 70 | :command/id (random-uuid) 71 | :counter-id (:counter/id counter)})) 72 | 73 | 74 | (rm/root context) 75 | 76 | (into [] (es/read event-store {})) 77 | 78 | 79 | "" 80 | ) 81 | 82 | (comment 83 | ;; Interact with the service via HTTP 84 | 85 | ;; Create a counter 86 | (try 87 | (:body 88 | (http/post 89 | "http://localhost:8080/command" 90 | {:content-type :transit+json 91 | :as :transit+json 92 | :form-params {:command {:command/name :example/create-counter 93 | :name "Counter C"}}})) 94 | (catch Exception e (ex-data e))) 95 | 96 | ;; Get all counters 97 | (def counters 98 | (try 99 | (:body 100 | (http/post 101 | "http://localhost:8080/query" 102 | {:content-type :transit+json 103 | :as :transit+json 104 | :form-params {:query {:query/name :example/counters}}})) 105 | (catch Exception e (ex-data e)))) 106 | 107 | 108 | 109 | ;; Increment first counter 110 | 111 | (try 112 | (:body 113 | (http/post 114 | "http://localhost:8080/command" 115 | {:content-type :transit+json 116 | :as :transit+json 117 | :form-params {:command {:command/name :example/increment-counter 118 | :counter-id (-> counters first :counter/id)}}})) 119 | (catch Exception e (ex-data e))) 120 | 121 | ;; Decrement a counter by ID 122 | 123 | (try 124 | (:body 125 | (http/post 126 | "http://localhost:8080/command" 127 | {:content-type :transit+json 128 | :as :transit+json 129 | :form-params {:command {:command/name :example/decrement-counter 130 | :counter-id (-> counters first :counter/id)}}})) 131 | (catch Exception e (ex-data e))) 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | "" 140 | ) -------------------------------------------------------------------------------- /components/behavior-tree-v2/src/ai/obney/grain/behavior_tree_v2/core/long_term_memory.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.behavior-tree-v2.core.long-term-memory 2 | (:require [ai.obney.grain.behavior-tree-v2.interface.protocol :as p] 3 | [ai.obney.grain.event-store-v2.interface :as es])) 4 | 5 | (defn latest 6 | "Return the latest long-term memory read model as a map. 7 | 8 | 9 | 10 | args: 11 | - config: 12 | - event-store: The event store to read from. 13 | - read-model-fn: Function of signature [initial-state events] to build the read model from events. 14 | - queries: Vector of queries to run against the event store. 15 | - state: Atom containing: 16 | - latest-event-id: Optional event ID to start reading from (for incremental updates). 17 | - snapshot: Initial state atom to use for the read model. 18 | " 19 | [{{:keys [event-store read-model-fn queries]} :config 20 | :keys [state] :as _this}] 21 | (when-not (and event-store read-model-fn queries) 22 | (throw (ex-info "Long Term Memory not configured" {}))) 23 | (let [{:keys [snapshot latest-event-id]} @state 24 | events (mapcat 25 | (fn [query] 26 | (into [] (es/read 27 | event-store 28 | (cond-> query 29 | latest-event-id (assoc :after latest-event-id))))) 30 | queries) 31 | read-model (read-model-fn (or snapshot {}) events)] 32 | (swap! state assoc 33 | :snapshot read-model 34 | :latest-event-id (if (seq events) (->> events last :event/id) latest-event-id)) 35 | read-model)) 36 | 37 | (defrecord LongTermMemoryEventStore [config] 38 | p/LongTermMemory 39 | (latest [this] 40 | (latest this))) 41 | 42 | (defn ->long-term-memory 43 | [config] 44 | (assoc (->LongTermMemoryEventStore config) 45 | :state (atom {}))) 46 | 47 | (comment 48 | 49 | (require '[ai.obney.grain.schema-util.interface :refer [defschemas]]) 50 | 51 | (defschemas events 52 | {:counter-created 53 | [:map 54 | [:counter-id :uuid]] 55 | 56 | :counter-incremented 57 | [:map 58 | [:counter-id :uuid]] 59 | 60 | :counter-decremented 61 | [:map 62 | [:counter-id :uuid]]}) 63 | 64 | (def event-store 65 | (es/start {:conn {:type :in-memory}})) 66 | 67 | (def counter-id (random-uuid)) 68 | 69 | (es/append 70 | event-store 71 | {:events 72 | [(es/->event 73 | {:type :counter-created 74 | :tags #{[:counter counter-id]} 75 | :body {:counter-id counter-id}}) 76 | 77 | (es/->event 78 | {:type :counter-incremented 79 | :tags #{[:counter counter-id]} 80 | :body {:counter-id counter-id}}) 81 | 82 | (es/->event 83 | {:type :counter-incremented 84 | :tags #{[:counter counter-id]} 85 | :body {:counter-id counter-id}}) 86 | 87 | (es/->event 88 | {:type :counter-decremented 89 | :tags #{[:counter counter-id]} 90 | :body {:counter-id counter-id}})]}) 91 | 92 | 93 | (def ltm 94 | (->long-term-memory 95 | {:event-store event-store 96 | :read-model-fn 97 | (fn [initial-state events] 98 | (reduce (fn [state event] 99 | (case (:event/type event) 100 | :counter-created (assoc state 101 | :counter-id (:counter-id event) 102 | :count 0) 103 | :counter-incremented (update state :count inc) 104 | :counter-decremented (update state :count dec) 105 | :grain/tx (update state :tx/log conj event) 106 | state)) 107 | initial-state 108 | events)) 109 | :queries 110 | [{:types #{:counter-created 111 | :counter-incremented 112 | :counter-decremented} 113 | :tags #{[:counter counter-id]}} 114 | {:types #{:grain/tx}}]})) 115 | 116 | (p/latest ltm) 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | "" 126 | ) -------------------------------------------------------------------------------- /components/event-model/src/ai/obney/grain/event_model/interface.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.event-model.interface 2 | (:require [ai.obney.grain.schema-util.interface :refer [defschemas]] 3 | [malli.core :as m])) 4 | 5 | #_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} 6 | (defschemas event-model 7 | {:command-name [:fn #(and (qualified-keyword? %) 8 | (= "command" (namespace %)))] 9 | 10 | :event-name [:fn #(and (qualified-keyword? %) 11 | (= "event" (namespace %)))] 12 | 13 | :view-name [:fn #(and (qualified-keyword? %) 14 | (= "view" (namespace %)))] 15 | 16 | :todo-processor-name [:fn #(and (qualified-keyword? %) 17 | (= "todo-processor" (namespace %)))] 18 | 19 | :screen-name [:fn #(and (qualified-keyword? %) 20 | (= "screen" (namespace %)))] 21 | 22 | :periodic-task-name [:fn #(and (qualified-keyword? %) 23 | (= "periodic-task" (namespace %)))] 24 | 25 | :flow-name [:fn #(and (qualified-keyword? %) 26 | (= "flow" (namespace %)))] 27 | 28 | :malli-schema [:fn #(m/schema? 29 | (try 30 | (m/schema %) 31 | (catch Exception _e)))] 32 | 33 | :given-when-then [:map 34 | [:given :string] 35 | [:when :string] 36 | [:then :string]] 37 | 38 | :given-when-thens [:vector :given-when-then] 39 | 40 | :command [:map 41 | [:name :command-name] 42 | [:description :string] 43 | [:given-when-thens {:optional true} :given-when-thens] 44 | [:schema :malli-schema]] 45 | 46 | :event [:map 47 | [:name :event-name] 48 | [:description :string] 49 | [:schema :malli-schema]] 50 | 51 | :view [:map 52 | [:name :view-name] 53 | [:description :string] 54 | [:schema :malli-schema]] 55 | 56 | :todo-processor [:map 57 | [:name :todo-processor-name] 58 | [:description :string]] 59 | 60 | :periodic-task [:map 61 | [:name :periodic-task-name] 62 | [:description :string] 63 | [:schedule :string]] 64 | 65 | :screen [:map 66 | [:name :screen-name] 67 | [:description :string]] 68 | 69 | :commands [:map-of :command-name :command] 70 | 71 | :events [:map-of :event-name :event] 72 | 73 | :views [:map-of :view-name :view] 74 | 75 | :todo-processors [:map-of :todo-processor-name :todo-processor] 76 | 77 | :periodic-tasks [:map-of :periodic-task-name :periodic-task] 78 | 79 | :screens [:map-of :screen-name :screen] 80 | 81 | :valid-step [:fn #(let [connect-from-type (when (:from %) (namespace (:from %))) 82 | connect-to-type (when (:to %) (namespace (:to %)))] 83 | (case connect-from-type 84 | "view" (contains? #{"todo-processor" "screen" "periodic-task" nil} connect-to-type) 85 | "todo-processor" (contains? #{"command" nil} connect-to-type) 86 | "screen" (contains? #{"command" nil} connect-to-type) 87 | "command" (contains? #{"event" nil} connect-to-type) 88 | "event" (contains? #{"view" nil} connect-to-type) 89 | "periodic-task" (contains? #{"command" nil} connect-to-type) 90 | nil true))] 91 | 92 | :step [:and 93 | :valid-step 94 | [:map 95 | [:from [:or 96 | :command-name 97 | :event-name 98 | :view-name 99 | :todo-processor-name 100 | :screen-name 101 | :periodic-task-name 102 | :nil]] 103 | [:to [:or 104 | :command-name 105 | :event-name 106 | :view-name 107 | :todo-processor-name 108 | :periodic-task-name 109 | :screen-name 110 | :nil]]]] 111 | 112 | :flow [:map 113 | [:name :flow-name] 114 | [:description :string] 115 | [:steps [:vector :step]]] 116 | 117 | :flows [:map-of :flow-name :flow] 118 | 119 | :event-model 120 | [:map 121 | [:commands {:optional true} :commands] 122 | [:events {:optional true} :events] 123 | [:views {:optional true} :views] 124 | [:todo-processors {:optional true} :todo-processors] 125 | [:periodic-tasks {:optional true} :periodic-tasks] 126 | [:screens {:optional true} :screens] 127 | [:flows {:optional true} :flows]]}) 128 | 129 | -------------------------------------------------------------------------------- /components/todo-processor/src/ai/obney/grain/todo_processor/core.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.todo-processor.core 2 | (:require [cognitect.anomalies :as anom] 3 | [com.brunobonacci.mulog :as u] 4 | [ai.obney.grain.event-store-v2.interface.schemas] 5 | [ai.obney.grain.event-store-v2.interface :as event-store] 6 | [ai.obney.grain.pubsub.interface :as pubsub] 7 | [ai.obney.grain.anomalies.interface :refer [anomaly?]] 8 | [integrant.core :as ig] 9 | [clojure.core.async :as async] 10 | [ai.obney.grain.core-async-thread-pool.interface :as thread-pool])) 11 | 12 | (defn process-event 13 | [{:keys [handler-fn event event-store] :as context}] 14 | (u/log ::process-event :event event) 15 | (u/trace 16 | ::processing-event 17 | [:event event :metric/name "TodoProcessed" :metric/resolution :high] 18 | (try 19 | (let [_ (u/log :metric/metric :metric/name "TodoStarted" :metric/value 1 :metric/resolution :high) 20 | result (or (handler-fn context) 21 | {::anom/category ::anom/fault 22 | ::anom/message "Todo Processor returned nil: %s"}) 23 | _ (u/log :metric/metric :metric/name "TodoFinished" :metric/value 1 :metric/resolution :high)] 24 | (if (anomaly? result) 25 | (u/log ::anomaly-in-todo-processor :anomaly result) 26 | (when-let [events (:result/events result)] 27 | (let [event-store-result (event-store/append event-store {:events events})] 28 | (when (anomaly? event-store-result) 29 | (u/log ::error-storing-events) 30 | {::anom/category ::anom/fault 31 | ::anom/message "Error storing events."}))))) 32 | (catch Throwable t 33 | (u/log ::uncaught-exception-in-todo-processor :exception t))))) 34 | 35 | (def ^:private system 36 | {::handler-fn {} 37 | ::topics {} 38 | ::event-sub {:event-pubsub (ig/ref ::event-pubsub) 39 | :in-chan (ig/ref ::in-chan) 40 | :topics (ig/ref ::topics)} 41 | ::event-pubsub {} 42 | ::context {} 43 | ::execution-fn {:context (ig/ref ::context) 44 | :handler-fn (ig/ref ::handler-fn)} 45 | ::in-chan {:size 1024} 46 | ::thread-pool {:thread-count 1 47 | :error-fn (fn [e] (u/log ::error ::error e)) 48 | :in-chan (ig/ref ::in-chan) 49 | :execution-fn (ig/ref ::execution-fn)}}) 50 | 51 | (defmethod ig/init-key ::context [_ config] 52 | config) 53 | 54 | (defmethod ig/init-key ::in-chan [_ config] 55 | (u/log ::starting-in-chan config) 56 | (async/chan (:size config))) 57 | 58 | (defmethod ig/halt-key! ::in-chan [_ in-chan] 59 | (u/log ::stopping-in-chan in-chan) 60 | (async/close! in-chan)) 61 | 62 | (defmethod ig/init-key ::execution-fn [_ {:keys [context handler-fn]}] 63 | (u/log ::starting-execution-fn) 64 | (fn [event] 65 | (async/thread 66 | (try (process-event (assoc context :event event :handler-fn handler-fn)) 67 | (catch Throwable t 68 | {::anom/category ::anom/fault 69 | ::anom/message "Error processing message" 70 | :exception t}))))) 71 | 72 | (defmethod ig/init-key ::thread-pool [_ config] 73 | (u/log ::starting-thread-pool config) 74 | (thread-pool/start config)) 75 | 76 | (defmethod ig/halt-key! ::thread-pool [_ thread-pool] 77 | (u/log ::stopping-thread-pool thread-pool) 78 | (thread-pool/stop thread-pool)) 79 | 80 | (defmethod ig/init-key ::event-pubsub [_ event-pubsub] 81 | event-pubsub) 82 | 83 | (defmethod ig/init-key ::event-sub [_ {:keys [event-pubsub in-chan topics]}] 84 | (run! #(pubsub/sub 85 | event-pubsub 86 | {:sub-chan in-chan 87 | :topic %}) 88 | topics)) 89 | 90 | (defmethod ig/init-key ::handler-fn [_ config] 91 | config) 92 | 93 | (defmethod ig/init-key ::topics [_ config] 94 | config) 95 | 96 | (defn start 97 | [config] 98 | (ig/init (merge system 99 | {::context (:context config) 100 | ::event-pubsub (:event-pubsub config) 101 | ::handler-fn (:handler-fn config) 102 | ::topics (:topics config)}))) 103 | 104 | (defn stop 105 | [todo-processor] 106 | (ig/halt! todo-processor)) 107 | 108 | (comment 109 | 110 | (def pubsub 111 | (pubsub/start {:type :core-async 112 | :topic-fn :event/name})) 113 | 114 | (def processor 115 | (start {:context {:hello :world} 116 | :handler-fn #(u/log ::BLAH :x %) 117 | :topics [:foo] 118 | :event-pubsub pubsub})) 119 | 120 | (stop processor) 121 | 122 | (pubsub/pub pubsub {:message {:event/name :foo}}) 123 | 124 | (u/start-publisher! {:type :console :pretty? true}) 125 | 126 | 127 | "" 128 | ) 129 | -------------------------------------------------------------------------------- /.clj-kondo/imports/cnuernber/dtype-next/tech/v3/datatype/functional_api.clj: -------------------------------------------------------------------------------- 1 | (ns tech.v3.datatype.functional-api 2 | (:require [clojure.set :as set])) 3 | 4 | (def init-binary-ops 5 | #{:tech.numerics/hypot 6 | :tech.numerics/bit-xor 7 | :tech.numerics/unsigned-bit-shift-right 8 | :tech.numerics/quot 9 | :tech.numerics/atan2 10 | :tech.numerics/* 11 | :tech.numerics/min 12 | :tech.numerics/- 13 | :tech.numerics/pow 14 | :tech.numerics/bit-test 15 | :tech.numerics/bit-and 16 | :tech.numerics/rem 17 | :tech.numerics/max 18 | :tech.numerics/bit-or 19 | :tech.numerics// 20 | :tech.numerics/bit-flip 21 | :tech.numerics/+ 22 | :tech.numerics/bit-shift-left 23 | :tech.numerics/bit-clear 24 | :tech.numerics/ieee-remainder 25 | :tech.numerics/bit-shift-right 26 | :tech.numerics/bit-set 27 | :tech.numerics/bit-and-not}) 28 | 29 | (def init-unary-ops 30 | #{:tech.numerics/tanh 31 | :tech.numerics/sq 32 | :tech.numerics/expm1 33 | :tech.numerics/log10 34 | :tech.numerics/cos 35 | :tech.numerics/tan 36 | :tech.numerics/atan 37 | :tech.numerics/sqrt 38 | :tech.numerics/cosh 39 | :tech.numerics/get-significand 40 | :tech.numerics/- 41 | :tech.numerics/next-up 42 | :tech.numerics/cbrt 43 | :tech.numerics/next-down 44 | :tech.numerics/exp 45 | :tech.numerics/log1p 46 | :tech.numerics// 47 | :tech.numerics/asin 48 | :tech.numerics/sinh 49 | :tech.numerics/rint 50 | :tech.numerics/+ 51 | :tech.numerics/bit-not 52 | :tech.numerics/signum 53 | :tech.numerics/abs 54 | :tech.numerics/ulp 55 | :tech.numerics/sin 56 | :tech.numerics/to-radians 57 | :tech.numerics/acos 58 | :tech.numerics/ceil 59 | :tech.numerics/to-degrees 60 | :tech.numerics/identity 61 | :tech.numerics/logistic 62 | :tech.numerics/log 63 | :tech.numerics/floor}) 64 | 65 | 66 | (defmacro implement-arithmetic-operations 67 | [] 68 | (let [binary-ops init-binary-ops 69 | unary-ops init-unary-ops 70 | dual-ops (set/intersection binary-ops unary-ops) 71 | unary-ops (set/difference unary-ops dual-ops)] 72 | `(do 73 | ~@(->> 74 | unary-ops 75 | (map 76 | (fn [opname] 77 | (let [op-sym (symbol (name opname))] 78 | `(defn ~op-sym 79 | ([~'x ~'options] 80 | (apply + ~'options) 81 | ~'x) 82 | ([~'x] 83 | ~'x)))))) 84 | ~@(->> 85 | binary-ops 86 | (map 87 | (fn [opname] 88 | (let [op-sym (symbol (name opname)) 89 | dual-op? (dual-ops opname)] 90 | (if dual-op? 91 | `(defn ~op-sym 92 | ([~'x] 93 | ~'x) 94 | ([~'x ~'y] 95 | [~'x ~'y]) 96 | ([~'x ~'y & ~'args] 97 | [~'x ~'y ~'args])) 98 | `(defn ~op-sym 99 | ([~'x ~'y] 100 | [~'x ~'y]) 101 | ([~'x ~'y & ~'args] 102 | [~'x ~'y ~'args])))))))))) 103 | 104 | 105 | (def init-unary-pred-ops 106 | [:tech.numerics/mathematical-integer? 107 | :tech.numerics/even? 108 | :tech.numerics/infinite? 109 | :tech.numerics/zero? 110 | :tech.numerics/not 111 | :tech.numerics/odd? 112 | :tech.numerics/finite? 113 | :tech.numerics/pos? 114 | :tech.numerics/nan? 115 | :tech.numerics/neg?]) 116 | 117 | 118 | (defmacro implement-unary-predicates 119 | [] 120 | `(do 121 | ~@(->> init-unary-pred-ops 122 | (map (fn [pred-op] 123 | (let [fn-symbol (symbol (name pred-op))] 124 | `(defn ~fn-symbol 125 | ([~'arg ~'_options] 126 | ~'arg) 127 | ([~'arg] 128 | ~'arg)))))))) 129 | 130 | 131 | (def init-binary-pred-ops 132 | [:tech.numerics/and 133 | :tech.numerics/or 134 | :tech.numerics/eq 135 | :tech.numerics/not-eq]) 136 | 137 | 138 | (defmacro implement-binary-predicates 139 | [] 140 | `(do 141 | ~@(->> init-binary-pred-ops 142 | (map (fn [pred-op] 143 | (let [fn-symbol (symbol (name pred-op))] 144 | `(defn ~fn-symbol 145 | [~'lhs ~'rhs] 146 | (apply + ~'lhs ~'rhs) 147 | ~'lhs))))))) 148 | 149 | 150 | (def init-binary-pred-comp-ops 151 | [:tech.numerics/> 152 | :tech.numerics/>= 153 | :tech.numerics/< 154 | :tech.numerics/<=]) 155 | 156 | 157 | (defmacro implement-compare-predicates 158 | [] 159 | `(do 160 | ~@(->> init-binary-pred-comp-ops 161 | (map (fn [pred-op] 162 | (let [fn-symbol (symbol (name pred-op)) 163 | k pred-op] 164 | `(defn ~fn-symbol 165 | ([~'lhs ~'rhs] 166 | (apply + [~'lhs ~'rhs]) 167 | ~'lhs) 168 | ([~'lhs ~'mid ~'rhs] 169 | (apply + [~'lhs ~'mid ~'rhs]) 170 | ~'lhs)))))))) 171 | -------------------------------------------------------------------------------- /components/behavior-tree-v2-dspy-extensions/src/ai/obney/grain/behavior_tree_v2_dspy_extensions/core.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.behavior-tree-v2-dspy-extensions.core 2 | (:require [ai.obney.grain.behavior-tree-v2.interface :as bt2] 3 | [ai.obney.grain.behavior-tree-v2.interface.protocol :as btp] 4 | [ai.obney.grain.event-store-v2.interface :as event-store] 5 | [ai.obney.grain.schema-util.interface :refer [defschemas]] 6 | [libpython-clj2.python :as py :refer [py.-]] 7 | [libpython-clj2.require :refer [require-python]] 8 | [clojure.walk :as walk])) 9 | 10 | ;; Initialize Python and import DSPy 11 | (require-python '[dspy :as dspy]) 12 | 13 | ;; ## DSPY Integration 14 | 15 | (defn extract-signature-metadata 16 | "Extract input and output keys from a signature var's metadata" 17 | [signature-var] 18 | (let [metadata (meta signature-var) 19 | dspy-meta (get metadata :dspy/signature) 20 | inputs (keys (:inputs dspy-meta)) 21 | outputs (keys (:outputs dspy-meta))] 22 | {:input-keys inputs 23 | :output-keys outputs})) 24 | 25 | (defmulti execute-dspy-operation 26 | "Execute DSPy operation: create module, run it, and emit events" 27 | (fn [operation _signature _context _inputs] operation)) 28 | 29 | (defn extract-outputs-from-result 30 | "Extract outputs from DSPy result and convert to Clojure data" 31 | [result context] 32 | (let [{:keys [signature]} (:opts context) 33 | {:keys [output-keys]} (extract-signature-metadata signature) 34 | result* (py.- result :outputs)] 35 | (reduce (fn [acc output-key] 36 | (let [field-name (name output-key) 37 | output-value (py/get-attr result* field-name) 38 | clj-value (let [v (py/->jvm output-value)] 39 | (cond 40 | (map? v) (walk/keywordize-keys v) 41 | :else v))] 42 | (assoc acc output-key clj-value))) 43 | {} output-keys))) 44 | 45 | (defschemas agent-events 46 | {:grain.agent/predicted 47 | [:map 48 | [:node-id :keyword] 49 | [:inputs :map] 50 | [:outputs :map]] 51 | 52 | :grain.agent/reasoned 53 | [:map 54 | [:node-id :keyword] 55 | [:inputs :map] 56 | [:outputs :map] 57 | [:reasoning :string]]}) 58 | 59 | (defmethod execute-dspy-operation :predict [_ signature context inputs] 60 | (let [python-signature (if (var? signature) @signature signature) 61 | dspy-module (dspy/Predict python-signature) 62 | result (apply dspy-module (apply concat inputs)) 63 | outputs (extract-outputs-from-result result context)] 64 | 65 | ;; Emit predicted event 66 | (when-let [event-store (:event-store context)] 67 | (when-let [node-id (get-in context [:opts :id])] 68 | (let [event (event-store/->event 69 | {:type :grain.agent/predicted 70 | :body {:node-id node-id 71 | :inputs inputs 72 | :outputs outputs}})] 73 | (event-store/append event-store {:events [event]})))) 74 | 75 | {:outputs outputs})) 76 | 77 | (defmethod execute-dspy-operation :chain-of-thought [_ signature context inputs] 78 | (let [python-signature (if (var? signature) @signature signature) 79 | dspy-module (dspy/ChainOfThought python-signature) 80 | result (apply dspy-module (apply concat inputs)) 81 | outputs (extract-outputs-from-result result context) 82 | reasoning (py.- result :reasoning)] 83 | 84 | ;; Emit reasoned event 85 | (when-let [event-store (:event-store context)] 86 | (when-let [node-id (get-in context [:opts :id])] 87 | (let [event (event-store/->event 88 | {:type :grain.agent/reasoned 89 | :body {:node-id node-id 90 | :inputs inputs 91 | :outputs outputs 92 | :reasoning reasoning}})] 93 | (event-store/append event-store {:events [event]})))) 94 | 95 | {:outputs outputs})) 96 | 97 | (defn dspy [{{:keys [id signature operation]} :opts 98 | :keys [st-memory] 99 | :as context}] 100 | (let [{:keys [input-keys]} (extract-signature-metadata signature)] 101 | (try 102 | (let [state (cond-> @st-memory 103 | (:lt-memory context) (merge (btp/latest (:lt-memory context)))) 104 | inputs (reduce (fn [acc key] 105 | (let [value (get state key)] 106 | (if value 107 | (assoc acc key value) 108 | acc))) 109 | {} input-keys)] 110 | (if inputs 111 | (let [result (execute-dspy-operation operation signature context {:inputs inputs})] 112 | (doseq [[output-key output-value] (:outputs result)] 113 | (swap! st-memory assoc output-key output-value)) 114 | (println (str "✓ " id) "completed successfully") 115 | bt2/success) 116 | (do 117 | (println (str "✗ " id " - missing inputs: " (pr-str input-keys))) 118 | bt2/failure))) 119 | (catch Exception e 120 | (println (str "✗ " id " error:") (.getMessage e)) 121 | bt2/failure)))) 122 | -------------------------------------------------------------------------------- /bases/example-base/src/ai/obney/grain/example_base/core.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.example-base.core 2 | (:require [ai.obney.grain.command-request-handler.interface :as crh] 3 | [ai.obney.grain.query-request-handler.interface :as qrh] 4 | [ai.obney.grain.periodic-task.interface :as pt] 5 | [ai.obney.grain.event-store-v2.interface :as es] 6 | [ai.obney.grain.event-store-postgres-v2.interface] 7 | [ai.obney.grain.webserver.interface :as ws] 8 | [ai.obney.grain.pubsub.interface :as ps] 9 | [ai.obney.grain.todo-processor.interface :as tp] 10 | [ai.obney.grain.mulog-aws-cloudwatch-emf-publisher.interface :as cloudwatch-emf] 11 | [clojure.set :as set] 12 | [com.brunobonacci.mulog :as u] 13 | [integrant.core :as ig] 14 | [nrepl.server :as nrepl] 15 | 16 | [ai.obney.grain.example-service.interface 17 | [commands :as commands] 18 | [queries :as queries] 19 | [todo-processors :as todo-processors] 20 | [periodic-tasks :as periodic-tasks] 21 | [schemas]])) 22 | 23 | ;; --------------------- ;; 24 | ;; Service Configuration ;; 25 | ;; --------------------- ;; 26 | 27 | ;; 28 | ;; This will be deleted later, just for testing ;; 29 | ;; 30 | 31 | 32 | (def system 33 | {::logger {} 34 | ::event-store {:logger (ig/ref ::logger) 35 | :event-pubsub (ig/ref ::event-pubsub) 36 | :conn {:type :in-memory ;; change to :postgres to try Postgres 37 | ;; uncomment below to try Postgres 38 | #_#_#_#_#_#_#_#_#_#_:server-name "localhost" 39 | :port-number "5433" 40 | :username "postgres" 41 | :password "password" 42 | :database-name "obneyai"}} 43 | 44 | ::event-pubsub {:type :core-async 45 | :topic-fn :event/type} 46 | 47 | ::example-periodic-task 48 | {:handler-fn #'periodic-tasks/example-periodic-task 49 | :schedule {:every 30 :duration :seconds} 50 | :context (ig/ref ::context) 51 | :task-name ::example-periodic-task} 52 | 53 | ::calculate-average-counter-value-todo-processor 54 | {:event-pubsub (ig/ref ::event-pubsub) 55 | :topics [:example/counter-incremented :example/counter-decremented] 56 | :handler-fn #'todo-processors/calculate-average-counter-value 57 | :context (ig/ref ::context)} 58 | 59 | ::context {:event-store (ig/ref ::event-store) 60 | :command-registry commands/commands 61 | :query-registry queries/queries 62 | :event-pubsub (ig/ref ::event-pubsub)} 63 | 64 | ::routes {:context (ig/ref ::context)} 65 | 66 | ::webserver {:http/routes (ig/ref ::routes) 67 | :http/port 8080 68 | :http/join? false} 69 | 70 | ::nrepl {:bind "0.0.0.0" :port 7888}}) 71 | 72 | 73 | 74 | ;; -------------- ;; 75 | ;; Integrant Keys ;; 76 | ;; -------------- ;; 77 | 78 | (defmethod ig/init-key ::logger [_ _] 79 | (let [console-pub-stop-fn 80 | (u/start-publisher! {:type :console-json 81 | :pretty? false}) 82 | 83 | cloudwatch-emf-pub-stop-fn 84 | (u/start-publisher! 85 | {:type :custom 86 | :fqn-function #'cloudwatch-emf/cloudwatch-emf-publisher})] 87 | (fn [] 88 | (console-pub-stop-fn) 89 | (cloudwatch-emf-pub-stop-fn)))) 90 | 91 | (defmethod ig/halt-key! ::logger [_ stop-fn] 92 | (stop-fn)) 93 | 94 | (defmethod ig/init-key ::event-store [_ config] 95 | (es/start config)) 96 | 97 | (defmethod ig/halt-key! ::event-store [_ event-store] 98 | (es/stop event-store)) 99 | 100 | (defmethod ig/init-key ::event-pubsub [_ config] 101 | (ps/start config)) 102 | 103 | (defmethod ig/halt-key! ::event-pubsub [_ event-pubsub] 104 | (ps/stop event-pubsub)) 105 | 106 | (defmethod ig/init-key ::calculate-average-counter-value-todo-processor [_ config] 107 | (tp/start config)) 108 | 109 | (defmethod ig/halt-key! ::calculate-average-counter-value-todo-processor [_ todo-processor] 110 | (tp/stop todo-processor)) 111 | 112 | (defmethod ig/init-key ::example-periodic-task [_ config] 113 | (pt/start 114 | {:handler-fn (partial (:handler-fn config) (:context config)) 115 | :schedule (:schedule config) 116 | :task-name (:task-name config)})) 117 | 118 | (defmethod ig/halt-key! ::example-periodic-task [_ task] 119 | (pt/stop task)) 120 | 121 | (defmethod ig/init-key ::context [_ context] 122 | context) 123 | 124 | (defmethod ig/init-key ::routes [_ {:keys [context]}] 125 | (set/union 126 | (crh/routes context) 127 | (qrh/routes context) 128 | #{["/healthcheck" :get [(fn [_] {:status 200 :body "OK"})] :route-name ::healthcheck]})) 129 | 130 | (defmethod ig/init-key ::webserver [_ config] 131 | (ws/start config)) 132 | 133 | (defmethod ig/halt-key! ::webserver [_ webserver] 134 | (ws/stop webserver)) 135 | 136 | (defmethod ig/init-key ::nrepl [_ config] 137 | (nrepl/start-server config)) 138 | 139 | (defmethod ig/halt-key! ::nrepl [_ server] 140 | (nrepl/stop-server server)) 141 | 142 | ;; ------------------- ;; 143 | ;; Lifecycle functions ;; 144 | ;; ------------------- ;; 145 | 146 | (defn start 147 | [] 148 | (u/set-global-context! 149 | {:app-name "example-app" :env "dev"}) 150 | (ig/init system)) 151 | 152 | (defn stop 153 | [rag-service] 154 | (ig/halt! rag-service)) 155 | 156 | ;; -------------- ;; 157 | ;; Runtime System ;; 158 | ;; -------------- ;; 159 | 160 | (defonce app (atom {})) 161 | 162 | (defn -main 163 | [& _] 164 | (reset! app (start)) 165 | (u/log ::app-started) 166 | (.addShutdownHook (Runtime/getRuntime) 167 | (Thread. #(do 168 | (u/log ::stopping-app) 169 | (stop @app))))) 170 | 171 | (comment 172 | 173 | (def app (start)) 174 | 175 | 176 | 177 | "") -------------------------------------------------------------------------------- /components/event-store-v2/src/ai/obney/grain/event_store_v2/core/in_memory.cljc: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.event-store-v2.core.in-memory 2 | (:refer-clojure :exclude [read]) 3 | (:require [ai.obney.grain.event-store-v2.interface.protocol :as p] 4 | [ai.obney.grain.event-store-v2.core :refer [->event]] 5 | [cognitect.anomalies :as anom] 6 | [clojure.set :as set] 7 | #?@(:clj [[com.brunobonacci.mulog :as u] 8 | [clj-uuid :as uuid]] 9 | :cljs [["uuid" :as uuid]]))) 10 | 11 | (defn start 12 | [_config] 13 | #?(:clj (ref {:events []}) 14 | :cljs (atom {:events []}))) 15 | 16 | (defn stop 17 | [state] 18 | #?(:clj (dosync (ref-set state nil)) 19 | :cljs (reset! state nil))) 20 | 21 | #?(:clj (defn read 22 | [event-store {:keys [tags types as-of after] :as args}] 23 | (u/trace 24 | ::read 25 | [:args args 26 | :metric/name "GrainReadEvents"] 27 | (let [filtered-events (->> (-> event-store :state deref :events) 28 | (filter 29 | (fn [event] 30 | (and 31 | (or (not tags) 32 | (set/subset? tags (:event/tags event))) 33 | (or (not types) 34 | (contains? types (:event/type event))) 35 | (cond 36 | as-of (or (uuid/< (:event/id event) as-of) 37 | (uuid/= (:event/id event) as-of)) 38 | after (uuid/> (:event/id event) after) 39 | :else true)))))] 40 | (reify 41 | ;; Support streaming reduction with init value 42 | clojure.lang.IReduceInit 43 | (reduce [_ f init] 44 | (reduce f init filtered-events)) 45 | ;; Support streaming reduction without init value 46 | clojure.lang.IReduce 47 | (reduce [_ f] 48 | (let [reduced-result 49 | (reduce 50 | (fn [acc event] 51 | (if (= acc ::none) 52 | event 53 | (f acc event))) 54 | ::none 55 | filtered-events)] 56 | (if (= reduced-result ::none) 57 | (f) ; Empty collection case 58 | reduced-result))))))) 59 | 60 | :cljs (defn read 61 | [event-store {:keys [tags types as-of after] :as _args}] 62 | (let [filtered-events (->> (-> event-store :state deref :events) 63 | (filter 64 | (fn [event] 65 | (and 66 | (or (not tags) 67 | (set/subset? tags (:event/tags event))) 68 | (or (not types) 69 | (contains? types (:event/type event))) 70 | (cond 71 | as-of (or (< (:event/id event) as-of) 72 | (= (:event/id event) as-of)) 73 | after (> (:event/id event) after) 74 | :else true)))))] 75 | (reify 76 | cljs.core/IReduce 77 | (-reduce [_ f] 78 | (let [reduced-result 79 | (reduce 80 | (fn [acc event] 81 | (if (= acc ::none) 82 | event 83 | (f acc event))) 84 | ::none 85 | filtered-events)] 86 | (if (= reduced-result ::none) 87 | (f) ; Empty collection case 88 | reduced-result))) 89 | (-reduce [_ f init] 90 | (reduce f init filtered-events)))))) 91 | 92 | #?(:clj (defn append 93 | [event-store {{:keys [predicate-fn] :as cas} :cas 94 | :keys [events tx-metadata]}] 95 | (u/trace 96 | ::append 97 | [:grain/event-ids (map :event/id events) 98 | :metric/name "GrainAppendEvents"] 99 | (let [tx (->event 100 | {:type :grain/tx 101 | :body {:event-ids (set (mapv :event/id events)) 102 | :metadata tx-metadata}})] 103 | (dosync 104 | (if cas 105 | (let [events* (read event-store cas) 106 | pred-result (predicate-fn events*)] 107 | (if pred-result 108 | (alter (:state event-store) update :events into events) 109 | (let [anomaly {::anom/category ::anom/conflict 110 | ::anom/message "CAS failed" 111 | :cas cas}] 112 | (u/log :grain/cas-failed :anomaly anomaly) 113 | anomaly))) 114 | (alter (:state event-store) update :events into (conj events tx))))))) 115 | 116 | :cljs (defn append 117 | [event-store {{:keys [predicate-fn] :as cas} :cas 118 | :keys [events tx-metadata]}] 119 | (let [tx (->event 120 | {:type :grain/tx 121 | :body {:event-ids (set (mapv :event/id events)) 122 | :metadata tx-metadata}})] 123 | (if cas 124 | (let [events* (read event-store cas) 125 | pred-result (predicate-fn events*)] 126 | (if pred-result 127 | (swap! (:state event-store) update :events into events) 128 | (let [anomaly {::anom/category ::anom/conflict 129 | ::anom/message "CAS failed" 130 | :cas cas}] 131 | anomaly))) 132 | (swap! (:state event-store) update :events into (conj events tx)))))) 133 | 134 | (defrecord InMemoryEventStore [config] 135 | p/EventStore 136 | 137 | (start [this] 138 | (assoc this :state (start config))) 139 | 140 | (stop [this] 141 | (stop (:state this)) 142 | (dissoc this :state)) 143 | 144 | (append [this args] 145 | (append this args)) 146 | 147 | (read [this args] 148 | (read this args))) 149 | 150 | (defmethod p/start-event-store :in-memory 151 | [config] 152 | (p/start (->InMemoryEventStore config))) -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Grain 2 | 3 | ## What is this? 4 | 5 | Grain is an “AI-native” framework for building Event-Sourced systems where Agentic Workflows are part of the domain model and not an afterthought. 6 | If you’ve ever struggled to bolt an agent framework onto an existing system, Grain gives you a coherent architecture where agents and events share the same backbone. 7 | 8 | 9 | ## Why did you make it? 10 | 11 | At ObneyAI, we use [Event Modeling and Event Sourcing](https://leanpub.com/eventmodeling-and-eventsourcing) to design [Simple](https://www.youtube.com/watch?v=SxdOUGdseq4) information systems and applications for our customers. We believe in investing in our tooling, because we always want to deliver the next project faster, but with a high degree of confidence in the platforms we deliver. Being in the emerging AI space, it's our job to wade through all the uncertainty and attempt to skate to where we think the puck is heading. Grain combines proven ideas from conventional software architecture with modern agent workflows, giving us (and you) a single, composable toolkit for building the next generation of AI-driven applications. 12 | 13 | ## Does it have an Agent Framework? 14 | 15 | We have taken a look at the landscape of Agent Frameworks and determined two things: 16 | 17 | 1. There are better ways to go about it than what most existing Agent Frameworks are doing. 18 | 2. Agent Frameworks are not rocket science, they are largely just orchestration engines, so it's ok to make your own if you have a good reason. 19 | 20 | Grain’s Agent Framework integrates directly with your Event-Sourced domain. Grain Agents are built with: 21 | 22 | - [Fully declarative Behavior Trees](https://arxiv.org/abs/2404.07439) 23 | - High leverage integration with [DSPY](https://dspy.ai/) 24 | - Short-term program memory 25 | - Long-term, event-sourced memory e.g. projections over your domain events. 26 | 27 | This means your agents can reason over the same source of truth as the rest of your system. 28 | 29 | A few demos are available [here](https://github.com/ObneyAI/macroexpand-2-demo) 30 | 31 | ## Can I use it? 32 | 33 | Yes, you can use it. Grain is MIT licensed software. We use Grain in production for our clients, but that doesn't mean it's perfect or ready in every way. Large portions of Grain are relatively stable at this point, such as the core CQRS / Event Sourcing components, but other aspects may change rapidly, such as the components that make up the Agent Framework. 34 | 35 | Using Grain feels like snapping Lego bricks together, each component is independent but plays nicely with the rest. Start with an in-memory event store for quick iteration, then swap in Postgres with a single line change when you’re ready. 36 | 37 | One promise we can make is that we will never break your software, we enhance existing components routinely, but we avoid making breaking changes that violate existing contracts between components. If a change we want to make is revolutionary enough, we will introduce a new version of a component, that way consumers of existing versions aren't negatively impacted. 38 | 39 | Our choice to deliver Grain as a simple system of cooperating components using [Polylith](https://polylith.gitbook.io/polylith) allows us this flexibility in addition to the ability to mix and match components in new and interesting ways to publish standalone tools as independent Polylith Projects from a single repository. 40 | 41 | ## How do I use it? 42 | 43 | It may be a while before we have extensive documentation. 44 | 45 | There are 2 main ways you can use Grain: 46 | 47 | 1. You can use Grain as a library if you don't foresee yourself needing to adjust any aspects of the components and framework to what you intend to build. 48 | 49 | 2. You could clone this repository and build your application directly within the framework, all of the pieces and parts would then be available for you to tweak and refine. The downside, of course, is that you would make it harder to take advantage of updates to Grain, but sometimes this level of control is a necessity. 50 | 51 | We've tried to give a decent demonstration of how to build an application with Grain in the base in `bases/example-base` and the component in `components/example-service`. Additionally, see `development/src/example_app_demo.clj` for a demo of how to start and interact with the example system. 52 | 53 | ### Authentication / Authorization 54 | 55 | At this time we leave these two aspects to the user, but this shouldn't be much of an issue, as the user has the option of composing their own routes together when using the webserver component or even using their own preferred webserver. 56 | 57 | ### Can I just use the Agent Framework? 58 | 59 | Sure you can. Grain really shines when you drink the Event Sourcing koolaid and model your entire domain as an Event Sourced system, but it's totally possible to just use Grain's Event Sourced, Behavior-Tree Agent Framework on its own and integrate it into your more conventional application systems. 60 | 61 | ## Available Packages 62 | 63 | Polylith projects are how we publish various aspects of Grain such that you can include them in your deps.edn file and pull them in as dependencies. 64 | 65 | Here is what we currently offer: 66 | 67 | | Package | Summary | 68 | | --- | --- | 69 | | **grain-core** | CQRS/Event Sourcing utilities with in-memory backend + Behavior Tree engine. | 70 | | **grain-event-store-postgres-v2** | Protocol-driven Postgres backend — swap in/out with a config change. | 71 | | **grain-dspy-extensions** | DSPy integration + re-usable BT node for LLM workflows. | 72 | | **grain-mulog-aws-cloudwatch-emf-publisher** | Mulog publisher for AWS CloudWatch metrics & dashboards. | 73 | 74 | 75 | 76 | ### grain-core 77 | 78 | This is the core set of utilities that can power what you see in the example application in this repo. It's everything you need to build an application that follows CQRS / Event Sourcing principles. It comes with an in-memory backend for the Event Store component for getting started quickly. The Event Sourced Behavior Tree Engine is included in this package. 79 | 80 | ```clojure 81 | obneyai/grain-core 82 | {:git/url "https://github.com/ObneyAI/grain.git" 83 | :sha "2d065bd2bc68922254f8cc0a085bdae46a60f5f7" 84 | :deps/root "projects/grain-core"} 85 | ``` 86 | 87 | ### grain-event-store-postgres-v2 88 | 89 | This is a Postgres backend for the Event Store component, pull it in and require the `ai.obney.grain.event-store-postgres-v2.interface` namespace to load its multimethod implementation. This will allow you to simply switch between `:in-memory` and `:postgres` with a one line change with no other code changes required. Our event-store-v2 component is protocol-driven and presents a consistent API to callers, backends are expected to implement the spec. You can even implement your own backend! 90 | 91 | ```clojure 92 | obneyai/grain-event-store-postgres-v2 93 | {:git/url "https://github.com/ObneyAI/grain.git" 94 | :sha "2d065bd2bc68922254f8cc0a085bdae46a60f5f7" 95 | :deps/root "projects/grain-event-store-postgres-v2"} 96 | ``` 97 | 98 | ### grain-dspy-extensions 99 | 100 | This is where the magic of Grain's Agent Framework happens. [DSPY](https://dspy.ai/) is a best in class Python library from Stanford for working with LLMs in sophisticated ways. This package includes our `clj-dspy` component and a re-useable `dspy` Behavior Tree action node for orchestrating reliable Agentic Workflows. You'll just have to see some examples to experience the magic, it's difficult to explain how cool it is. However, you do take a dependency on Python when you use these tools, so you will want to set up a `python.edn` file in the root of your project directory with the following content: `{:python-executable ".venv/bin/python"}`. Then you will need to create a python virtual environment called `.venv` in the root of your directory using a tool of your choice. We like [uv](https://docs.astral.sh/uv/). You'll need to install at least Python `3.12`. 101 | 102 | We think the dependency on Python is pretty neat! Python is really in the spotlight these days thanks to a lot of applied AI tooling being heavily Python based. It's fantastic that a Clojure application can combine the best of both the JVM and Python in order to create an innovative product that is more than the sum of its parts. 103 | 104 | ```clojure 105 | obneyai/grain-dspy-extensions 106 | {:git/url "https://github.com/ObneyAI/grain.git" 107 | :sha "2d065bd2bc68922254f8cc0a085bdae46a60f5f7" 108 | :deps/root "projects/grain-dspy-extensions"} 109 | ``` 110 | 111 | ### grain-mulog-aws-cloudwatch-emf-publisher 112 | 113 | Grain uses [mulog](https://github.com/BrunoBonacci/mulog) for logging and tracing. This is good for you, because it means if you have a preferred logging solution, all you have to do is implement a custom mulog publisher, intercept Grain's logs, and translate them into your own logging solution. This package is a custom publisher that we use to enable automatic creation of CloudWatch metrics in AWS for Dashboards, Alerting, and other observability use-cases. 114 | 115 | ```clojure 116 | obneyai/grain-mulog-aws-cloudwatch-emf-publisher 117 | {:git/url "https://github.com/ObneyAI/grain.git" 118 | :sha "2d065bd2bc68922254f8cc0a085bdae46a60f5f7" 119 | :deps/root "projects/grain-mulog-aws-cloudwatch-emf-publisher"} 120 | ``` 121 | 122 | ## What's next? 123 | 124 | - Comprehensive Documentation 125 | - More examples 126 | 127 | ## Contact Us 128 | 129 | If you have questions or want help getting started, then feel free to come find us in the Clojurian Slack in the [#grain](https://clojurians.slack.com/archives/C099K3D7XRV) channel. 130 | 131 | If you have feedback or find bugs or problems, feel free to create a github issue. -------------------------------------------------------------------------------- /components/event-store-postgres-v2/src/ai/obney/grain/event_store_postgres_v2/core.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.event-store-postgres-v2.core 2 | (:refer-clojure :exclude [read]) 3 | (:require [ai.obney.grain.event-store-v2.interface.protocol :as p :refer [EventStore start-event-store]] 4 | [ai.obney.grain.event-store-v2.interface :refer [->event]] 5 | [next.jdbc :as jdbc] 6 | [com.brunobonacci.mulog :as u] 7 | [integrant.core :as ig] 8 | [hikari-cp.core :as hikari] 9 | [cognitect.anomalies :as anom] 10 | [clojure.string :as string] 11 | [clojure.edn :as edn])) 12 | 13 | ;; -------------------------- ;; 14 | ;; Event Store Initialization ;; 15 | ;; -------------------------- ;; 16 | 17 | (defn init-idempotently 18 | [{::keys [connection-pool] :as _event-store}] 19 | (u/trace 20 | ::initializing-event-store-idempotently 21 | [] 22 | (jdbc/with-transaction [conn connection-pool] 23 | (doseq [statement ["CREATE SCHEMA IF NOT EXISTS grain;" 24 | 25 | ;; Table 26 | "CREATE TABLE IF NOT EXISTS grain.events ( 27 | id UUID PRIMARY KEY, 28 | time TIMESTAMPTZ NOT NULL, 29 | type TEXT NOT NULL, 30 | tags TEXT[] NOT NULL, 31 | edn TEXT NOT NULL 32 | );" 33 | 34 | "CREATE TABLE IF NOT EXISTS grain.global_lock ( 35 | id INTEGER PRIMARY KEY 36 | );" 37 | 38 | "INSERT INTO grain.global_lock (id) VALUES (1) ON CONFLICT DO NOTHING;" 39 | 40 | "CREATE INDEX IF NOT EXISTS idx_events_type ON grain.events(type);" 41 | 42 | "CREATE INDEX IF NOT EXISTS idx_events_tags_gin ON grain.events USING GIN (tags);"]] 43 | 44 | (jdbc/execute! conn [statement]))))) 45 | 46 | ;; --------------------------- ;; 47 | ;; Integrant / Lifecycle Setup ;; 48 | ;; --------------------------- ;; 49 | 50 | (defn start 51 | [{::keys [_server-name _port-number _username _password _database-name] :as config}] 52 | (u/trace 53 | ::starting-event-store 54 | [] 55 | (let [config* (assoc config :adapter "postgresql") 56 | system (ig/init 57 | {::config config* 58 | ::connection-pool {::config (ig/ref ::config)}})] 59 | (init-idempotently system) 60 | system))) 61 | 62 | (defn stop 63 | [event-store] 64 | (u/trace 65 | ::stopping-event-store 66 | [] 67 | (ig/halt! event-store))) 68 | 69 | ;; ---------------;; 70 | ;; Integrant keys ;; 71 | ;; -------------- ;; 72 | 73 | (defmethod ig/init-key ::config [_ config] 74 | config) 75 | 76 | (defmethod ig/init-key ::connection-pool [_ {::keys [config]}] 77 | (try 78 | (hikari/make-datasource config) 79 | (catch Throwable t 80 | (u/log ::error-creating-connection-pool :error t) 81 | (throw t)))) 82 | 83 | (defmethod ig/halt-key! ::connection-pool [_ connection-pool] 84 | (hikari/close-datasource connection-pool)) 85 | 86 | (defn parse-tags 87 | "Parse tags from PostgreSQL string array format to set of tuples" 88 | [tags-array] 89 | (when tags-array 90 | (let [tags-vec (if (instance? org.postgresql.jdbc.PgArray tags-array) 91 | (.getArray tags-array) 92 | tags-array)] 93 | (when (seq tags-vec) 94 | (->> tags-vec 95 | (map #(let [[entity-type entity-id] (string/split % #":" 2)] 96 | [(keyword entity-type) (java.util.UUID/fromString entity-id)])) 97 | (into #{})))))) 98 | 99 | (defn key-fn 100 | [k] 101 | (if (qualified-keyword? k) 102 | (str (namespace k) "/" (name k)) 103 | (str (name k)))) 104 | 105 | (defn transform-row 106 | "Transform PostgreSQL row to event schema format" 107 | [{:keys [id time type tags edn] :as row}] 108 | (try 109 | (let [body-data (when (seq edn) (edn/read-string edn)) 110 | parsed-tags (parse-tags tags)] 111 | (merge 112 | {:event/id id 113 | :event/timestamp time 114 | :event/type (keyword (string/replace type #"^:" "")) 115 | :event/tags (or parsed-tags #{})} 116 | body-data)) 117 | (catch Exception e 118 | (u/log ::error-transforming-row :error e :row row) 119 | (throw e)))) 120 | 121 | 122 | (defn read 123 | [event-store {:keys [tags types after as-of]}] 124 | (let [tag-clauses (when tags 125 | [["tags @> ?::text[]" 126 | (into-array String 127 | (map #(str (key-fn (first %)) ":" (second %)) tags))]]) 128 | clauses (->> (concat tag-clauses 129 | [(when types 130 | ["type = ANY(?)" 131 | (into-array String (mapv #(str ":" (key-fn %)) types))]) 132 | (when after ["id > ?" after]) 133 | (when as-of ["id >= ?" as-of])]) 134 | (remove nil?)) 135 | where-sql (if (seq clauses) 136 | (str "WHERE " (clojure.string/join " AND " (map first clauses))) 137 | "") 138 | params (map second clauses) 139 | sql (str 140 | "SELECT id, time, type, tags, edn " 141 | "FROM grain.events " 142 | where-sql 143 | " ORDER BY id") 144 | plan (jdbc/plan 145 | (get-in event-store [:state ::connection-pool]) 146 | (into [sql] params) 147 | {:fetch-size 500})] 148 | (reify 149 | ;; Wrap JDBC plan to pre-process rows into proper event format while retaining the plan's lazy nature. 150 | clojure.lang.IReduceInit 151 | (reduce [_ f init] 152 | (reduce 153 | (fn [acc row] 154 | (f acc (transform-row row))) 155 | init 156 | plan)) 157 | ;; Make compatible with transducers while retaining lazy evaluation. 158 | clojure.lang.IReduce 159 | (reduce [_ f] 160 | (let [reduced-result 161 | (reduce 162 | (fn [acc row] 163 | (if (= acc ::none) 164 | (transform-row row) 165 | (f acc (transform-row row)))) 166 | ::none 167 | plan)] 168 | (if (= reduced-result ::none) 169 | (f) 170 | reduced-result)))))) 171 | 172 | (defn insert-events 173 | [conn events] 174 | (jdbc/execute-batch! 175 | conn 176 | "INSERT INTO grain.events (id, time, type, tags, edn) VALUES (?, ?, ?, ?, ?)" 177 | (for [event events] 178 | [(:event/id event) 179 | (:event/timestamp event) 180 | (str (:event/type event)) 181 | (into-array 182 | String 183 | (reduce 184 | (fn [acc [k v]] 185 | (conj acc (str (key-fn k) ":" v))) 186 | [] 187 | (:event/tags event))) 188 | (pr-str 189 | (dissoc 190 | event 191 | :event/id 192 | :event/timestamp 193 | :event/type 194 | :event/tags))]) 195 | {:batch-size 100})) 196 | 197 | 198 | (defn append 199 | [event-store {{:keys [predicate-fn] :as cas} :cas 200 | :keys [events tx-metadata]}] 201 | (let [events* (conj 202 | events 203 | (->event 204 | {:type :grain/tx 205 | :body (cond-> {:event-ids (set (mapv :event/id events))} 206 | tx-metadata (assoc :metadata tx-metadata))}))] 207 | (jdbc/with-transaction 208 | [conn (get-in event-store [:state ::connection-pool])] 209 | (jdbc/execute! conn ["SET LOCAL lock_timeout = '5000ms'"]) 210 | (jdbc/execute! conn ["SELECT id FROM grain.global_lock FOR UPDATE"]) 211 | (if cas 212 | (if (predicate-fn (read event-store cas)) 213 | (insert-events conn events*) 214 | (let [anomaly {::anom/category ::anom/conflict 215 | ::anom/message "CAS failed" 216 | ::cas cas}] 217 | (u/log ::cas-failed :anomaly anomaly) 218 | anomaly)) 219 | (insert-events conn events*))))) 220 | 221 | ;; ----------------- ;; 222 | ;; Record Definition ;; 223 | ;; ----------------- ;; 224 | 225 | (defrecord PostgresEventStore [config] 226 | EventStore 227 | 228 | (start [this] 229 | (assoc this :state (start config))) 230 | 231 | (stop [this] 232 | (stop (:state this)) 233 | (dissoc this :state)) 234 | 235 | (append [this args] 236 | (append this args)) 237 | 238 | (read [this args] 239 | (read this args))) 240 | 241 | (defmethod start-event-store :postgres 242 | [config] 243 | (p/start 244 | (->PostgresEventStore (dissoc (:conn config) :type)))) 245 | 246 | (comment 247 | 248 | (def es (p/start-event-store 249 | {:conn {:type :postgres 250 | :server-name "localhost" 251 | :port-number "5433" 252 | :username "postgres" 253 | :password "password" 254 | :database-name "obneyai"}})) 255 | 256 | (stop es) 257 | 258 | 259 | (def user-id #uuid "eae56ad0-6575-418f-a5f5-bab2674ac2c9") 260 | 261 | (p/append 262 | es 263 | {:events [(->event 264 | {:type :hello/world 265 | :tags #{[:user user-id]} 266 | :body {:user-id user-id}})]}) 267 | 268 | 269 | (reduce 270 | (fn [acc event] 271 | (conj acc event)) 272 | [] 273 | (read es {})) 274 | 275 | 276 | {:event/id #uuid "0197cbed-d1e3-70ac-8fca-a9c23706d550", 277 | :event/timestamp #inst "2025-07-02T16:17:30.083469000-00:00", 278 | :event/type :hello/world, 279 | :event/tags #{[:user #uuid "eae56ad0-6575-418f-a5f5-bab2674ac2c9"]}, 280 | :user-id #uuid "eae56ad0-6575-418f-a5f5-bab2674ac2c9", 281 | :message "Testing transformation"} 282 | 283 | 284 | 285 | "") -------------------------------------------------------------------------------- /components/query-processor/test/ai/obney/grain/query_processor/interface_test.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.query-processor.interface-test 2 | (:require [clojure.test :refer :all] 3 | [ai.obney.grain.query-processor.interface :as qp] 4 | [ai.obney.grain.schema-util.interface :refer [defschemas]] 5 | [cognitect.anomalies :as anom] 6 | [clj-uuid :as uuid]) 7 | (:import [java.time OffsetDateTime])) 8 | 9 | ;; Register test query schemas 10 | (defschemas test-queries 11 | {:test/simple-query [:map] 12 | :test/query-with-id [:map [:resource-id :uuid]] 13 | :test/query-with-name [:map [:name :string]] 14 | :test/unknown-query [:map] 15 | :test/some-query [:map] 16 | :test/nil-handler [:map] 17 | :test/forbidden-handler [:map] 18 | :test/not-found-handler [:map] 19 | :test/context-handler [:map [:foo :string]] 20 | :test/empty-result [:map] 21 | :test/anomaly-handler [:map] 22 | :test/context-anomaly [:map] 23 | :test/throwing-handler [:map]}) 24 | 25 | ;; Test Helpers 26 | 27 | (defn make-query 28 | [query-name & {:keys [id timestamp extra-data] 29 | :or {id (uuid/v4) 30 | timestamp (OffsetDateTime/now)}}] 31 | (merge {:query/name query-name 32 | :query/id id 33 | :query/timestamp timestamp} 34 | extra-data)) 35 | 36 | (defn make-context 37 | [query query-registry] 38 | {:query query 39 | :query-registry query-registry}) 40 | 41 | (defn make-registry 42 | [query-handlers] 43 | (into {} (map (fn [[k v]] [k {:handler-fn v}]) query-handlers))) 44 | 45 | ;; Sample Query Handlers 46 | 47 | (defn successful-handler 48 | [_context] 49 | {:query/result {:status "completed" :data "test-data"}}) 50 | 51 | (defn empty-result-handler 52 | [_context] 53 | {}) 54 | 55 | (defn handler-with-id 56 | [{{:keys [resource-id]} :query}] 57 | {:query/result {:resource-id resource-id :found true}}) 58 | 59 | (defn handler-returning-nil 60 | [_context] 61 | nil) 62 | 63 | (defn handler-returning-not-found 64 | [_context] 65 | {::anom/category ::anom/not-found 66 | ::anom/message "Resource not found"}) 67 | 68 | (defn handler-returning-forbidden 69 | [_context] 70 | {::anom/category ::anom/forbidden 71 | ::anom/message "Access denied"}) 72 | 73 | (defn handler-receiving-context 74 | [context] 75 | {:query/result {:received-query (:query context) 76 | :has-registry (contains? context :query-registry)}}) 77 | 78 | (defn handler-throwing-exception 79 | [_context] 80 | (throw (ex-info "Database query failed" {:error-type :database-connection}))) 81 | 82 | ;; Tests 83 | 84 | ;; 1. Happy Path Tests 85 | 86 | (deftest test-successful-query-processing 87 | (testing "Valid query with registered handler executes successfully" 88 | (let [query (make-query :test/simple-query) 89 | registry (make-registry {:test/simple-query successful-handler}) 90 | context (make-context query registry) 91 | result (qp/process-query context)] 92 | (is (not (contains? result ::anom/category))) 93 | (is (= {:status "completed" :data "test-data"} (:query/result result)))))) 94 | 95 | (deftest test-query-with-parameters 96 | (testing "Query with parameters are passed to handler" 97 | (let [test-id (uuid/v4) 98 | query (make-query :test/query-with-id :extra-data {:resource-id test-id}) 99 | registry (make-registry {:test/query-with-id handler-with-id}) 100 | context (make-context query registry) 101 | result (qp/process-query context)] 102 | (is (not (contains? result ::anom/category))) 103 | (is (= test-id (get-in result [:query/result :resource-id]))) 104 | (is (true? (get-in result [:query/result :found])))))) 105 | 106 | (deftest test-query-with-empty-result 107 | (testing "Query returning empty map (no explicit result) is successful" 108 | (let [query (make-query :test/empty-result) 109 | registry (make-registry {:test/empty-result empty-result-handler}) 110 | context (make-context query registry) 111 | result (qp/process-query context)] 112 | (is (not (contains? result ::anom/category))) 113 | (is (= {} result))))) 114 | 115 | ;; 2. Query Registry Tests 116 | 117 | (deftest test-unregistered-query 118 | (testing "Unregistered query returns ::anom/not-found anomaly" 119 | (let [query (make-query :test/unknown-query) 120 | registry (make-registry {}) 121 | context (make-context query registry) 122 | result (qp/process-query context)] 123 | (is (= ::anom/not-found (::anom/category result))) 124 | (is (string? (::anom/message result))) 125 | (is (= "Unknown Query" (::anom/message result)))))) 126 | 127 | (deftest test-nil-registry 128 | (testing "Query lookup with nil registry returns not-found" 129 | (let [query (make-query :test/some-query) 130 | registry nil 131 | context (make-context query registry) 132 | result (qp/process-query context)] 133 | (is (= ::anom/not-found (::anom/category result)))))) 134 | 135 | ;; 3. Schema Validation Tests 136 | 137 | (deftest test-query-missing-name 138 | (testing "Query missing :query/name returns ::anom/not-found (fails registry lookup)" 139 | (let [query {:query/id (uuid/v4) 140 | :query/timestamp (OffsetDateTime/now)} 141 | registry (make-registry {:test/simple-query successful-handler}) 142 | context (make-context query registry) 143 | result (qp/process-query context)] 144 | (is (= ::anom/not-found (::anom/category result))) 145 | (is (string? (::anom/message result)))))) 146 | 147 | (deftest test-query-missing-id 148 | (testing "Query missing :query/id returns ::anom/incorrect" 149 | (let [query {:query/name :test/simple-query 150 | :query/timestamp (OffsetDateTime/now)} 151 | registry (make-registry {:test/simple-query successful-handler}) 152 | context (make-context query registry) 153 | result (qp/process-query context)] 154 | (is (= ::anom/incorrect (::anom/category result))) 155 | (is (string? (::anom/message result)))))) 156 | 157 | (deftest test-query-missing-timestamp 158 | (testing "Query missing :query/timestamp returns ::anom/incorrect" 159 | (let [query {:query/name :test/simple-query 160 | :query/id (uuid/v4)} 161 | registry (make-registry {:test/simple-query successful-handler}) 162 | context (make-context query registry) 163 | result (qp/process-query context)] 164 | (is (= ::anom/incorrect (::anom/category result))) 165 | (is (string? (::anom/message result)))))) 166 | 167 | (deftest test-query-invalid-name-type 168 | (testing "Query with invalid :query/name type returns ::anom/not-found (fails registry lookup)" 169 | (let [query {:query/name "not-a-keyword" 170 | :query/id (uuid/v4) 171 | :query/timestamp (OffsetDateTime/now)} 172 | registry (make-registry {:test/simple-query successful-handler}) 173 | context (make-context query registry) 174 | result (qp/process-query context)] 175 | (is (= ::anom/not-found (::anom/category result)))))) 176 | 177 | (deftest test-query-invalid-id-type 178 | (testing "Query with invalid :query/id type returns ::anom/incorrect" 179 | (let [query {:query/name :test/simple-query 180 | :query/id "not-a-uuid" 181 | :query/timestamp (OffsetDateTime/now)} 182 | registry (make-registry {:test/simple-query successful-handler}) 183 | context (make-context query registry) 184 | result (qp/process-query context)] 185 | (is (= ::anom/incorrect (::anom/category result)))))) 186 | 187 | (deftest test-validation-error-message-is-human-readable 188 | (testing "Schema validation errors include human-readable explanations" 189 | (let [query {:query/name :test/simple-query} 190 | registry (make-registry {:test/simple-query successful-handler}) 191 | context (make-context query registry) 192 | result (qp/process-query context)] 193 | (is (= ::anom/incorrect (::anom/category result))) 194 | (is (string? (::anom/message result))) 195 | (is (pos? (count (::anom/message result))))))) 196 | 197 | (deftest test-query-specific-schema-validation 198 | (testing "Query-specific schema validation catches missing required fields" 199 | (let [query (make-query :test/query-with-name) 200 | ;; Missing required :name field 201 | registry (make-registry {:test/query-with-name successful-handler}) 202 | context (make-context query registry) 203 | result (qp/process-query context)] 204 | (is (= ::anom/incorrect (::anom/category result))) 205 | (is (contains? result :error/explain))))) 206 | 207 | ;; 4. Handler Execution Tests 208 | 209 | (deftest test-handler-returning-nil 210 | (testing "Handler returning nil produces ::anom/fault" 211 | (let [query (make-query :test/nil-handler) 212 | registry (make-registry {:test/nil-handler handler-returning-nil}) 213 | context (make-context query registry) 214 | result (qp/process-query context)] 215 | (is (= ::anom/fault (::anom/category result))) 216 | (is (re-find #"returned nil" (::anom/message result)))))) 217 | 218 | (deftest test-handler-returning-not-found-anomaly 219 | (testing "Handler returning not-found anomaly passes it through" 220 | (let [query (make-query :test/not-found-handler) 221 | registry (make-registry {:test/not-found-handler handler-returning-not-found}) 222 | context (make-context query registry) 223 | result (qp/process-query context)] 224 | (is (= ::anom/not-found (::anom/category result))) 225 | (is (= "Resource not found" (::anom/message result)))))) 226 | 227 | (deftest test-handler-returning-forbidden-anomaly 228 | (testing "Handler returning forbidden anomaly passes it through" 229 | (let [query (make-query :test/forbidden-handler) 230 | registry (make-registry {:test/forbidden-handler handler-returning-forbidden}) 231 | context (make-context query registry) 232 | result (qp/process-query context)] 233 | (is (= ::anom/forbidden (::anom/category result))) 234 | (is (= "Access denied" (::anom/message result)))))) 235 | 236 | (deftest test-context-passed-to-handler 237 | (testing "Context is properly passed to handler" 238 | (let [query (make-query :test/context-handler :extra-data {:foo "bar"}) 239 | registry (make-registry {:test/context-handler handler-receiving-context}) 240 | context (make-context query registry) 241 | result (qp/process-query context)] 242 | (is (not (contains? result ::anom/category))) 243 | (is (= query (get-in result [:query/result :received-query]))) 244 | (is (true? (get-in result [:query/result :has-registry])))))) 245 | 246 | (deftest test-handler-throwing-exception 247 | (testing "Handler throwing exception returns ::anom/fault anomaly" 248 | (let [query (make-query :test/throwing-handler) 249 | registry (make-registry {:test/throwing-handler handler-throwing-exception}) 250 | context (make-context query registry) 251 | result (qp/process-query context)] 252 | (is (= ::anom/fault (::anom/category result))) 253 | (is (string? (::anom/message result))) 254 | (is (re-find #"Error executing query handler" (::anom/message result))) 255 | (is (re-find #"Database query failed" (::anom/message result)))))) 256 | 257 | ;; 5. Error Handling Tests 258 | 259 | (deftest test-different-anomaly-categories-preserved 260 | (testing "Different anomaly categories are handled properly" 261 | (doseq [category [::anom/forbidden 262 | ::anom/incorrect 263 | ::anom/not-found 264 | ::anom/conflict 265 | ::anom/fault]] 266 | (let [handler (fn [_context] 267 | {::anom/category category 268 | ::anom/message (str "Test " category)}) 269 | query (make-query :test/anomaly-handler) 270 | registry (make-registry {:test/anomaly-handler handler}) 271 | context (make-context query registry) 272 | result (qp/process-query context)] 273 | (is (= category (::anom/category result))) 274 | (is (= (str "Test " category) (::anom/message result))))))) 275 | 276 | (deftest test-anomaly-context-preserved 277 | (testing "Error context is preserved in anomalies" 278 | (let [handler (fn [_context] 279 | {::anom/category ::anom/conflict 280 | ::anom/message "Resource already exists" 281 | :resource-id 123 282 | :extra-info "Additional context"}) 283 | query (make-query :test/context-anomaly) 284 | registry (make-registry {:test/context-anomaly handler}) 285 | context (make-context query registry) 286 | result (qp/process-query context)] 287 | (is (= ::anom/conflict (::anom/category result))) 288 | (is (= "Resource already exists" (::anom/message result))) 289 | (is (= 123 (:resource-id result))) 290 | (is (= "Additional context" (:extra-info result)))))) 291 | -------------------------------------------------------------------------------- /scripts/create_component.bb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (ns create-component 4 | (:require [clojure.java.io :as io] 5 | [clojure.string :as str] 6 | [babashka.fs :as fs])) 7 | 8 | (def templates 9 | {:deps-edn 10 | "{:paths [\"src\" \"resources\"] 11 | :deps {} 12 | :aliases {:test {:extra-paths [\"test\"] 13 | :extra-deps {}}}}" 14 | 15 | :interface-schemas 16 | "(ns ai.obney.grain.{{component-name}}.interface.schemas 17 | \"The schemas ns in a grain service component defines the schemas for commands, events, queries, etc. 18 | 19 | It uses the `defschemas` macro to register the schemas centrally for the rest of 20 | the system to use. 21 | 22 | Schemas are validated in places such as the command-processor 23 | and event-store.\" 24 | (:require [ai.obney.grain.schema-util.interface :refer [defschemas]])) 25 | 26 | (defschemas commands 27 | {{{namespace}}/example-command 28 | [:map 29 | [:name :string]]}) 30 | 31 | (defschemas events 32 | {{{namespace}}/example-event 33 | [:map 34 | [:name :string]]}) 35 | 36 | (defschemas queries 37 | {{{namespace}}/example-query 38 | [:map]})" 39 | 40 | :core-commands 41 | "(ns ai.obney.grain.{{component-name}}.core.commands 42 | \"The core commands namespace in a grain service component implements 43 | the command handlers and defines the command registry. Command functions 44 | take a context that includes any necessary dependencies, to be injected 45 | in the base for the service. Usually a command-request-handler or another 46 | type of adapter will call the command processor, which will access the command 47 | registry for the entire application in the context. Commands either return a cognitect 48 | anomaly or a map that optionally has a :command-result/events key containing a sequence of 49 | valid events per the event-store event schema and optionally :command/result which is some 50 | data that is meant to be returned to the caller, see command-request-handler for example.\" 51 | (:require [ai.obney.grain.{{component-name}}.interface.read-models :as read-models] 52 | [ai.obney.grain.event-store-v2.interface :refer [->event]] 53 | [cognitect.anomalies :as anom])) 54 | 55 | (defn example-command 56 | \"Example command handler.\" 57 | [context] 58 | (let [name (get-in context [:command :name])] 59 | {:command-result/events 60 | [(->event {:type {{namespace}}/example-event 61 | :tags #{[:example (random-uuid)]} 62 | :body {:name name}})]})) 63 | 64 | (def commands 65 | {{{namespace}}/example-command {:handler-fn #'example-command}})" 66 | 67 | :core-queries 68 | "(ns ai.obney.grain.{{component-name}}.core.queries 69 | \"The core queries namespace in a grain service component implements 70 | the query handlers and defines the query registry. Query functions 71 | take a context that includes any necessary dependencies, to be injected 72 | in the base for the service. Usually a query-request-handler or another 73 | type of adapter will call the query processor, which will access the query 74 | registry for the entire application in the context. Queries either return a cognitect 75 | anomaly or a map that optionally has a :query/result which is some 76 | data that is meant to be returned to the caller, see query-request-handler for example.\" 77 | (:require [ai.obney.grain.{{component-name}}.interface.read-models :as read-models] 78 | [cognitect.anomalies :as anom])) 79 | 80 | (defn example-query 81 | [context] 82 | (let [result (read-models/root context)] 83 | {:query/result result})) 84 | 85 | (def queries 86 | {{{namespace}}/example-query {:handler-fn #'example-query}})" 87 | 88 | :core-read-models 89 | "(ns ai.obney.grain.{{component-name}}.core.read-models 90 | \"The core read-models namespace in a grain app is where projections are created from events. 91 | Events are retrieved using the event-store and the read model is built through reducing usually. 92 | These tend to be used by the other components of the grain app, such as commands, queries, periodic tasks, 93 | and todo-processors.\" 94 | (:require [ai.obney.grain.event-store-v2.interface :as event-store] 95 | [com.brunobonacci.mulog :as u])) 96 | 97 | (defmulti apply-event 98 | \"Apply an event to the read model.\" 99 | (fn [_state event] 100 | (:event/type event))) 101 | 102 | (defmethod apply-event {{namespace}}/example-event 103 | [state {:keys [name]}] 104 | (assoc state :example {:name name})) 105 | 106 | (defmethod apply-event :default 107 | [state _event] 108 | ;; If the event is not recognized, return the state unchanged. 109 | state) 110 | 111 | (defn apply-events 112 | \"Applies a sequence of events to the read model state.\" 113 | [events] 114 | (let [result (reduce 115 | (fn [state event] 116 | (apply-event state event)) 117 | {} 118 | events)] 119 | (when (seq result) 120 | result))) 121 | 122 | (defn root 123 | \"Returns the root entity of the read model.\" 124 | [{:keys [event-store] :as _context}] 125 | (let [events (event-store/read 126 | event-store 127 | {:types #{{{namespace}}/example-event}}) 128 | state (u/trace 129 | ::read-model-root 130 | [:metric/name \"ReadModel{{component-name-pascal}}Root\"] 131 | (apply-events events))] 132 | state))" 133 | 134 | :core-todo-processors 135 | "(ns ai.obney.grain.{{component-name}}.core.todo-processors 136 | \"The core todo-processors namespace in a grain service is where todo-processor handler functions are defined. 137 | These functions receive a context and have a specific return signature. They can return a cognitect anomaly, 138 | a map with a `:result/events` key containing a sequence of valid events per the event-store event 139 | schema, or an empty map. Sometimes the todo-processor will just call a command through the commant-processor. 140 | The wiring up of the context and the function occurs in the grain app base. The todo-processor subscribes to 141 | one or more events via pubsub and only ever processes a single event at a time, which is included in the context.\" 142 | (:require [ai.obney.grain.command-processor.interface :as command-processor] 143 | [ai.obney.grain.time.interface :as time])) 144 | 145 | (defn example-todo-processor 146 | \"Example todo processor that processes events.\" 147 | [{:keys [_event] :as context}] 148 | ;; Example: calling a command in response to an event 149 | (command-processor/process-command 150 | (assoc context 151 | :command 152 | {:command/id (random-uuid) 153 | :command/timestamp (time/now) 154 | :command/name {{namespace}}/example-command 155 | :name \"processed-event\"})))" 156 | 157 | :core-periodic-tasks 158 | "(ns ai.obney.grain.{{component-name}}.core.periodic-tasks 159 | \"The periodic tasks namespace in a grain service component is where 160 | periodic task functions are defined. These functions accept a context, 161 | which is wired up in the base for the grain app, and the time, provided 162 | by the periodic-task component implementation. 163 | 164 | Periodic tasks are less rigid than commands and todo-processors and generally 165 | do not have a specific return value. So they use the various dependencies in the context 166 | in order to perform their work with discretion.\" 167 | (:require [com.brunobonacci.mulog :as u])) 168 | 169 | (defn example-periodic-task 170 | [_context _time] 171 | (u/trace ::example-periodic-task []))" 172 | 173 | :interface-commands 174 | "(ns ai.obney.grain.{{component-name}}.interface.commands 175 | (:require [ai.obney.grain.{{component-name}}.core.commands :as core])) 176 | 177 | (def commands core/commands)" 178 | 179 | :interface-queries 180 | "(ns ai.obney.grain.{{component-name}}.interface.queries 181 | (:require [ai.obney.grain.{{component-name}}.core.queries :as core])) 182 | 183 | (def queries core/queries)" 184 | 185 | :interface-read-models 186 | "(ns ai.obney.grain.{{component-name}}.interface.read-models 187 | (:require [ai.obney.grain.{{component-name}}.core.read-models :as core])) 188 | 189 | (defn root 190 | [context] 191 | (core/root context))" 192 | 193 | :interface-todo-processors 194 | "(ns ai.obney.grain.{{component-name}}.interface.todo-processors 195 | (:require [ai.obney.grain.{{component-name}}.core.todo-processors :as core])) 196 | 197 | (def todo-processors 198 | {{{namespace}}/example-event {:handler-fn #'core/example-todo-processor}})" 199 | 200 | :interface-periodic-tasks 201 | "(ns ai.obney.grain.{{component-name}}.interface.periodic-tasks 202 | (:require [ai.obney.grain.{{component-name}}.core.periodic-tasks :as core])) 203 | 204 | (def periodic-tasks 205 | {:example-periodic-task {:handler-fn #'core/example-periodic-task 206 | :schedule \"0 0 * * * ?\" ;; Every hour 207 | :description \"Example periodic task\"}})"}) 208 | 209 | (defn kebab-case [s] 210 | (-> s 211 | (str/replace #"_" "-") 212 | (str/replace #"([a-z])([A-Z])" "$1-$2") 213 | str/lower-case)) 214 | 215 | (defn snake-case [s] 216 | (-> s 217 | (str/replace #"-" "_") 218 | (str/replace #"([a-z])([A-Z])" "$1_$2") 219 | str/lower-case)) 220 | 221 | (defn pascal-case [s] 222 | (->> (str/split s #"[-_]") 223 | (map str/capitalize) 224 | (str/join))) 225 | 226 | (defn create-directory [path] 227 | (when-not (fs/exists? path) 228 | (fs/create-dirs path) 229 | (println "Created directory:" path))) 230 | 231 | (defn substitute-template [template component-name] 232 | (let [kebab-name (kebab-case component-name) 233 | snake-name (snake-case component-name) 234 | pascal-name (pascal-case component-name) 235 | namespace (str ":" kebab-name)] 236 | (-> template 237 | (str/replace "{{component-name}}" kebab-name) 238 | (str/replace "{{component-name-snake}}" snake-name) 239 | (str/replace "{{component-name-pascal}}" pascal-name) 240 | (str/replace "{{namespace}}" namespace)))) 241 | 242 | (defn write-file [path content] 243 | (io/make-parents path) 244 | (spit path content) 245 | (println "Created file:" path)) 246 | 247 | (defn create-component [component-name] 248 | (let [kebab-name (kebab-case component-name) 249 | snake-name (snake-case component-name) 250 | base-path (str "components/" kebab-name) 251 | src-path (str base-path "/src/ai/obney/grain/" snake-name) 252 | test-path (str base-path "/test/ai/obney/grain/" snake-name) 253 | resources-path (str base-path "/resources/" kebab-name)] 254 | 255 | (println "Creating component:" kebab-name) 256 | 257 | ;; Create directories 258 | (create-directory base-path) 259 | (create-directory (str src-path "/interface")) 260 | (create-directory (str src-path "/core")) 261 | (create-directory test-path) 262 | (create-directory resources-path) 263 | 264 | ;; Create files 265 | (write-file (str base-path "/deps.edn") 266 | (substitute-template (:deps-edn templates) component-name)) 267 | 268 | (write-file (str src-path "/interface/schemas.clj") 269 | (substitute-template (:interface-schemas templates) component-name)) 270 | 271 | (write-file (str src-path "/interface/commands.clj") 272 | (substitute-template (:interface-commands templates) component-name)) 273 | 274 | (write-file (str src-path "/interface/queries.clj") 275 | (substitute-template (:interface-queries templates) component-name)) 276 | 277 | (write-file (str src-path "/interface/read_models.clj") 278 | (substitute-template (:interface-read-models templates) component-name)) 279 | 280 | (write-file (str src-path "/interface/todo_processors.clj") 281 | (substitute-template (:interface-todo-processors templates) component-name)) 282 | 283 | (write-file (str src-path "/interface/periodic_tasks.clj") 284 | (substitute-template (:interface-periodic-tasks templates) component-name)) 285 | 286 | (write-file (str src-path "/core/commands.clj") 287 | (substitute-template (:core-commands templates) component-name)) 288 | 289 | (write-file (str src-path "/core/queries.clj") 290 | (substitute-template (:core-queries templates) component-name)) 291 | 292 | (write-file (str src-path "/core/read_models.clj") 293 | (substitute-template (:core-read-models templates) component-name)) 294 | 295 | (write-file (str src-path "/core/todo_processors.clj") 296 | (substitute-template (:core-todo-processors templates) component-name)) 297 | 298 | (write-file (str src-path "/core/periodic_tasks.clj") 299 | (substitute-template (:core-periodic-tasks templates) component-name)) 300 | 301 | (println "Component" kebab-name "created successfully!") 302 | (println "Next steps:") 303 | (println "1. Add the component to your project's deps.edn") 304 | (println "2. Update the schemas with your actual commands, events, and queries") 305 | (println "3. Implement your business logic in the core namespaces") 306 | (println "4. Wire up the component in your base application"))) 307 | 308 | (defn -main [& args] 309 | (if (= 1 (count args)) 310 | (create-component (first args)) 311 | (do 312 | (println "Usage: bb create-component.bb COMPONENT_NAME") 313 | (println "Example: bb create-component.bb user-service") 314 | (System/exit 1)))) 315 | 316 | (when (= *file* (System/getProperty "babashka.file")) 317 | (apply -main *command-line-args*)) -------------------------------------------------------------------------------- /components/todo-processor/test/ai/obney/grain/todo_processor/interface_test.clj: -------------------------------------------------------------------------------- 1 | (ns ai.obney.grain.todo-processor.interface-test 2 | (:require [clojure.test :refer :all] 3 | [ai.obney.grain.todo-processor.core :as core] 4 | [ai.obney.grain.todo-processor.interface :as tp] 5 | [ai.obney.grain.event-store-v2.interface :as es] 6 | [ai.obney.grain.schema-util.interface :refer [defschemas]] 7 | [ai.obney.grain.pubsub.interface :as pubsub] 8 | [cognitect.anomalies :as anom] 9 | [clj-uuid :as uuid])) 10 | 11 | ;; Register test event schemas 12 | (defschemas test-events 13 | {:test/event-processed [:map 14 | [:event-id :uuid] 15 | [:status :string]] 16 | :test/event-1 [:map [:num :int]] 17 | :test/event-2 [:map [:num :int]] 18 | :test/event-3 [:map [:num :int]]}) 19 | 20 | ;; Test Fixtures 21 | 22 | (def ^:dynamic *event-store* nil) 23 | 24 | (defn event-store-fixture [f] 25 | (let [store (es/start {:conn {:type :in-memory}})] 26 | (binding [*event-store* store] 27 | (try 28 | (f) 29 | (finally 30 | (es/stop store)))))) 31 | 32 | (use-fixtures :each event-store-fixture) 33 | 34 | ;; Test Helpers 35 | 36 | (defn make-event 37 | [event-type & {:keys [body tags] 38 | :or {body {} 39 | tags #{}}}] 40 | (es/->event {:type event-type 41 | :tags tags 42 | :body body})) 43 | 44 | (defn make-context 45 | [event handler-fn] 46 | {:event event 47 | :handler-fn handler-fn 48 | :event-store *event-store*}) 49 | 50 | ;; Sample Handler Functions 51 | 52 | (defn successful-handler 53 | "Handler that processes successfully with no events to store" 54 | [_context] 55 | {}) 56 | 57 | (defn handler-with-events 58 | "Handler that returns events to be stored" 59 | [_context] 60 | (let [event-id (uuid/v4)] 61 | {:result/events 62 | [(es/->event {:type :test/event-processed 63 | :tags #{[:test event-id]} 64 | :body {:event-id event-id 65 | :status "processed"}})]})) 66 | 67 | (defn handler-with-multiple-events 68 | "Handler that returns multiple events" 69 | [_context] 70 | {:result/events 71 | [(es/->event {:type :test/event-1 72 | :tags #{[:test (uuid/v4)]} 73 | :body {:num 1}}) 74 | (es/->event {:type :test/event-2 75 | :tags #{[:test (uuid/v4)]} 76 | :body {:num 2}}) 77 | (es/->event {:type :test/event-3 78 | :tags #{[:test (uuid/v4)]} 79 | :body {:num 3}})]}) 80 | 81 | (defn handler-returning-nil 82 | "Handler that returns nil" 83 | [_context] 84 | nil) 85 | 86 | (defn handler-returning-anomaly 87 | "Handler that returns an anomaly" 88 | [_context] 89 | {::anom/category ::anom/fault 90 | ::anom/message "Handler failed"}) 91 | 92 | 93 | (defn handler-throwing-exception 94 | "Handler that throws an uncaught exception" 95 | [_context] 96 | (throw (ex-info "Unexpected error in handler" {:error-type :database-connection}))) 97 | 98 | ;; Tests 99 | 100 | ;; 1. Happy Path Tests 101 | 102 | (deftest test-successful-processing 103 | (testing "Handler processes event successfully with no events to store" 104 | (let [event (make-event :test/trigger-event :body {:data "test"}) 105 | context (make-context event successful-handler) 106 | result (core/process-event context)] 107 | ;; process-event returns nil for successful processing 108 | (is (nil? result)) 109 | 110 | ;; Verify no events were stored (except :grain/tx) 111 | (let [events (->> (es/read *event-store* {}) 112 | (into []) 113 | (filter #(not= :grain/tx (:event/type %))))] 114 | (is (empty? events)))))) 115 | 116 | (deftest test-handler-with-events 117 | (testing "Handler returns events that get stored in event-store" 118 | (let [event (make-event :test/trigger-event :body {:data "test"}) 119 | context (make-context event handler-with-events) 120 | result (core/process-event context)] 121 | ;; process-event returns nil on success 122 | (is (nil? result)) 123 | 124 | ;; Verify event was stored (filter out :grain/tx) 125 | (let [events (->> (es/read *event-store* {}) 126 | (into []) 127 | (filter #(not= :grain/tx (:event/type %))))] 128 | (is (= 1 (count events))) 129 | (is (= :test/event-processed (:event/type (first events)))) 130 | (is (uuid? (:event-id (first events)))) 131 | (is (= "processed" (:status (first events)))))))) 132 | 133 | (deftest test-handler-with-multiple-events 134 | (testing "Handler returns multiple events that all get stored" 135 | (let [event (make-event :test/trigger-event) 136 | context (make-context event handler-with-multiple-events) 137 | result (core/process-event context)] 138 | ;; process-event returns nil on success 139 | (is (nil? result)) 140 | 141 | ;; Verify all events were stored (filter out :grain/tx) 142 | (let [events (->> (es/read *event-store* {}) 143 | (into []) 144 | (filter #(not= :grain/tx (:event/type %))))] 145 | (is (= 3 (count events))) 146 | (is (= #{:test/event-1 :test/event-2 :test/event-3} 147 | (set (map :event/type events)))))))) 148 | 149 | (deftest test-context-passed-to-handler 150 | (testing "Context is properly passed to handler with event, event-store, handler-fn" 151 | (let [event (make-event :test/trigger-event) 152 | received-context (atom nil) 153 | handler (fn [context] 154 | (reset! received-context context) 155 | {}) 156 | context (make-context event handler)] 157 | (core/process-event context) 158 | ;; Verify context was passed correctly 159 | (is (not (nil? @received-context))) 160 | (is (contains? @received-context :event)) 161 | (is (contains? @received-context :event-store)) 162 | (is (contains? @received-context :handler-fn))))) 163 | 164 | ;; 2. Event Store Integration Tests 165 | 166 | (deftest test-no-events-when-empty-result 167 | (testing "No events appended when handler returns empty map" 168 | (let [event (make-event :test/trigger-event) 169 | context (make-context event successful-handler) 170 | _ (core/process-event context) 171 | events (->> (es/read *event-store* {}) 172 | (into []) 173 | (filter #(not= :grain/tx (:event/type %))))] 174 | (is (empty? events))))) 175 | 176 | (deftest test-events-readable-after-append 177 | (testing "Events can be read back after being appended" 178 | (let [event (make-event :test/trigger-event) 179 | context (make-context event handler-with-events)] 180 | (core/process-event context) 181 | (let [events (->> (es/read *event-store* {}) 182 | (into []) 183 | (filter #(not= :grain/tx (:event/type %))))] 184 | (is (= 1 (count events))) 185 | (is (uuid? (:event-id (first events)))) 186 | (is (string? (:status (first events)))))))) 187 | 188 | (deftest test-multiple-todo-processor-invocations 189 | (testing "Multiple invocations append events independently" 190 | (let [event1 (make-event :test/trigger-1) 191 | event2 (make-event :test/trigger-2) 192 | context1 (make-context event1 handler-with-events) 193 | context2 (make-context event2 handler-with-events)] 194 | (core/process-event context1) 195 | (core/process-event context2) 196 | 197 | (let [events (->> (es/read *event-store* {}) 198 | (into []) 199 | (filter #(not= :grain/tx (:event/type %))))] 200 | (is (= 2 (count events))) 201 | (is (every? #(= :test/event-processed (:event/type %)) events)))))) 202 | 203 | ;; 3. Handler Execution Tests 204 | 205 | (deftest test-handler-returning-nil 206 | (testing "Handler returning nil produces fault anomaly but returns nil (logged)" 207 | (let [event (make-event :test/trigger-event) 208 | context (make-context event handler-returning-nil) 209 | result (core/process-event context)] 210 | ;; Anomaly is created and logged, but process-event returns nil 211 | (is (nil? result))))) 212 | 213 | (deftest test-handler-returning-anomaly 214 | (testing "Handler returning anomaly is logged and returns nil" 215 | (let [event (make-event :test/trigger-event) 216 | context (make-context event handler-returning-anomaly) 217 | result (core/process-event context)] 218 | ;; Anomaly is logged, process-event returns nil 219 | (is (nil? result))))) 220 | 221 | (deftest test-handler-throwing-exception 222 | (testing "Handler throwing exception is caught and returns nil (logged)" 223 | (let [event (make-event :test/trigger-event) 224 | context (make-context event handler-throwing-exception) 225 | result (core/process-event context)] 226 | ;; Exception is caught and logged, returns nil 227 | (is (nil? result))))) 228 | 229 | (deftest test-context-contains-original-event 230 | (testing "Context contains the original triggering event" 231 | (let [test-body {:data "test-data" :count 42} 232 | event (make-event :test/trigger-event :body test-body) 233 | received-event (atom nil) 234 | handler (fn [context] 235 | (reset! received-event (:event context)) 236 | {}) 237 | context (make-context event handler)] 238 | (core/process-event context) 239 | (is (some? @received-event)) 240 | (is (= :test/trigger-event (:event/type @received-event))) 241 | ;; Event body fields are flattened into the event map 242 | (is (= "test-data" (:data @received-event))) 243 | (is (= 42 (:count @received-event)))))) 244 | 245 | ;; 4. Error Handling Tests 246 | 247 | (deftest test-different-anomaly-categories 248 | (testing "Different anomaly categories from handler are logged (returns nil)" 249 | (doseq [category [::anom/fault 250 | ::anom/forbidden 251 | ::anom/incorrect 252 | ::anom/not-found 253 | ::anom/conflict]] 254 | (let [handler (fn [_context] 255 | {::anom/category category 256 | ::anom/message (str "Test " category)}) 257 | event (make-event :test/trigger-event) 258 | context (make-context event handler) 259 | result (core/process-event context)] 260 | ;; Anomalies are logged but process-event returns nil 261 | (is (nil? result)))))) 262 | 263 | (deftest test-anomaly-context-preserved 264 | (testing "Anomalies from handler are logged (returns nil)" 265 | (let [handler (fn [_context] 266 | {::anom/category ::anom/conflict 267 | ::anom/message "Resource conflict" 268 | :resource-id 123 269 | :extra-info "Additional data"}) 270 | event (make-event :test/trigger-event) 271 | context (make-context event handler) 272 | result (core/process-event context)] 273 | ;; Anomaly is logged, process-event returns nil 274 | (is (nil? result))))) 275 | 276 | (deftest test-handler-with-invalid-events 277 | (testing "Handler returning invalid events causes event-store error" 278 | (let [handler (fn [_context] 279 | {:result/events 280 | [(es/->event {:type :test/invalid-event 281 | :tags #{[:test (uuid/v4)]} 282 | :body {:missing-field "value"}})]}) 283 | event (make-event :test/trigger-event) 284 | context (make-context event handler) 285 | result (core/process-event context)] 286 | ;; Event store will reject invalid events 287 | ;; Result should be a fault anomaly 288 | (is (= ::anom/fault (::anom/category result))) 289 | (is (= "Error storing events." (::anom/message result)))))) 290 | 291 | ;; 5. Integration Tests - Backpressure and Concurrency 292 | 293 | (deftest test-backpressure-with-slow-handler-and-many-events 294 | (testing "Todo-processor handles backpressure with 1000 events and slow handler (100ms each)" 295 | (let [processed-count (atom 0) 296 | processed-events (atom []) 297 | slow-handler (fn [{:keys [event]}] 298 | ;; Simulate slow processing 299 | (Thread/sleep 100) 300 | (swap! processed-count inc) 301 | (swap! processed-events conj (:event/type event)) 302 | {}) 303 | ;; Create pubsub with :event/type as topic-fn 304 | ps (pubsub/start {:type :core-async 305 | :topic-fn :event/type}) 306 | ;; Start todo-processor with slow handler 307 | processor (tp/start {:event-pubsub ps 308 | :topics [:test/backpressure-event] 309 | :handler-fn slow-handler 310 | :context {:event-store *event-store*}})] 311 | 312 | (try 313 | ;; Rapidly publish 1000 events to test backpressure handling 314 | ;; Note: >1024 events would exceed core.async's pending puts limit on the pubsub channel 315 | (let [num-events 1000] 316 | (dotimes [i num-events] 317 | (pubsub/pub ps {:message {:event/type :test/backpressure-event 318 | :event/id (uuid/v4) 319 | :event-number i}})) 320 | 321 | ;; Wait for all events to be processed 322 | ;; With 100ms per event and parallel processing, should complete in reasonable time 323 | ;; Use generous timeout to account for backpressure 324 | (let [timeout-ms 5000 325 | start-time (System/currentTimeMillis)] 326 | (loop [] 327 | (when (< @processed-count num-events) 328 | (when (> (- (System/currentTimeMillis) start-time) timeout-ms) 329 | (throw (ex-info "Timeout waiting for events to process" 330 | {:processed @processed-count 331 | :expected num-events 332 | :elapsed-ms (- (System/currentTimeMillis) start-time)}))) 333 | (Thread/sleep 50) 334 | (recur)))) 335 | 336 | ;; Verify all events were processed 337 | (is (= num-events @processed-count)) 338 | (is (= num-events (count @processed-events))) 339 | 340 | ;; Verify no events were dropped 341 | (is (every? #(= :test/backpressure-event %) @processed-events))) 342 | 343 | (finally 344 | (tp/stop processor) 345 | (pubsub/stop ps)))))) 346 | --------------------------------------------------------------------------------