From 984a678d927215f55d65d79050a09a6af948ca98 Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 28 Jul 2026 07:37:57 +0700 Subject: [PATCH] pi harness reimplement --- IMPLEMENTATION.md | 119 ++ Manifest.toml | 1142 +-------------- Project.toml | 49 +- README.md | 186 ++- aisystemprompt.md => ai_system_prompt.md | 0 fast_inference_guideline.txt | 72 - src/AgentCore.jl | 149 ++ src/MCTSexamplePrompt.py | 537 ------- src/YiemAgent.jl | 47 - src/agent.jl | 416 ++++++ src/agent_loop.jl | 861 +++++++++++ src/harness_types.jl | 1083 ++++++++++++++ src/interface.jl | 1400 ------------------ src/llmfunction.jl | 1647 ---------------------- src/messages.jl | 183 +++ src/prompt_templates.jl | 335 +++++ src/session/jsonl_repo.jl | 148 ++ src/session/jsonl_storage.jl | 290 ++++ src/session/memory_repo.jl | 133 ++ src/session/memory_storage.jl | 227 +++ src/session/repo_utils.jl | 65 + src/session/session.jl | 422 ++++++ src/skills.jl | 375 +++++ src/stream_fn.jl | 45 + src/system_prompt.jl | 56 + src/tools/bash.jl | 49 + src/tools/edit.jl | 32 + src/tools/edit_diff.jl | 67 + src/tools/file_mutation_queue.jl | 59 + src/tools/image.jl | 66 + src/tools/index.jl | 35 + src/tools/path_utils.jl | 44 + src/tools/read.jl | 35 + src/tools/write.jl | 26 + src/type.jl | 375 ----- src/types.jl | 588 ++++++++ src/util.jl | 457 ------ 37 files changed, 6124 insertions(+), 5696 deletions(-) create mode 100644 IMPLEMENTATION.md rename aisystemprompt.md => ai_system_prompt.md (100%) delete mode 100644 fast_inference_guideline.txt create mode 100644 src/AgentCore.jl delete mode 100644 src/MCTSexamplePrompt.py delete mode 100644 src/YiemAgent.jl create mode 100644 src/agent.jl create mode 100644 src/agent_loop.jl create mode 100644 src/harness_types.jl delete mode 100644 src/interface.jl delete mode 100644 src/llmfunction.jl create mode 100644 src/messages.jl create mode 100644 src/prompt_templates.jl create mode 100644 src/session/jsonl_repo.jl create mode 100644 src/session/jsonl_storage.jl create mode 100644 src/session/memory_repo.jl create mode 100644 src/session/memory_storage.jl create mode 100644 src/session/repo_utils.jl create mode 100644 src/session/session.jl create mode 100644 src/skills.jl create mode 100644 src/stream_fn.jl create mode 100644 src/system_prompt.jl create mode 100644 src/tools/bash.jl create mode 100644 src/tools/edit.jl create mode 100644 src/tools/edit_diff.jl create mode 100644 src/tools/file_mutation_queue.jl create mode 100644 src/tools/image.jl create mode 100644 src/tools/index.jl create mode 100644 src/tools/path_utils.jl create mode 100644 src/tools/read.jl create mode 100644 src/tools/write.jl delete mode 100644 src/type.jl create mode 100644 src/types.jl delete mode 100644 src/util.jl diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md new file mode 100644 index 0000000..ccb21b5 --- /dev/null +++ b/IMPLEMENTATION.md @@ -0,0 +1,119 @@ +# Julia Implementation - AgentCore + +This directory contains a Julia reimplementation of the `@earendil-works/pi-agent-core` package. + +## Project Structure + +``` +julia_implementation/ +├── src/ +│ ├── AgentCore.jl # Main module entry point +│ ├── types.jl # Core type definitions +│ ├── stream_fn.jl # Stream function utilities +│ ├── agent_loop.jl # Low-level agent loop +│ ├── agent.jl # High-level Agent struct +│ ├── harness_types.jl # Extended types for AgentHarness +│ ├── messages.jl # Custom message types +│ ├── system_prompt.jl # System prompt formatting +│ ├── skills.jl # Skill loading and formatting +│ ├── prompt_templates.jl # Prompt template handling +│ ├── agent_harness.jl # AgentHarness implementation +│ │ +│ ├── session/ +│ │ ├── session.jl # Session class +│ │ ├── jsonl_storage.jl # JSONL storage +│ │ ├── jsonl_repo.jl # JSONL repository +│ │ ├── memory_storage.jl # In-memory storage +│ │ ├── memory_repo.jl # In-memory repository +│ │ └── repo_utils.jl # Repository utilities +│ │ +│ ├── tools/ +│ │ ├── index.jl # Tool exports +│ │ ├── bash.jl # Bash execution tool +│ │ ├── read.jl # File read tool +│ │ ├── write.jl # File write tool +│ │ ├── edit.jl # File edit tool +│ │ ├── edit_diff.jl # Diff computation +│ │ ├── image.jl # Image utilities +│ │ ├── path_utils.jl # Path resolution +│ │ └── file_mutation_queue.jl # File mutation serialization +│ │ +│ ├── compaction/ +│ │ ├── compaction.jl # Context compaction +│ │ ├── utils.jl # Compaction utilities +│ │ └── branch_summarization.jl # Branch summarization +│ │ +│ ├── utils/ +│ │ ├── truncate.jl # Output truncation +│ │ └── shell_output.jl # Shell output capture +│ │ +│ ├── proxy.jl # Proxy stream function +│ └── utils.jl # Utility functions +│ +├── test/ +├── Project.toml +├── Manifest.toml +└── README.md +``` + +## Key Features + +### Core Architecture + +The implementation follows the same layered architecture as the TypeScript version: + +1. **Low-level (agent_loop.jl)**: Pure agent loop logic that works with `AgentMessage[]` +2. **High-level (agent.jl)**: Stateful wrapper with event streaming and queueing +3. **Harness (agent_harness.jl)**: Session persistence, resource management, hooks +4. **Session (session/)**: Conversation history with compaction and branching +5. **Tools (tools/)**: Built-in execution tools (bash, read, write, edit) + +### Julia-Specific Features + +- **Type system**: Uses Julia's parametric types for type-safe tool definitions +- **Multiple dispatch**: Extensible via multiple dispatch for custom message types +- **Async primitives**: Leverages Julia's `@async` and `@spawn` for concurrent operations +- **Error handling**: Julia exceptions with typed error codes + +## Building + +```julia +using Pkg +Pkg.activate("julia_implementation") +Pkg.instantiate() +``` + +## Usage Example + +```julia +using AgentCore + +# Create an agent +agent = Agent() + +# Subscribe to events +subscribe(agent) do event, signal + if event isa MessageEndEvent + println("Message: $(event.message)") + end +end + +# Run a prompt +prompt(agent, "Hello, world!") +``` + +## Compatibility + +This implementation aims for API compatibility with the TypeScript version while providing idiomatic Julia abstractions. + +## Status + +This is an active implementation. Core functionality is in place, with ongoing work on: + +- Complete tool implementations +- Full session repository functionality +- Test suite + +## License + +MIT diff --git a/Manifest.toml b/Manifest.toml index 6fe654f..d67c090 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,1127 +1,25 @@ -# This file is machine-generated - editing it directly is not advised - -julia_version = "1.12.6" -manifest_format = "2.0" -project_hash = "1c1379a2cec320abc347f3acb5ee815ba9855aa6" - -[[deps.Accessors]] -deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"] -git-tree-sha1 = "7063ad1083578215c7c4bf410368150abe8d5524" -uuid = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" -version = "0.1.45" - - [deps.Accessors.extensions] - AxisKeysExt = "AxisKeys" - IntervalSetsExt = "IntervalSets" - LinearAlgebraExt = "LinearAlgebra" - StaticArraysExt = "StaticArrays" - StructArraysExt = "StructArrays" - TestExt = "Test" - UnitfulExt = "Unitful" - - [deps.Accessors.weakdeps] - AxisKeys = "94b1ba4f-4ee9-5380-92f1-94cde586c3c5" - IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" - LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" - StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" - StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" - Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" - -[[deps.AliasTables]] -deps = ["PtrArrays", "Random"] -git-tree-sha1 = "9876e1e164b144ca45e9e3198d0b689cadfed9ff" -uuid = "66dad0bd-aa9a-41b7-9441-69ab47430ed8" -version = "1.1.3" - -[[deps.ArgTools]] -uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" -version = "1.1.2" - -[[deps.ArnoldiMethod]] -deps = ["LinearAlgebra", "Random", "StaticArrays"] -git-tree-sha1 = "d57bd3762d308bded22c3b82d033bff85f6195c6" -uuid = "ec485272-7323-5ecc-a04f-4719b315124d" -version = "0.4.0" - -[[deps.ArrowTypes]] -deps = ["Sockets", "UUIDs"] -git-tree-sha1 = "404265cd8128a2515a81d5eae16de90fdef05101" -uuid = "31f734f8-188a-4ce0-8406-c8a06bd891cd" -version = "2.3.0" - -[[deps.Artifacts]] -uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" -version = "1.11.0" - -[[deps.Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -version = "1.11.0" - -[[deps.BufferedStreams]] -git-tree-sha1 = "6863c5b7fc997eadcabdbaf6c5f201dc30032643" -uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d" -version = "1.2.2" - -[[deps.CEnum]] -git-tree-sha1 = "389ad5c84de1ae7cf0e28e381131c98ea87d54fc" -uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" -version = "0.5.0" - -[[deps.CRC32c]] -uuid = "8bf52ea8-c179-5cab-976a-9e18b702a9bc" -version = "1.11.0" - -[[deps.CSV]] -deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "PrecompileTools", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings", "WorkerUtilities"] -git-tree-sha1 = "8d8e0b0f350b8e1c91420b5e64e5de774c2f0f4d" -uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" -version = "0.10.16" - -[[deps.CodeTracking]] -deps = ["InteractiveUtils", "REPL", "UUIDs"] -git-tree-sha1 = "cfb7a2e89e245a9d5016b70323db412b3a7438d5" -uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" -version = "3.0.2" - -[[deps.CodecBase]] -deps = ["TranscodingStreams"] -git-tree-sha1 = "40956acdbef3d8c7cc38cba42b56034af8f8581a" -uuid = "6c391c72-fb7b-5838-ba82-7cfb1bcfecbf" -version = "0.3.4" - -[[deps.CodecZlib]] -deps = ["TranscodingStreams", "Zlib_jll"] -git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" -uuid = "944b1d66-785c-5afd-91f1-9de20f533193" -version = "0.7.8" - -[[deps.CommonSolve]] -git-tree-sha1 = "eeaad7cef88554c2fa56b5a3f71cfd5cb708c662" -uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" -version = "0.2.11" - -[[deps.Compat]] -deps = ["TOML", "UUIDs"] -git-tree-sha1 = "9d8a54ce4b17aa5bdce0ea5c34bc5e7c340d16ad" -uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "4.18.1" -weakdeps = ["Dates", "LinearAlgebra"] - - [deps.Compat.extensions] - CompatLinearAlgebraExt = "LinearAlgebra" - -[[deps.Compiler]] -git-tree-sha1 = "382d79bfe72a406294faca39ef0c3cef6e6ce1f1" -uuid = "807dbc54-b67e-4c79-8afb-eafe4df6f2e1" -version = "0.1.1" - -[[deps.CompilerSupportLibraries_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.3.0+1" - -[[deps.CompositionsBase]] -git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" -uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b" -version = "0.1.2" -weakdeps = ["InverseFunctions"] - - [deps.CompositionsBase.extensions] - CompositionsBaseInverseFunctionsExt = "InverseFunctions" - -[[deps.ConstructionBase]] -git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb" -uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" -version = "1.6.0" - - [deps.ConstructionBase.extensions] - ConstructionBaseIntervalSetsExt = "IntervalSets" - ConstructionBaseLinearAlgebraExt = "LinearAlgebra" - ConstructionBaseStaticArraysExt = "StaticArrays" - - [deps.ConstructionBase.weakdeps] - IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" - LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" - StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" - -[[deps.Crayons]] -git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15" -uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" -version = "4.1.1" - -[[deps.DBInterface]] -git-tree-sha1 = "a444404b3f94deaa43ca2a58e18153a82695282b" -uuid = "a10d1c49-ce27-4219-8d33-6db1a4562965" -version = "2.6.1" - -[[deps.DataAPI]] -git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe" -uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" -version = "1.16.0" - -[[deps.DataFrames]] -deps = ["Compat", "DataAPI", "DataStructures", "Future", "InlineStrings", "InvertedIndices", "IteratorInterfaceExtensions", "LinearAlgebra", "Markdown", "Missings", "PooledArrays", "PrecompileTools", "PrettyTables", "Printf", "Random", "Reexport", "SentinelArrays", "SortingAlgorithms", "Statistics", "TableTraits", "Tables", "Unicode"] -git-tree-sha1 = "5fab31e2e01e70ad66e3e24c968c264d1cf166d6" -uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -version = "1.8.2" - -[[deps.DataStructures]] -deps = ["OrderedCollections"] -git-tree-sha1 = "6fb53a69613a0b2b68a0d12671717d307ab8b24e" -uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -version = "0.19.5" - -[[deps.DataValueInterfaces]] -git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" -uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464" -version = "1.0.0" - -[[deps.Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -version = "1.11.0" - -[[deps.Decimals]] -git-tree-sha1 = "e98abef36d02a0ec385d68cd7dadbce9b28cbd88" -uuid = "abce61dc-4473-55a0-ba07-351d65e31d42" -version = "0.4.1" - -[[deps.Distances]] -deps = ["LinearAlgebra", "Statistics", "StatsAPI"] -git-tree-sha1 = "c7e3a542b999843086e2f29dac96a618c105be1d" -uuid = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" -version = "0.10.12" - - [deps.Distances.extensions] - DistancesChainRulesCoreExt = "ChainRulesCore" - DistancesSparseArraysExt = "SparseArrays" - - [deps.Distances.weakdeps] - ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" - -[[deps.Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" -version = "1.11.0" - -[[deps.Distributions]] -deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "Roots", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] -git-tree-sha1 = "cd3c5ac74cd3923c8945c6a81518c46abd0e73a3" -uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" -version = "0.25.129" - - [deps.Distributions.extensions] - DistributionsChainRulesCoreExt = "ChainRulesCore" - DistributionsDensityInterfaceExt = "DensityInterface" - DistributionsSparseConnectivityTracerExt = "SparseConnectivityTracer" - DistributionsTestExt = "Test" - - [deps.Distributions.weakdeps] - ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d" - SparseConnectivityTracer = "9f842d2f-2579-4b1d-911e-f412cf18a3f5" - Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[[deps.DocStringExtensions]] -git-tree-sha1 = "7442a5dfe1ebb773c29cc2962a8980f47221d76c" -uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -version = "0.9.5" - -[[deps.Downloads]] -deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] -uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.7.0" - -[[deps.EnumX]] -git-tree-sha1 = "c49898e8438c828577f04b92fc9368c388ac783c" -uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" -version = "1.0.7" - -[[deps.ExprTools]] -git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec" -uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" -version = "0.1.10" - -[[deps.EzXML]] -deps = ["Printf", "XML2_jll"] -git-tree-sha1 = "7ea1aa5869e2626ccae84480e4f37185bc6f41d3" -uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" -version = "1.2.3" - -[[deps.FileIO]] -deps = ["Pkg", "Requires", "UUIDs"] -git-tree-sha1 = "6621fef488e496356c9c9625d0562c12a6070819" -uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -version = "1.20.0" -weakdeps = ["HTTP"] - - [deps.FileIO.extensions] - HTTPExt = "HTTP" - -[[deps.FilePathsBase]] -deps = ["Compat", "Dates"] -git-tree-sha1 = "3bab2c5aa25e7840a4b065805c0cdfc01f3068d2" -uuid = "48062228-2e41-5def-b9a4-89aafe57970f" -version = "0.9.24" -weakdeps = ["Mmap", "Test"] - - [deps.FilePathsBase.extensions] - FilePathsBaseMmapExt = "Mmap" - FilePathsBaseTestExt = "Test" - -[[deps.FileWatching]] -uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" -version = "1.11.0" - -[[deps.FillArrays]] -deps = ["LinearAlgebra"] -git-tree-sha1 = "2f979084d1e13948a3352cf64a25df6bd3b4dca3" -uuid = "1a297f60-69ca-5386-bcde-b61e274b549b" -version = "1.16.0" -weakdeps = ["PDMats", "SparseArrays", "StaticArrays", "Statistics"] - - [deps.FillArrays.extensions] - FillArraysPDMatsExt = "PDMats" - FillArraysSparseArraysExt = "SparseArrays" - FillArraysStaticArraysExt = "StaticArrays" - FillArraysStatisticsExt = "Statistics" - -[[deps.Future]] -deps = ["Random"] -uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" -version = "1.11.0" - -[[deps.Gamma]] -git-tree-sha1 = "86f86b6168a016ed88e4ae4e64577b98c3b59e8e" -uuid = "a0844989-3bd2-4988-8bea-c9407ab0941b" -version = "1.1.0" - -[[deps.GeneralUtils]] -deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "Graphs", "HTTP", "JSON", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "StringDistances", "UUIDs"] -git-tree-sha1 = "93293126d24d3929ef6a5067f347bc28c6582c71" -repo-rev = "main" -repo-url = "https://git.yiem.cc/ton/GeneralUtils" -uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe" -version = "0.5.10" - -[[deps.Graphs]] -deps = ["ArnoldiMethod", "DataStructures", "Inflate", "LinearAlgebra", "Random", "SimpleTraits", "SparseArrays", "Statistics"] -git-tree-sha1 = "7eb45fe833a5b7c51cf6d89c5a841d5967e44be3" -uuid = "86223c79-3864-5bf0-83f7-82e725a168b6" -version = "1.14.0" - - [deps.Graphs.extensions] - GraphsSharedArraysExt = "SharedArrays" - - [deps.Graphs.weakdeps] - Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" - SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383" - -[[deps.HTTP]] -deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"] -git-tree-sha1 = "c2c808326222b6dc4bec295a83b55f79aeec98e0" -uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "2.5.5" - -[[deps.HashArrayMappedTries]] -git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" -uuid = "076d061b-32b6-4027-95e0-9a2c6f6d7e74" -version = "0.2.0" - -[[deps.HypergeometricFunctions]] -deps = ["Gamma", "LinearAlgebra"] -git-tree-sha1 = "18d7deab5fb0440dc6a7b6993c5c27b25420de10" -uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a" -version = "0.3.29" - -[[deps.ICU_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "b3d8be712fbf9237935bde0ce9b5a736ae38fc34" -uuid = "a51ab1cf-af8e-5615-a023-bc2c838bba6b" -version = "76.2.0+0" - -[[deps.Infinity]] -deps = ["Dates", "Random", "Requires"] -git-tree-sha1 = "cf8234411cbeb98676c173f930951ea29dca3b23" -uuid = "a303e19e-6eb4-11e9-3b09-cd9505f79100" -version = "0.2.4" - -[[deps.Inflate]] -git-tree-sha1 = "d1b1b796e47d94588b3757fe84fbf65a5ec4a80d" -uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" -version = "0.1.5" - -[[deps.InlineStrings]] -git-tree-sha1 = "8f3d257792a522b4601c24a577954b0a8cd7334d" -uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" -version = "1.4.5" -weakdeps = ["ArrowTypes", "Parsers"] - - [deps.InlineStrings.extensions] - ArrowTypesExt = "ArrowTypes" - ParsersExt = "Parsers" - -[[deps.InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -version = "1.11.0" - -[[deps.Intervals]] -deps = ["ArrowTypes", "Dates", "Printf", "RecipesBase", "Serialization", "TimeZones"] -git-tree-sha1 = "d6fe00b123e32ddd17231b35d69a6394e696fd5a" -uuid = "d8418881-c3e1-53bb-8760-2df7ec849ed5" -version = "1.11.0" - -[[deps.InverseFunctions]] -git-tree-sha1 = "a779299d77cd080bf77b97535acecd73e1c5e5cb" -uuid = "3587e190-3f89-42d0-90ee-14403ec27112" -version = "0.1.17" -weakdeps = ["Dates", "Test"] - - [deps.InverseFunctions.extensions] - InverseFunctionsDatesExt = "Dates" - InverseFunctionsTestExt = "Test" - -[[deps.InvertedIndices]] -git-tree-sha1 = "6da3c4316095de0f5ee2ebd875df8721e7e0bdbe" -uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" -version = "1.3.1" - -[[deps.IrrationalConstants]] -git-tree-sha1 = "b2d91fe939cae05960e760110b328288867b5758" -uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" -version = "0.2.6" - -[[deps.IterTools]] -git-tree-sha1 = "42d5f897009e7ff2cf88db414a389e5ed1bdd023" -uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e" -version = "1.10.0" - -[[deps.IteratorInterfaceExtensions]] -git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" -uuid = "82899510-4779-5014-852e-03e436cf321d" -version = "1.0.0" - -[[deps.JLLWrappers]] -deps = ["Artifacts", "Preferences"] -git-tree-sha1 = "7204148362dafe5fe6a273f855b8ccbe4df8173e" -uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.8.0" - -[[deps.JSON]] -deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"] -git-tree-sha1 = "c89d196f5ffb64bfbf80985b699ea913b0d2c211" -uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "1.6.1" -weakdeps = ["ArrowTypes"] - - [deps.JSON.extensions] - JSONArrowExt = ["ArrowTypes"] - -[[deps.JSON3]] -deps = ["Dates", "Mmap", "Parsers", "PrecompileTools", "StructTypes", "UUIDs"] -git-tree-sha1 = "411eccfe8aba0814ffa0fdf4860913ed09c34975" -uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" -version = "1.14.3" -weakdeps = ["ArrowTypes"] - - [deps.JSON3.extensions] - JSON3ArrowExt = ["ArrowTypes"] - -[[deps.JuliaInterpreter]] -deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] -git-tree-sha1 = "58927c485919bf17ea308d9d82156de1adf4b006" -uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" -version = "0.10.12" - -[[deps.JuliaSyntaxHighlighting]] -deps = ["StyledStrings"] -uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" -version = "1.12.0" - -[[deps.Kerberos_krb5_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "0f2899fdadaab4b8f57db558ba21bdb4fb52f1f0" -uuid = "b39eb1a6-c29a-53d7-8c32-632cd16f18da" -version = "1.21.3+0" - -[[deps.LLMMCTS]] -deps = ["GeneralUtils", "JSON", "PrettyPrinting"] -git-tree-sha1 = "3dff98131dfa79be8c9bd84fc51cb0ba1832c472" -repo-rev = "main" -repo-url = "https://git.yiem.cc/ton/LLMMCTS" -uuid = "d76c5a4d-449e-4835-8cc4-dd86ec44f241" -version = "0.1.5" - -[[deps.LaTeXStrings]] -git-tree-sha1 = "dda21b8cbd6a6c40d9d02a73230f9d70fed6918c" -uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" -version = "1.4.0" - -[[deps.LayerDicts]] -git-tree-sha1 = "6087ad3521d6278ebe5c27ae55e7bbb15ca312cb" -uuid = "6f188dcb-512c-564b-bc01-e0f76e72f166" -version = "1.0.0" - -[[deps.LibCURL]] -deps = ["LibCURL_jll", "MozillaCACerts_jll"] -uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" -version = "0.6.4" - -[[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] -uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.15.0+0" - -[[deps.LibGit2]] -deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] -uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" -version = "1.11.0" - -[[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] -uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.9.0+0" - -[[deps.LibPQ]] -deps = ["CEnum", "DBInterface", "Dates", "Decimals", "DocStringExtensions", "FileWatching", "Infinity", "Intervals", "IterTools", "LayerDicts", "LibPQ_jll", "Libdl", "Memento", "OffsetArrays", "SQLStrings", "Tables", "TimeZones", "UTCDateTimes"] -git-tree-sha1 = "3d227cd13cbf1e9a54d7748dab33e078da6f9168" -uuid = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1" -version = "1.18.0" - -[[deps.LibPQ_jll]] -deps = ["Artifacts", "ICU_jll", "JLLWrappers", "Kerberos_krb5_jll", "Libdl", "OpenSSL_jll", "Zstd_jll"] -git-tree-sha1 = "c692057e05ba6da348bc45d5dab8c7a2c88da518" -uuid = "08be9ffa-1c94-5ee5-a977-46a84ec9b350" -version = "16.14.0+0" - -[[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "OpenSSL_jll"] -uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.3+1" - -[[deps.Libdl]] -uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -version = "1.11.0" - -[[deps.Libiconv_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809" -uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" -version = "1.18.0+0" - -[[deps.LinearAlgebra]] -deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] -uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -version = "1.12.0" - -[[deps.LogExpFunctions]] -deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] -git-tree-sha1 = "bba2d9aa057d8f126415de240573e86a8f39d2a1" -uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" -version = "1.0.1" - - [deps.LogExpFunctions.extensions] - LogExpFunctionsChainRulesCoreExt = "ChainRulesCore" - LogExpFunctionsChangesOfVariablesExt = "ChangesOfVariables" - LogExpFunctionsInverseFunctionsExt = "InverseFunctions" - - [deps.LogExpFunctions.weakdeps] - ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - ChangesOfVariables = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0" - InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" - -[[deps.Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" -version = "1.11.0" - -[[deps.LoweredCodeUtils]] -deps = ["CodeTracking", "Compiler", "JuliaInterpreter"] -git-tree-sha1 = "3733419e9a71156b389f3e331672d2e95436783f" -uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" -version = "3.6.2" - -[[deps.MacroTools]] -git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" -uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -version = "0.5.16" - -[[deps.Markdown]] -deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -version = "1.11.0" - -[[deps.MbedTLS]] -deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"] -git-tree-sha1 = "8785729fa736197687541f7053f6d8ab7fc44f92" -uuid = "739be429-bea8-5141-9913-cc70e7f3736d" -version = "1.1.10" - -[[deps.MbedTLS_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "ff69a2b1330bcb730b9ac1ab7dd680176f5896b8" -uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.1010+0" - -[[deps.Memento]] -deps = ["Dates", "Distributed", "Requires", "Serialization", "Sockets", "Test", "UUIDs"] -git-tree-sha1 = "e03a25cb3b6569623f8246d3d8b3faa7ce86f4ad" -uuid = "f28f55f0-a522-5efc-85c2-fe41dfb9b2d9" -version = "1.5.0" - -[[deps.Missings]] -deps = ["DataAPI"] -git-tree-sha1 = "ec4f7fbeab05d7747bdf98eb74d130a2a2ed298d" -uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" -version = "1.2.0" - -[[deps.Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" -version = "1.11.0" - -[[deps.Mocking]] -deps = ["Compat", "ExprTools"] -git-tree-sha1 = "2c140d60d7cb82badf06d8783800d0bcd1a7daa2" -uuid = "78c3b35d-d492-501b-9361-3d52fe80e533" -version = "0.8.1" - -[[deps.MozillaCACerts_jll]] -uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2025.11.4" - -[[deps.NATS]] -deps = ["Base64", "BufferedStreams", "CodecBase", "Dates", "DocStringExtensions", "JSON3", "MbedTLS", "NanoDates", "Random", "ScopedValues", "Sockets", "Sodium", "StructTypes", "URIs"] -git-tree-sha1 = "a1cdf34ba90ee5cd2658e487d3277ffafee712ce" -uuid = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a" -version = "0.1.1" - -[[deps.NanoDates]] -deps = ["Dates", "Parsers"] -git-tree-sha1 = "850a0557ae5934f6e67ac0dc5ca13d0328422d1f" -uuid = "46f1a544-deae-4307-8689-c12aa3c955c6" -version = "1.0.3" - -[[deps.NetworkOptions]] -uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.3.0" - -[[deps.OffsetArrays]] -git-tree-sha1 = "117432e406b5c023f665fa73dc26e79ec3630151" -uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" -version = "1.17.0" - - [deps.OffsetArrays.extensions] - OffsetArraysAdaptExt = "Adapt" - - [deps.OffsetArrays.weakdeps] - Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" - -[[deps.OpenBLAS_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] -uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -version = "0.3.29+0" - -[[deps.OpenLibm_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "05823500-19ac-5b8b-9628-191a04bc5112" -version = "0.8.7+0" - -[[deps.OpenSSL_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.5.4+0" - -[[deps.OpenSpecFun_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] -git-tree-sha1 = "1346c9208249809840c91b26703912dff463d335" -uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" -version = "0.5.6+0" - -[[deps.OrderedCollections]] -git-tree-sha1 = "94ba93778373a53bfd5a0caaf7d809c445292ff4" -uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "1.8.2" - -[[deps.PDMats]] -deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"] -git-tree-sha1 = "26766d4b5f1a410c218a19b85a672c6edb693c65" -uuid = "90014a1f-27ba-587c-ab20-58faa44d9150" -version = "0.11.40" -weakdeps = ["StatsBase"] - - [deps.PDMats.extensions] - StatsBaseExt = "StatsBase" - -[[deps.Parsers]] -deps = ["Dates", "PrecompileTools", "UUIDs"] -git-tree-sha1 = "32a4e09c5f29402573d673901778a0e03b0807b9" -uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.8.6" - -[[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.12.1" -weakdeps = ["REPL"] - - [deps.Pkg.extensions] - REPLExt = "REPL" - -[[deps.PooledArrays]] -deps = ["DataAPI", "Future"] -git-tree-sha1 = "36d8b4b899628fb92c2749eb488d884a926614d3" -uuid = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" -version = "1.4.3" - -[[deps.PrecompileTools]] -deps = ["Preferences"] -git-tree-sha1 = "edbeefc7a4889f528644251bdb5fc9ab5348bc2c" -uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.3.4" - -[[deps.Preferences]] -deps = ["TOML"] -git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" -uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.5.2" - -[[deps.PrettyPrinting]] -git-tree-sha1 = "142ee93724a9c5d04d78df7006670a93ed1b244e" -uuid = "54e16d92-306c-5ea0-a30b-337be88ac337" -version = "0.4.2" - -[[deps.PrettyTables]] -deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"] -git-tree-sha1 = "ebf455bb866ee6737030e3d3816bb6a0683c4325" -uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" -version = "3.4.0" - - [deps.PrettyTables.extensions] - PrettyTablesExcelExt = "XLSX" - PrettyTablesTypstryExt = "Typstry" - - [deps.PrettyTables.weakdeps] - Typstry = "f0ed7684-a786-439e-b1e3-3b82803b501e" - XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" - -[[deps.Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -version = "1.11.0" - -[[deps.PtrArrays]] -git-tree-sha1 = "4fbbafbc6251b883f4d2705356f3641f3652a7fe" -uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d" -version = "1.4.0" - -[[deps.QuadGK]] -deps = ["DataStructures", "LinearAlgebra"] -git-tree-sha1 = "5e8e8b0ab68215d7a2b14b9921a946fee794749e" -uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" -version = "2.11.3" - - [deps.QuadGK.extensions] - QuadGKEnzymeExt = "Enzyme" - - [deps.QuadGK.weakdeps] - Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" - -[[deps.REPL]] -deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -version = "1.11.0" - -[[deps.Random]] -deps = ["SHA"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -version = "1.11.0" - -[[deps.RecipesBase]] -deps = ["PrecompileTools"] -git-tree-sha1 = "5c3d09cc4f31f5fc6af001c250bf1278733100ff" -uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" -version = "1.3.4" - -[[deps.Reexport]] -git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" -uuid = "189a3867-3050-52da-a836-e630ba90ab69" -version = "1.2.2" - -[[deps.Requires]] -deps = ["UUIDs"] -git-tree-sha1 = "62389eeff14780bfe55195b7204c0d8738436d64" -uuid = "ae029012-a4dd-5104-9daa-d747884805df" -version = "1.3.1" - -[[deps.Reseau]] -deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"] -git-tree-sha1 = "0eab6d95ed40c2ef3992255c1c71e4f9748932b5" -uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36" -version = "1.3.1" - -[[deps.Revise]] -deps = ["CRC32c", "CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"] -git-tree-sha1 = "27e3ee13fc8739a59b380d6163d6a82f52c03bd7" -uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" -version = "3.15.1" -weakdeps = ["Distributed"] - - [deps.Revise.extensions] - DistributedExt = "Distributed" - -[[deps.Rmath]] -deps = ["Random", "Rmath_jll"] -git-tree-sha1 = "5b3d50eb374cea306873b371d3f8d3915a018f0b" -uuid = "79098fc4-a85e-5d69-aa6a-4863f24498fa" -version = "0.9.0" - -[[deps.Rmath_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "58cdd8fb2201a6267e1db87ff148dd6c1dbd8ad8" -uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f" -version = "0.5.1+0" - -[[deps.Roots]] -deps = ["Accessors", "CommonSolve", "Printf"] -git-tree-sha1 = "a7caaf7ba8cf307112ca443784d1b56b4a591455" -uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" -version = "3.0.5" - - [deps.Roots.extensions] - RootsChainRulesCoreExt = "ChainRulesCore" - RootsForwardDiffExt = "ForwardDiff" - RootsIntervalRootFindingExt = "IntervalRootFinding" - RootsSymPyExt = "SymPy" - RootsSymPyPythonCallExt = "SymPyPythonCall" - RootsUnitfulExt = "Unitful" - - [deps.Roots.weakdeps] - ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" - IntervalRootFinding = "d2bf35a9-74e0-55ec-b149-d360ff49b807" - SymPy = "24249f21-da20-56a4-8eb1-6a02cf4ae2e6" - SymPyPythonCall = "bc8888f7-b21e-4b7c-a06a-5d9c9496438c" - Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" - -[[deps.SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" -version = "0.7.0" - -[[deps.SQLLLM]] -deps = ["CSV", "DataFrames", "DataStructures", "Dates", "FileIO", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "PrettyPrinting", "Random", "Revise", "StatsBase", "Tables", "URIs", "UUIDs"] -git-tree-sha1 = "bae2fd2e2b087753fbb3415896be41df1ae0eb90" -repo-rev = "main" -repo-url = "https://git.yiem.cc/ton/SQLLLM" -uuid = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3" -version = "0.2.8" - -[[deps.SQLStrings]] -git-tree-sha1 = "55de0530689832b1d3d43491ee6b67bd54d3323c" -uuid = "af517c2e-c243-48fa-aab8-efac3db270f5" +name = "AgentCore" +uuid = "6e2f7b3a-9a0b-4e8e-8f8f-8f8f8f8f8f8f" +authors = ["Mario Zechner "] version = "0.1.0" -[[deps.ScopedValues]] -deps = ["HashArrayMappedTries", "Logging"] -git-tree-sha1 = "67a144433c4ce877ee6d1ada69a124d6b1ecf7be" -uuid = "7e506255-f358-4e82-b7e4-beb19740aa63" -version = "1.6.2" +[deps] +Dates = "ade2ca70-3891-5945-98fb-dc09409a37d3" +JSON3 = "0f8b85d8-8d2f-5481-9e3b-d9a10a9b6c53" +Libdl = "8f399da3-355a-58d1-55dd-a8cd37d21846" +Markdown = "d6f4372e-7a37-5ca6-90db-23e40208355e" +Mmap = "a63ad114-7ff6-5b6b-903e-90ddba579e5d" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Sockets = "6462fe0b-2de3-572b-8e7f-4c2f5e2c2e2b" +Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf2a872" +UUIDs = "cf7118a7-4649-5bc2-89ac-36d7b14660ca" -[[deps.Scratch]] -deps = ["Dates"] -git-tree-sha1 = "9b81b8393e50b7d4e6d0a9f14e192294d3b7c109" -uuid = "6c6a2e73-6563-6170-7368-637461726353" -version = "1.3.0" +[extras] +Test = "8dfed614-e22c-5e4d-98d3-97fe1b80e45d" -[[deps.SentinelArrays]] -deps = ["Dates", "Random"] -git-tree-sha1 = "084c47c7c5ce5cfecefa0a98dff69eb3646b5a80" -uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c" -version = "1.4.10" +[targets] +test = ["Test"] -[[deps.Serde]] -deps = ["CSV", "Dates", "EzXML", "JSON", "TOML", "UUIDs", "YAML"] -git-tree-sha1 = "f397fc8779cc53e4677c2708f3802c6996f28d00" -uuid = "db9b398d-9517-45f8-9a95-92af99003e0e" -version = "3.7.2" - -[[deps.Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -version = "1.11.0" - -[[deps.SimpleTraits]] -deps = ["InteractiveUtils", "MacroTools"] -git-tree-sha1 = "7ddb0b49c109481b046972c0e4ab02b2127d6a75" -uuid = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" -version = "0.9.6" - -[[deps.Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -version = "1.11.0" - -[[deps.Sodium]] -deps = ["Base64", "libsodium_jll"] -git-tree-sha1 = "907703e0d50846f300650d7225bdcab145b7bca9" -uuid = "4f5b5e99-b0ad-42cd-b47a-334e172ec8bd" -version = "1.1.2" - -[[deps.SortingAlgorithms]] -deps = ["DataStructures"] -git-tree-sha1 = "13cd91cc9be159e3f4d95b857fa2aa383b53772a" -uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" -version = "1.2.3" - -[[deps.SparseArrays]] -deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] -uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -version = "1.12.0" - -[[deps.SpecialFunctions]] -deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] -git-tree-sha1 = "6547cbdd8ce32efba0d21c5a40fa96d1a3548f9f" -uuid = "276daf66-3868-5448-9aa4-cd146d93841b" -version = "2.8.0" - - [deps.SpecialFunctions.extensions] - SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" - - [deps.SpecialFunctions.weakdeps] - ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - -[[deps.StaticArrays]] -deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] -git-tree-sha1 = "246a8bb2e6667f832eea063c3a56aef96429a3db" -uuid = "90137ffa-7385-5640-81b9-e52037218182" -version = "1.9.18" - - [deps.StaticArrays.extensions] - StaticArraysChainRulesCoreExt = "ChainRulesCore" - StaticArraysStatisticsExt = "Statistics" - - [deps.StaticArrays.weakdeps] - ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" - -[[deps.StaticArraysCore]] -git-tree-sha1 = "6ab403037779dae8c514bad259f32a447262455a" -uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" -version = "1.4.4" - -[[deps.Statistics]] -deps = ["LinearAlgebra"] -git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0" -uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -version = "1.11.1" -weakdeps = ["SparseArrays"] - - [deps.Statistics.extensions] - SparseArraysExt = ["SparseArrays"] - -[[deps.StatsAPI]] -deps = ["LinearAlgebra"] -git-tree-sha1 = "178ed29fd5b2a2cfc3bd31c13375ae925623ff36" -uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0" -version = "1.8.0" - -[[deps.StatsBase]] -deps = ["AliasTables", "DataAPI", "DataStructures", "IrrationalConstants", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] -git-tree-sha1 = "e4d7a1a0edc20af42689ea6f4f3587a2175d50ee" -uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -version = "0.34.12" - -[[deps.StatsFuns]] -deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"] -git-tree-sha1 = "770240df9a3b8888065046948f7a09b4e0f997d5" -uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c" -version = "2.2.0" - - [deps.StatsFuns.extensions] - StatsFunsChainRulesCoreExt = "ChainRulesCore" - StatsFunsInverseFunctionsExt = "InverseFunctions" - - [deps.StatsFuns.weakdeps] - ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" - -[[deps.StringDistances]] -deps = ["Distances", "StatsAPI"] -git-tree-sha1 = "cd83a04baf746e3b43b83c61b7de77ab0409b80a" -uuid = "88034a9c-02f8-509d-84a9-84ec65e18404" -version = "1.0.0" - -[[deps.StringEncodings]] -deps = ["Libiconv_jll"] -git-tree-sha1 = "b765e46ba27ecf6b44faf70df40c57aa3a547dcb" -uuid = "69024149-9ee7-55f6-a4c4-859efe599b68" -version = "0.3.7" - -[[deps.StringManipulation]] -deps = ["PrecompileTools"] -git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5" -uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e" -version = "0.4.4" - -[[deps.StructTypes]] -deps = ["Dates", "UUIDs"] -git-tree-sha1 = "159331b30e94d7b11379037feeb9b690950cace8" -uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" -version = "1.11.0" - -[[deps.StructUtils]] -deps = ["Dates", "UUIDs"] -git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8" -uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" -version = "2.8.2" - - [deps.StructUtils.extensions] - StructUtilsMeasurementsExt = ["Measurements"] - StructUtilsStaticArraysCoreExt = ["StaticArraysCore"] - StructUtilsTablesExt = ["Tables"] - - [deps.StructUtils.weakdeps] - Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" - StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" - Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" - -[[deps.StyledStrings]] -uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" -version = "1.11.0" - -[[deps.SuiteSparse]] -deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"] -uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" - -[[deps.SuiteSparse_jll]] -deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] -uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" -version = "7.8.3+2" - -[[deps.TOML]] -deps = ["Dates"] -uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" -version = "1.0.3" - -[[deps.TZJData]] -deps = ["Artifacts"] -git-tree-sha1 = "72df96b3a595b7aab1e101eb07d2a435963a97e2" -uuid = "dc5dba14-91b3-4cab-a142-028a31da12f7" -version = "1.5.0+2025b" - -[[deps.TableTraits]] -deps = ["IteratorInterfaceExtensions"] -git-tree-sha1 = "c06b2f539df1c6efa794486abfb6ed2022561a39" -uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" -version = "1.0.1" - -[[deps.Tables]] -deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"] -git-tree-sha1 = "0f38a06c83f0007bbab3cf911262841c9a0f07e0" -uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -version = "1.13.0" - -[[deps.Tar]] -deps = ["ArgTools", "SHA"] -uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" -version = "1.10.0" - -[[deps.Test]] -deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -version = "1.11.0" - -[[deps.TimeZones]] -deps = ["Artifacts", "Dates", "Downloads", "InlineStrings", "Mocking", "Printf", "Scratch", "TZJData", "Unicode", "p7zip_jll"] -git-tree-sha1 = "d422301b2a1e294e3e4214061e44f338cafe18a2" -uuid = "f269a46b-ccf7-5d73-abea-4c690281aa53" -version = "1.22.2" -weakdeps = ["RecipesBase"] - - [deps.TimeZones.extensions] - TimeZonesRecipesBaseExt = "RecipesBase" - -[[deps.TranscodingStreams]] -git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742" -uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" -version = "0.11.3" - -[[deps.URIs]] -git-tree-sha1 = "bef26fb046d031353ef97a82e3fdb6afe7f21b1a" -uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" -version = "1.6.1" - -[[deps.UTCDateTimes]] -deps = ["Dates", "TimeZones"] -git-tree-sha1 = "4af3552bf0cf4a071bf3d14bd20023ea70f31b62" -uuid = "0f7cfa37-7abf-4834-b969-a8aa512401c2" -version = "1.6.1" - -[[deps.UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -version = "1.11.0" - -[[deps.Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" -version = "1.11.0" - -[[deps.WeakRefStrings]] -deps = ["DataAPI", "InlineStrings", "Parsers"] -git-tree-sha1 = "0716e01c3b40413de5dedbc9c5c69f27cddfddfc" -uuid = "ea10d353-3f73-51f8-a26c-33c1cb351aa5" -version = "1.4.3" - -[[deps.WorkerUtilities]] -git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7" -uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60" -version = "1.6.1" - -[[deps.XML2_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"] -git-tree-sha1 = "3f3315d89fc954a28f5b471bce698ed6e27481be" -uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a" -version = "2.15.3+0" - -[[deps.YAML]] -deps = ["Base64", "Dates", "Printf", "StringEncodings"] -git-tree-sha1 = "a1c0c7585346251353cddede21f180b96388c403" -uuid = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" -version = "0.4.16" - -[[deps.YiemAgent]] -deps = ["Base64", "CSV", "DataFrames", "DataStructures", "Dates", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serde", "Serialization", "URIs", "UUIDs"] -path = "." -uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" -version = "0.7.4" - -[[deps.Zlib_jll]] -deps = ["Libdl"] -uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.3.1+2" - -[[deps.Zstd_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "446b23e73536f84e8037f5dce465e92275f6a308" -uuid = "3161d3a3-bdf6-5164-811a-617609db77b4" -version = "1.5.7+1" - -[[deps.libblastrampoline_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" -version = "5.15.0+0" - -[[deps.libsodium_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "011b0a7331b41c25524b64dc42afc9683ee89026" -uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8" -version = "1.0.21+0" - -[[deps.nghttp2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.64.0+1" - -[[deps.p7zip_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] -uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.7.0+0" +[compat] +julia = "1.9" diff --git a/Project.toml b/Project.toml index 4a4df5f..7438516 100644 --- a/Project.toml +++ b/Project.toml @@ -1,37 +1,22 @@ -name = "YiemAgent" -uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" +name = "AgentCore" +uuid = "6e2f7b3a-9a0b-4e8e-8f8f-8f8f8f8f8f8f" +authors = ["Mario Zechner "] version = "0.8.0" -authors = ["narawat lamaiin "] [deps] -Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" -DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -GeneralUtils = "c6c72f09-b708-4ac8-ac7c-2084d70108fe" -HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -LLMMCTS = "d76c5a4d-449e-4835-8cc4-dd86ec44f241" -LibPQ = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1" -NATS = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a" -PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337" +Dates = "ade2ca70-3891-5945-98fb-dc09409a37d3" +JSON3 = "0f8b85d8-8d2f-5481-9e3b-d9a10a9b6c53" +Libdl = "8f399da3-355a-58d1-55dd-a8cd37d21846" +Markdown = "d6f4372e-7a37-5ca6-90db-23e40208355e" +Mmap = "a63ad114-7ff6-5b6b-903e-90ddba579e5d" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" -SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3" -Serde = "db9b398d-9517-45f8-9a95-92af99003e0e" -Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" -UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +Sockets = "6462fe0b-2de3-572b-8e7f-4c2f5e2c2e2b" +Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf2a872" +UUIDs = "cf7118a7-4649-5bc2-89ac-36d7b14660ca" -[compat] -Base64 = "1.11.0" -CSV = "0.10.15" -DataFrames = "1.7.0" -GeneralUtils = "0.5.10" -HTTP = "2.4.0" -JSON = "1.6.1" -LLMMCTS = "0.1.5" -NATS = "0.1.0" -SQLLLM = "0.2.8" -Serde = "3.7.2" +[extras] +Test = "8dfed614-e22c-5e4d-98d3-97fe1b80e45d" + +[targets] +test = ["Test"] diff --git a/README.md b/README.md index 371084c..2908eae 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,179 @@ -version 0.1.0 -TODO: - [WORKING] build MCTS() for planning - [] executeplan() to execute the plan - -Change from version: 0.0.9 - - \ No newline at end of file +# AgentCore.jl - Julia Implementation of Pi Agent Core + +A Julia reimplementation of the `@earendil-works/pi-agent-core` package, providing a stateful agent framework for LLM interactions. + +## Overview + +This package provides: +- Low-level `agentLoop` for stateful LLM interactions with tool execution +- High-level `Agent` struct with state management, event streaming, and queueing +- `AgentHarness` for session persistence, resource management, and extension hooks +- Built-in tools for file operations (read, write, edit) and bash execution +- Session management with JSONL-based storage, compaction, and branch navigation + +## Architecture + +The Julia implementation follows the same layered architecture as the TypeScript version: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ AgentHarness │ +│ (Session persistence, resource management) │ +└─────────────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────▼───────────────────────────────────────┐ +│ Agent │ +│ (State management, event streaming, queueing) │ +└─────────────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────▼───────────────────────────────────────┐ +│ AgentLoop │ +│ (Low-level loop, tool execution) │ +└─────────────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────▼───────────────────────────────────────┐ +│ Session │ +│ (Conversation history, compaction, branching) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Installation + +```julia +using Pkg +Pkg.add("AgentCore") +``` + +## Quick Start + +```julia +using AgentCore + +# Create an agent with default configuration +agent = Agent() + +# Subscribe to events +subscribe(agent) do event, signal + if event isa MessageEndEvent + println("Received message: $(event.message)") + end +end + +# Run a prompt +prompt(agent, "Hello, how are you?") +``` + +## Core Concepts + +### Agent + +The `Agent` struct provides a high-level interface for interacting with LLMs. It manages: +- Conversation state (messages, tools, system prompt) +- Event streaming and lifecycle management +- Steering and follow-up message queues +- Abort handling + +### AgentLoop + +The `agentLoop` function implements the core agent loop that: +- Transforms `AgentMessage[]` to `Message[]` at the LLM call boundary +- Executes tool calls (parallel or sequential) +- Emits lifecycle events +- Handles steering and follow-up messages + +### AgentHarness + +The `AgentHarness` provides: +- Session persistence with JSONL storage +- Resource management (skills, prompt templates) +- Extension hooks system +- Tool execution with context +- Branch navigation and compaction + +### Sessions + +Sessions track conversation history using a tree-based structure: +- Branch-based history with compaction +- Tree navigation (moveTo, navigateTree) +- Message and metadata persistence + +## Built-in Tools + +### Bash Tool + +Execute shell commands with output capture and truncation. + +```julia +bash_tool = createBashTool() +``` + +### Read Tool + +Read files with support for text and images. + +```julia +read_tool = createReadTool() +``` + +### Write Tool + +Write content to files with automatic directory creation. + +```julia +write_tool = createWriteTool() +``` + +### Edit Tool + +Edit files using exact text replacement. + +```julia +edit_tool = createEditTool() +``` + +## Session Storage + +AgentCore supports two session storage backends: + +1. **JsonlSessionStorage** - File-based storage using JSONL format +2. **InMemorySessionStorage** - In-memory storage for testing + +## Compaction + +The compaction system manages context window usage by: +- Summarizing old conversation history +- Retaining recent messages +- Supporting iterative updates to summaries + +## Event System + +AgentCore uses a rich event system for monitoring and control: + +- `AgentStartEvent` / `AgentEndEvent` - Agent lifecycle +- `TurnStartEvent` / `TurnEndEvent` - Conversation turns +- `MessageStartEvent` / `MessageEndEvent` - Message lifecycle +- `ToolExecutionStartEvent` / `ToolExecutionEndEvent` - Tool execution + +## Examples + +See the `examples/` directory for more detailed examples. + +## Differences from TypeScript + +While maintaining API compatibility where possible, this Julia implementation: +- Uses Julia's type system for better compile-time guarantees +- Leverages Julia's multiple dispatch for extensibility +- Uses Julia's async primitives for concurrent operations +- Provides more idiomatic Julia error handling + +## Contributing + +Contributions are welcome! Please see `CONTRIBUTING.md` for details. + +## License + +MIT + +## Acknowledgments + +This is a reimplementation of the [Pi Agent Core](https://github.com/earendil-works/pi/packages/agent) package in Julia. diff --git a/aisystemprompt.md b/ai_system_prompt.md similarity index 100% rename from aisystemprompt.md rename to ai_system_prompt.md diff --git a/fast_inference_guideline.txt b/fast_inference_guideline.txt deleted file mode 100644 index bcf5564..0000000 --- a/fast_inference_guideline.txt +++ /dev/null @@ -1,72 +0,0 @@ -To make **LLM-driven inference** fast while maintaining its dynamic capabilities, there are a few practices or approaches to avoid, as they could lead to performance bottlenecks or inefficiencies. Here's what *not* to do: - ---- - -### **1. Avoid Using Overly Large Models for Every Query** -While larger LLMs like GPT-4 provide high accuracy and nuanced responses, they may slow down real-time processing due to their computational complexity. Instead: -- Use distilled or smaller models (e.g., GPT-3.5 Turbo or fine-tuned versions) for faster inference without compromising much on quality. - ---- - -### **2. Avoid Excessive Entity Preprocessing** -Don’t rely on overly complicated preprocessing steps (like advanced NER models or regex-heavy pipelines) to extract entities from the query before invoking the LLM. This could add latency. Instead: -- Design efficient prompts that allow the LLM to extract entities and generate responses simultaneously. - ---- - -### **3. Avoid Asking the LLM Multiple Separate Questions** -Running the LLM for multiple subtasks—for example, entity extraction first and response generation second—can significantly slow down the pipeline. Instead: -- Create prompts that combine tasks into one pass, e.g., *"Identify the city name and generate a weather response for this query: 'What's the weather in London?'"*. - ---- - -### **4. Don’t Overload the LLM with Context History** -Excessively lengthy conversation history or irrelevant context in your prompts can slow down inference times. Instead: -- Provide only the relevant context for each query, trimming unnecessary parts of the conversation. - ---- - -### **5. Avoid Real-Time Dependence on External APIs** -Using external APIs to fetch supplementary data (e.g., weather details or location info) during every query can introduce latency. Instead: -- Pre-fetch API data asynchronously and use the LLM to integrate it dynamically into responses. - ---- - -### **6. Avoid Running LLM on Underpowered Hardware** -Running inference on CPUs or low-spec GPUs will result in slower response times. Instead: -- Deploy the LLM on optimized infrastructure (e.g., high-performance GPUs like NVIDIA A100 or cloud platforms like Azure AI) to reduce latency. - ---- - -### **7. Skip Lengthy Generative Prompts** -Avoid prompts that encourage the LLM to produce overly detailed or verbose responses, as these take longer to process. Instead: -- Use concise prompts that focus on generating actionable or succinct answers. - ---- - -### **8. Don’t Ignore Optimization Techniques** -Failing to optimize your LLM setup can drastically impact performance. For example: -- Avoid skipping techniques like model quantization (reducing numerical precision to speed up inference) or distillation (training smaller models). - ---- - -### **9. Don’t Neglect Response Caching** -While you may not want a full caching system to avoid sunk costs, dismissing lightweight caching entirely can impact speed. Instead: -- Use temporary session-based caching for very frequent queries, without committing to a full-fledged cache infrastructure. - ---- - -### **10. Avoid One-Size-Fits-All Solutions** -Applying the same LLM inference method to all queries—whether simple or complex—will waste processing resources. Instead: -- Route basic queries to faster, specialized models and use the LLM for nuanced or multi-step queries only. - ---- - -### Summary: Focus on Efficient Design -By avoiding these pitfalls, you can ensure that LLM-driven inference remains fast and responsive: -- Optimize prompts. -- Use smaller models for simpler queries. -- Run the LLM on high-performance hardware. -- Trim unnecessary preprocessing or contextual steps. - -Would you like me to help refine a prompt or suggest specific tools to complement your implementation? Let me know! \ No newline at end of file diff --git a/src/AgentCore.jl b/src/AgentCore.jl new file mode 100644 index 0000000..420c85f --- /dev/null +++ b/src/AgentCore.jl @@ -0,0 +1,149 @@ +# AgentCore.jl - A Julia implementation of the Pi Agent Core framework +# +# This is a reimplementation of the TypeScript pi-agent-core package in idiomatic Julia. +# +# The AgentCore package provides: +# - Low-level `agentLoop` for stateful LLM interactions with tool execution +# - High-level `Agent` struct with state management, event streaming, and queueing +# - `AgentHarness` for session persistence, resource management, and extension hooks +# - Built-in tools for file operations (read, write, edit) and bash execution +# - Session management with JSONL-based storage, compaction, and branch navigation +# +# For more information about the original TypeScript implementation, see: +# https://github.com/earendil-works/pi/packages/agent + +module AgentCore + +# Core modules +include("types.jl") +include("stream_fn.jl") +include("agent_loop.jl") +include("agent.jl") + +# Harness modules +include("harness_types.jl") +include("messages.jl") +include("system_prompt.jl") +include("skills.jl") +include("prompt_templates.jl") +include("agent_harness.jl") + +# Session modules +include("session/session.jl") +include("session/jsonl_storage.jl") +include("session/jsonl_repo.jl") +include("session/memory_storage.jl") +include("session/memory_repo.jl") +include("session/repo_utils.jl") + +# Tool modules +include("tools/index.jl") +include("tools/bash.jl") +include("tools/read.jl") +include("tools/write.jl") +include("tools/edit.jl") +include("tools/edit_diff.jl") +include("tools/image.jl") +include("tools/path_utils.jl") +include("tools/file_mutation_queue.jl") + +# Compaction modules +include("compaction/compaction.jl") +include("compaction/utils.jl") +include("compaction/branch_summarization.jl") + +# Utility modules +include("utils/truncate.jl") +include("utils/shell_output.jl") +include("proxy.jl") + +# Re-export public API +export + # Core types + AgentMessage, + AgentTool, + AgentContext, + AgentEvent, + ThinkingLevel, + ToolExecutionMode, + QueueMode, + AgentState, + + # Agent + Agent, + AgentOptions, + + # AgentLoop + AgentLoopConfig, + agentLoop, + agentLoopContinue, + runAgentLoop, + runAgentLoopContinue, + + # AgentHarness + AgentHarness, + AgentHarnessOptions, + AgentHarnessEvent, + AgentHarnessResources, + AgentHarnessSystemPrompt, + + # Session + Session, + SessionStorage, + SessionRepo, + JsonlSessionStorage, + JsonlSessionRepo, + InMemorySessionStorage, + InMemorySessionRepo, + + # Tools + createBashTool, + createReadTool, + createWriteTool, + createEditTool, + ExecutionEnv, + + # Compaction + compact, + prepareCompaction, + DEFAULT_COMPACTION_SETTINGS, + generateSummary, + generateBranchSummary, + + # Utils + truncateHead, + truncateTail, + formatSize, + DEFAULT_MAX_LINES, + DEFAULT_MAX_BYTES, + + # Messages + convertToLlm, + bashExecutionToText, + + # System prompt + formatSkillsForSystemPrompt, + + # Skills + loadSkills, + formatSkillInvocation, + + # Prompt templates + loadPromptTemplates, + formatPromptTemplateInvocation, + parseCommandArgs, + substituteArgs, + + # Proxy + streamProxy, + ProxyStreamOptions, + + # Stream + setDefaultStreamFn, + getDefaultStreamFn, + + # Utility functions + uuidv7, + create_timestamp + +end diff --git a/src/MCTSexamplePrompt.py b/src/MCTSexamplePrompt.py deleted file mode 100644 index 9b52f9e..0000000 --- a/src/MCTSexamplePrompt.py +++ /dev/null @@ -1,537 +0,0 @@ -https://github.com/andyz245/LanguageAgentTreeSearch/blob/main/hotpot/hotpot.py - - -standard_prompt = ''' -Write a coherent passage of 4 short paragraphs. The end sentence of each paragraph must be: {input} -''' - -reflection_prompt = '''You are an advanced reasoning agent that can improve based on self refection. You will be given a previous reasoning trial in which you were given access to an Docstore API environment and a question to answer. You were unsuccessful in answering the question either because you guessed the wrong answer with Finish[], or you used up your set number of reasoning steps. In a few sentences, Diagnose a possible reason for failure and devise a new, concise, high level plan that aims to mitigate the same failure. Use complete sentences. -Here are some examples: -Previous Trial: -Question: The Rome Protocols were signed by three Prime Ministers one of which was assassinated as part of what? -Thought 1: I need to search Rome Protocols, find the three Prime Ministers, then find what they were assassinated as part of. -Action 1: Search[Rome Protocols] -Observation 1: The Rome Protocols were a series of three international agreements concluded in Rome on 17 March 1934 between the governments of Austria, Hungary and Italy. They were signed by Italian Prime Minister Benito Mussolini, Austrian Chancellor Engelbert Dollfuss and Hungarian Prime Minister Gyula Gömbös. All of the protocols went into effect on 12 July 1934 and were registered in League of Nations Treaty Series on 12 December 1934. -Thought 2: The three Prime Ministers were Benito Mussolini, Engelbert Dollfuss, and Gyula Gömbös. I need to search Benito Mussolini and find what he was assassinated as part of. -Action 2: Search[Benito Mussolini] -Observation 2: Benito Amilcare Andrea Mussolini (UK: MU(U)SS-ə-LEE-nee, US: MOOSS-, Italian: [beˈniːto aˈmilkare anˈdrɛːa mussoˈliːni]; 29 July 1883 – 28 April 1945) was an Italian politician and journalist who founded and led the National Fascist Party (PNF). He was Prime Minister of Italy from the March on Rome in 1922 until his deposition in 1943, as well as "Duce" of Italian fascism from the establishment of the Italian Fasces of Combat in 1919 until his summary execution in 1945 by Italian partisans. As dictator of Italy and principal founder of fascism, Mussolini inspired and supported the international spread of fascist movements during the inter-war period.Mussolini was originally a socialist politician and a journalist at the Avanti! newspaper. In 1912, he became a member of the National Directorate of the Italian Socialist Party (PSI), but he was expelled from the PSI for advocating military intervention in World War I, in opposition to the party's stance on neutrality. In 1914, Mussolini founded a new journal, Il Popolo d'Italia, and served in the Royal Italian Army during the war until he was wounded and discharged in 1917. Mussolini denounced the PSI, his views now centering on Italian nationalism instead of socialism, and later founded the fascist movement which came to oppose egalitarianism and class conflict, instead advocating "revolutionary nationalism" transcending class lines. On 31 October 1922, following the March on Rome (28–30 October), Mussolini was appointed prime minister by King Victor Emmanuel III, becoming the youngest individual to hold the office up to that time. After removing all political opposition through his secret police and outlawing labor strikes, Mussolini and his followers consolidated power through a series of laws that transformed the nation into a one-party dictatorship. Within five years, Mussolini had established dictatorial authority by both legal and illegal means and aspired to create a totalitarian state. In 1929, Mussolini signed the Lateran Treaty with the Holy See to establish Vatican City. -Mussolini's foreign policy aimed to restore the ancient grandeur of the Roman Empire by expanding Italian colonial possessions and the fascist sphere of influence. In the 1920s, he ordered the Pacification of Libya, instructed the bombing of Corfu over an incident with Greece, established a protectorate over Albania, and incorporated the city of Fiume into the Italian state via agreements with Yugoslavia. In 1936, Ethiopia was conquered following the Second Italo-Ethiopian War and merged into Italian East Africa (AOI) with Eritrea and Somalia. In 1939, Italian forces annexed Albania. Between 1936 and 1939, Mussolini ordered the successful Italian military intervention in Spain in favor of Francisco Franco during the Spanish Civil War. Mussolini's Italy initially tried to avoid the outbreak of a second global war, sending troops at the Brenner Pass to delay Anschluss and taking part in the Stresa Front, the Lytton Report, the Treaty of Lausanne, the Four-Power Pact and the Munich Agreement. However, Italy then alienated itself from Britain and France by aligning with Germany and Japan. Germany invaded Poland on 1 September 1939, resulting in declarations of war by France and the UK and the start of World War II. -On 10 June 1940, Mussolini decided to enter the war on the Axis side. Despite initial success, the subsequent Axis collapse on multiple fronts and eventual Allied invasion of Sicily made Mussolini lose the support of the population and members of the Fascist Party. As a consequence, early on 25 July 1943, the Grand Council of Fascism passed a motion of no confidence in Mussolini; later that day King Victor Emmanuel III dismissed him as head of government and had him placed in custody, appointing Pietro Badoglio to succeed him as Prime Minister. After the king agreed to an armistice with the Allies, on 12 September 1943 Mussolini was rescued from captivity in the Gran Sasso raid by German paratroopers and Waffen-SS commandos led by Major Otto-Harald Mors. Adolf Hitler, after meeting with the rescued former dictator, then put Mussolini in charge of a puppet regime in northern Italy, the Italian Social Republic (Italian: Repubblica Sociale Italiana, RSI), informally known as the Salò Republic, causing a civil war. In late April 1945, in the wake of near total defeat, Mussolini and his mistress Clara Petacci attempted to flee to Switzerland, but both were captured by Italian communist partisans and summarily executed by firing squad on 28 April 1945 near Lake Como. The bodies of Mussolini and his mistress were then taken to Milan, where they were hung upside down at a service station to publicly confirm their demise. -Thought 3: Benito Mussolini was assassinated as part of the collapse of the Axis powers during World War II. So the Rome Protocols were signed by three Prime Ministers one of which was assassinated as part of World War II. -Action 3: Finish[World War II] - -Reflection: I searched one of the prime ministers involved in the signing, then attemted to answer right away. I should have searched each of the prime ministers, then looked up 'death' on each of their pages in order to get more information before answering. - -Previous Trial: -Question: Kam Heskin plays Paige Morgan in a 2004 film directed by who? -Thought 1: I need to search Kam Heskin and Paige Morgan, find the 2004 film they are in, then find the director of the film. -Action 1: Search[Kam Heskin] -Observation 1: Kam Erika Heskin (born May 8, 1973) is an American actress. She began her career playing Caitlin Deschanel on the NBC daytime soap opera Sunset Beach (1998–1999), before appearing in films Planet of the Apes (2001) and Catch Me If You Can (2002). Heskin went on in 2003 to play Elizabeth Bennet in an independent film Pride & Prejudice: A Latter-Day Comedy, Robin in Charmed (2004), and Paige Morgan in The Prince and Me film franchise (2006–2010). -Thought 2: Kam Heskin plays Paige Morgan in The Prince and Me film franchise. I need to search The Prince and Me film franchise and find the director of the 2004 film. -Action 2: Search[The Prince and Me film franchise] -Observation 2: Could not find [The Prince and Me film franchise]. Similar: ['Kam Heskin', 'Prince of Persia', 'Harry Potter and the Half-Blood Prince (film)', 'Prince of Persia: The Sands of Time (film)', 'The Chronicles of Narnia (film series)', 'Majestic Prince (franchise)', 'Kingsman (franchise)', 'The Little Prince (2015 film)', 'Shrek (franchise)', 'Harry Potter (film series)'] -Thought 3: I need to search The Prince and Me (2004 film) instead. -Action 3: Search[The Prince and Me (2004 film)] -Observation 3: Could not find [The Prince and Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Kam Heskin', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince of Persia', 'Brooklynn Prince', 'Prince (musician)'] -Thought 4: I need to search The Prince & Me (2004 film). -Action 4: Search[The Prince & Me (2004 film)] -Observation 4: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince'] -Thought 5: I need to search The Prince & Me (2004 film) instead. -Action 5: Search[The Prince & Me (2004 film)] -Observation 5: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince'] -Thought 6: I need to search The Prince & Me (2004 film). -Action 6: Search[The Prince & Me (2004 film)] -Observation 6: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince'] - -Reflection: I got stuck in a loop where I kept trying to search 'The Prince & Me (2004 film)' but the page could not be found. Instead I should have tried to search the similar results that had a similar name to see and they were made in 2004. - -Previous Trial: -Question: The Oberoi family is part of a hotel company that has a head office in what city? -Thought 1: I need to search the Oberoi family and find the city where the hotel company's head office is located. -Action 1: Search[Oberoi family] -Observation 1: The Oberoi Group is a luxury hotel group with its head office in New Delhi, India.[2] Founded in 1934, the company owns and operates 32 luxury hotels and two river cruise ships in 7 countries, primarily under its Oberoi Hotels & Resorts and Trident brands.[3] The group also operates The Oberoi Centre for Learning and Development, which is regarded as one of Asia's top institutions for hospitality education.[4]. The foundations of the Oberoi Group date back to 1934 when The Rai Bahadur Mohan Singh Oberoi, the founder of the group, bought two properties: the Maidens in Delhi and the Clarke's in Shimla.[5] In the following years Oberoi, assisted by his two sons, Tilak Raj Singh Oberoi and Prithvi Raj Singh Oberoi (P.R.S. Oberoi -Thought 2: The Oberoi Group has its head office in New Delhi, India. I just need to finish with New Delhi. -Action 2: Finish[New Delhi] -Observation 2: Episode finished, reward = 0 - -Reflection: I appeared to have retrieved the correct information about The Oberoi Family and the location of it's head office, and provided a corresponding answer. However this answer does not exactly match the ground truth answer so I should try a different wording, such as Delhi. - -Previous trial: -{trajectory}Reflection:''' - -cot_prompt = ''' -Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. -After each observation, provide the next Thought and next Action. Here are some examples: - -Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? -Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area. -Action 1: Search[Colorado orogeny] -Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny. -Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector. -Action 2: Lookup[eastern sector] -Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2] -Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range. -Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions: -Thought 4: I need to search High Plains (United States). -Action 4: Search[High Plains (United States)] -Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3] -Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. -Action 5: Finish[1,800 to 7,000 ft] - -Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county? -Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded. -Action 1: Search[Guitars for Wounded Warriors] -Observation 1: Guitars for Wounded Warriors is the sixth solo studio album by guitarist Brian Tarquin, released in August 2014 by Cleopatra Records. In 2016, it received a Best Compilation Album nomination by the Independent Music Awards. All tracks were recorded at Tarquin's Jungle Room Studios in New Paltz (village), New York. Being moved by the lack of support for returning veterans through his life Tarquin decided to compose a heavy instrumental rock album as a way to show his appreciation to all veterans. So once again he enlisted top notch players to guest with him on the album, Billy Sheehan on Black Hawk, Ron \"Bumblefoot\" Thal and Reb Beach on Taliban Terror, Steve Morse on Freedom, Chris Poland on Charlie Surfs and Hunting, Chuck Loeb on Escape Kabul, Hal Lindes on Sand & Blood, Gary Hoey on 5 Klicks To Hell and Baghdad, Alex De Rosso Dokken on Broken Arrow, and The Flyin' Ryan Brothers on Surgical Strike. The entire album was engineered, produced and composed by Tarquin especially for each one of the guest guitarists. Partial proceeds are donated to the Fisher House Foundation from sales. -Thought 2: Guitars for Wounded Warriors was recorded in New Paltz (village), New York. I need to search New Paltz (village), New York and find which county it is in -Action 2: Search[New Paltz (village), New York] -Observation 2: New Paltz is a village in Ulster County located in the U.S. state of New York. It is approximately 80 miles (130 km) north of New York City and 70 miles (110 km) south of Albany. The population was 7,324 at the 2020 census.[3] -Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County. -Action 3: Finish[Ulster County] - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] - -{input} -''' - -cot_prompt_short = ''' -Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. -After each observation, provide the next Thought and next Action. Here are some examples: - -Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? -Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area. -Action 1: Search[Colorado orogeny] -Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny. -Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector. -Action 2: Lookup[eastern sector] -Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2] -Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range. -Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions: -Thought 4: I need to search High Plains (United States). -Action 4: Search[High Plains (United States)] -Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3] -Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. -Action 5: Finish[1,800 to 7,000 ft] - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] - -{input} -''' - -cot_prompt_feedback_short = '''You are also an advanced reasoning agent that can improve based on self refection. Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. -After each observation, provide the next Thought and next Action. Here are some examples: - -Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? -Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area. -Action 1: Search[Colorado orogeny] -Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny. -Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector. -Action 2: Lookup[eastern sector] -Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2] -Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range. -Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions: -Thought 4: I need to search High Plains (United States). -Action 4: Search[High Plains (United States)] -Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3] -Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. -Action 5: Finish[1,800 to 7,000 ft] - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] - -You have attempted to answer the following question before and failed. The following reflection(s) give a plan to avoid failing to answer the question in the same way you did previously. Use them to improve your strategy of correctly answering the given question. - -{trajectories} - -{input} -''' - -cot_prompt_feedback = '''You are also an advanced reasoning agent that can improve based on self refection. Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. -After each observation, provide the next Thought and next Action. Here are some examples: - -Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? -Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area. -Action 1: Search[Colorado orogeny] -Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny. -Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector. -Action 2: Lookup[eastern sector] -Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2] -Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range. -Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions: -Thought 4: I need to search High Plains (United States). -Action 4: Search[High Plains (United States)] -Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3] -Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. -Action 5: Finish[1,800 to 7,000 ft] - -Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county? -Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded. -Action 1: Search[Guitars for Wounded Warriors] -Observation 1: Guitars for Wounded Warriors is the sixth solo studio album by guitarist Brian Tarquin, released in August 2014 by Cleopatra Records. In 2016, it received a Best Compilation Album nomination by the Independent Music Awards. All tracks were recorded at Tarquin's Jungle Room Studios in New Paltz (village), New York. Being moved by the lack of support for returning veterans through his life Tarquin decided to compose a heavy instrumental rock album as a way to show his appreciation to all veterans. So once again he enlisted top notch players to guest with him on the album, Billy Sheehan on Black Hawk, Ron \"Bumblefoot\" Thal and Reb Beach on Taliban Terror, Steve Morse on Freedom, Chris Poland on Charlie Surfs and Hunting, Chuck Loeb on Escape Kabul, Hal Lindes on Sand & Blood, Gary Hoey on 5 Klicks To Hell and Baghdad, Alex De Rosso Dokken on Broken Arrow, and The Flyin' Ryan Brothers on Surgical Strike. The entire album was engineered, produced and composed by Tarquin especially for each one of the guest guitarists. Partial proceeds are donated to the Fisher House Foundation from sales. -Thought 2: Guitars for Wounded Warriors was recorded in New Paltz (village), New York. I need to search New Paltz (village), New York and find which county it is in -Action 2: Search[New Paltz (village), New York] -Observation 2: New Paltz is a village in Ulster County located in the U.S. state of New York. It is approximately 80 miles (130 km) north of New York City and 70 miles (110 km) south of Albany. The population was 7,324 at the 2020 census.[3] -Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County. -Action 3: Finish[Ulster County] - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] - -You have attempted to answer the following question before and failed, either because your reasoning for the answer was incorrect or the phrasing of your response did not exactly match the answer. The following reflection(s) give a plan to avoid failing to answer the question in the same way you did previously. Use them to improve your strategy of correctly answering the given question. - -{trajectories} -When providing the thought and action for the current trial, that into account these failed trajectories and make sure not to repeat the same mistakes and incorrect answers. - -{input} -''' - -vote_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a list of trajectories, decide which trajectory is most promising. Analyze each trajectory in detail and consider possible errors, then conclude in the last line "The best trajectory is {s}", where s the integer id of the trajectory. -''' - -compare_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Briefly analyze the correctness of the following two trajectories. Conclude in the last line "The more correct trajectory is 1", "The more correct trajectory is 2", or "The two trajectories are similarly correct". -''' - -score_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, analyze the following trajectory, then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. -''' - -value_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. - -Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990? -Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990. -Action 1: Search[Formula One drivers from Mexico] -Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole). -Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information. -Action 2: Lookup[Formula One drivers from Mexico] -Observation 2: No more results. -Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium. -Action 3: Search[Mexican Grand Prix winners] -Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix']. -Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners. -Action 4: Search[Mexican Grand Prix] -Thus the correctness score is 3 - -Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe? -Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United. -Action 1: Search[David Beckham's football manager] -Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park']. -Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager. -Action 2: Search[Manchester United] -Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields. -Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe. -Action 3: Search[Manchester United managers] -Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers. -Action 4: Search[list of Manchester United managers] -Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -Thus the correctness score is 4 - -Question: Which magazine was started first Arthur's Magazine or First for Women? -Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first. -Action 1: Search[Arthur's Magazine] -Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3] -Thus the correctness score is 10 - -{input} -''' - -value_prompt_feedback = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. - -Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990? -Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990. -Action 1: Search[Formula One drivers from Mexico] -Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole). -Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information. -Action 2: Lookup[Formula One drivers from Mexico] -Observation 2: No more results. -Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium. -Action 3: Search[Mexican Grand Prix winners] -Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix']. -Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners. -Action 4: Search[Mexican Grand Prix] -Thus the correctness score is 3 - -Question: Which magazine was started first Arthur's Magazine or First for Women? -Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first. -Action 1: Search[Arthur's Magazine] -Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3] -Thus the correctness score is 10 - -{trajectories} -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] -Thus the correctness score is 10 - -{input} -''' - -value_prompt_reasoning = '''You are an advanced reasoning agent that can improve based on self refection. Analyze the trajectories of your previous solutions to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. - -Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990? -Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990. -Action 1: Search[Formula One drivers from Mexico] -Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole). -Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information. -Action 2: Lookup[Formula One drivers from Mexico] -Observation 2: No more results. -Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium. -Action 3: Search[Mexican Grand Prix winners] -Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix']. -Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners. -Action 4: Search[Mexican Grand Prix] -This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners of the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested. -Thus the correctness score is 3 - -Question: Which magazine was started first Arthur's Magazine or First for Women? -Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first. -Action 1: Search[Arthur's Magazine] -Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3] -This trajectory is correct as it is reasonable to search for the first magazine provided in the question. It is also better to have simple searches corresponding to a single entity, making this the best action. -Thus the correctness score is 10 - -Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe? -Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United. -Action 1: Search[David Beckham's football manager] -Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park']. -Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager. -Action 2: Search[Manchester United] -Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields. -Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe. -Action 3: Search[Manchester United managers] -Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers. -Action 4: Search[list of Manchester United managers] -Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -This trajectory is mostly incorrect as I was not able to retrieve the relevant information to answer the question. The searches are too broad and not relevant to the specific player recruited, David Beckham. The last two actions are also too similar. It would be better to first search for David Beckham and lookup information related to his manager. -Thus the correctness score is 4 - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] -This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search. -Thus the correctness score is 10 - -{input} -''' - -value_prompt_reasoning_feedback = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. - -Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990? -Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990. -Action 1: Search[Formula One drivers from Mexico] -Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole). -Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information. -Action 2: Lookup[Formula One drivers from Mexico] -Observation 2: No more results. -Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium. -Action 3: Search[Mexican Grand Prix winners] -Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix']. -Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners. -Action 4: Search[Mexican Grand Prix] -This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners it is reasonable to checkof the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested. -Thus the correctness score is 3 - -Question: Which magazine was started first Arthur's Magazine or First for Women? -Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first. -Action 1: Search[Arthur's Magazine] -Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3] -This trajectory is correct as it is reasonable to search for the first magazine provided in the question. It is also better to have simple searches corresponding to a single entity, making this the best action. -Thus the correctness score is 10 - -Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe? -Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United. -Action 1: Search[David Beckham's football manager] -Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park']. -Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager. -Action 2: Search[Manchester United] -Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields. -Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe. -Action 3: Search[Manchester United managers] -Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers. -Action 4: Search[list of Manchester United managers] -Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 1886–87 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 2022–23 season, Manchester United have had 23 full-time managers. -This trajectory is mostly incorrect as I was not able to retrieve the relevant information to answer the question. The searches are too broad and not relevant to the specific player recruited, David Beckham. The last two actions are also too similar. It would be better to first search for David Beckham and lookup information related to his manager. -Thus the correctness score is 4 - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] -This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search. -Thus the correctness score is 10 - -{trajectories} - -{input} -''' - -value_prompt_reasoning_feedback_short = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. - -Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10. - -Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990? -Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990. -Action 1: Search[Formula One drivers from Mexico] -Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole). -Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information. -Action 2: Lookup[Formula One drivers from Mexico] -Observation 2: No more results. -Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium. -Action 3: Search[Mexican Grand Prix winners] -Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix']. -Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners. -Action 4: Search[Mexican Grand Prix] -This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners of the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested. -Thus the correctness score is 3 - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening. -Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after". -Action 2: Lookup[named after] -Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous. -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] -This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search. -Thus the correctness score is 10 - -{trajectories} - -{input} -''' - -rap_prompt = ''' -Solve a question answering task with interleaving Thought and Action steps. Thought can reason about the current situation, and Action can be three types: -(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search. -(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage. -(3) Finish[answer], which returns the answer and finishes the task. -Provide the next Thought and next Action. Here are some examples: - -Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into? -Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area. -Action 1: Search[Colorado orogeny] -Thought 2: Then I need to look up eastern sector. -Action 2: Lookup[eastern sector] -Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range. -Action 3: Search[High Plains] -Thought 4: I need to search High Plains (United States). -Action 4: Search[High Plains (United States)] -Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft. -Action 5: Finish[1,800 to 7,000 ft] - -Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county? -Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded. -Action 1: Search[Guitars for Wounded Warriors] -Thought 2: I need to search New Paltz (village), New York and find which county it is in -Action 2: Search[New Paltz (village), New York] -Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County. -Action 3: Finish[Ulster County] - -Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who? -Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after. -Action 1: Search[Milhouse] -Thought 2: I can look up "named after" for finding the specific individual Milhouse is named after. -Action 2: Lookup[named after] -Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon. -Action 3: Finish[President Richard Nixon] - -{input} -''' \ No newline at end of file diff --git a/src/YiemAgent.jl b/src/YiemAgent.jl deleted file mode 100644 index 63951b9..0000000 --- a/src/YiemAgent.jl +++ /dev/null @@ -1,47 +0,0 @@ -module YiemAgent - - # export agent - - - """ Order by dependencies of each file. The 1st included file must not depend on any other - files and each file can only depend on the file included before it. - """ - - include("type.jl") - using .type - - include("util.jl") - using .util - - include("llmfunction.jl") - using .llmfunction - - include("interface.jl") - using .interface - - -# ---------------------------------------------- 100 --------------------------------------------- # - - - - - - - - - - - - - - - - - - - - - - - -end # module YiemAgent_v1 diff --git a/src/agent.jl b/src/agent.jl new file mode 100644 index 0000000..009114a --- /dev/null +++ b/src/agent.jl @@ -0,0 +1,416 @@ +""" + agent.jl - High-level Agent struct + +This module implements the high-level Agent wrapper around the low-level agent loop, +providing state management, event streaming, and queueing for steering and follow-up messages. +""" + +module Agent + +using ..Types: * +using ..AgentLoop: * +using ..StreamFn: * + +# ============================================================================ +# Default convertToLlm function +# ============================================================================ + +function defaultConvertToLlm(messages::Vector{AgentMessage})::Vector{Message} + return filter( + (m) -> m.role == "user" || m.role == "assistant" || m.role == "toolResult", + messages, + ) +end + +# ============================================================================ +# Empty usage constant +# ============================================================================ + +const EMPTY_USAGE = Usage( + 0, 0, 0, 0, 0, UsageCost(0.0, 0.0, 0.0, 0.0, 0.0) +) + +# ============================================================================ +# Pending message queue +# ============================================================================ + +mutable struct PendingMessageQueue + messages::Vector{AgentMessage} + mode::QueueMode + + function PendingMessageQueue(mode::QueueMode) + new(AgentMessage[], mode) + end +end + +function enqueue!(queue::PendingMessageQueue, message::AgentMessage) + push!(queue.messages, message) +end + +function hasItems(queue::PendingMessageQueue)::Bool + return !isempty(queue.messages) +end + +function drain(queue::PendingMessageQueue)::Vector{AgentMessage} + if queue.mode == QUEUE_ALL + result = copy(queue.messages) + empty!(queue.messages) + return result + else + if isempty(queue.messages) + return AgentMessage[] + end + first = popfirst!(queue.messages) + return [first] + end +end + +function clear!(queue::PendingMessageQueue) + empty!(queue.messages) +end + +# ============================================================================ +# Active run state +# ============================================================================ + +mutable struct ActiveRun + promise::Promise + abort_controller::Base.Atomic{Union{Base.AbstractLock, Nothing}} +end + +# ============================================================================ +# Agent struct +# ============================================================================ + +mutable struct Agent + _state::AgentState + listeners::Set{Tuple{Function, Ref{Bool}}} + steering_queue::PendingMessageQueue + follow_up_queue::PendingMessageQueue + + convert_to_llm::Function + transform_context::Union{Function, Nothing} + stream_function::StreamFn + get_api_key::Union{Function, Nothing} + on_payload::Union{Function, Nothing} + on_response::Union{Function, Nothing} + before_tool_call::Union{Function, Nothing} + after_tool_call::Union{Function, Nothing} + prepare_next_turn::Union{Function, Nothing} + prepare_next_turn_with_context::Union{Function, Nothing} + active_run::Union{ActiveRun, Nothing} + session_id::Union{String, Nothing} + thinking_budgets::Union{Dict{String, Int64}, Nothing} + transport::String + max_retry_delay_ms::Union{Int64, Nothing} + tool_execution::ToolExecutionMode + + function Agent(options::Dict{Symbol, Any}=Dict{Symbol, Any}()) + runtime_options = merge( + Dict{Symbol, Any}( + :stream_fn => getDefaultStreamFn(), + :convertToLlm => defaultConvertToLlm, + :steeringMode => QUEUE_ONE_AT_A_TIME, + :followUpMode => QUEUE_ONE_AT_A_TIME, + :toolExecution => EXECUTION_PARALLEL, + :transport => "auto", + ), + options, + ) + + state = AgentState( + get(runtime_options, :systemPrompt, ""), + get(runtime_options, :model, Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0)), + get(runtime_options, :thinkingLevel, THINKING_OFF), + get(runtime_options, :tools, AgentTool[]), + get(runtime_options, :messages, AgentMessage[]), + ) + + new( + state, + Set{Tuple{Function, Ref{Bool}}}(), + PendingMessageQueue(QUEUE_ONE_AT_A_TIME), + PendingMessageQueue(QUEUE_ONE_AT_A_TIME), + get(runtime_options, :convertToLlm, defaultConvertToLlm), + get(runtime_options, :transformContext, nothing), + get(runtime_options, :stream_fn, getDefaultStreamFn()), + get(runtime_options, :getApiKey, nothing), + get(runtime_options, :onPayload, nothing), + get(runtime_options, :onResponse, nothing), + get(runtime_options, :beforeToolCall, nothing), + get(runtime_options, :afterToolCall, nothing), + get(runtime_options, :prepareNextTurn, nothing), + get(runtime_options, :prepareNextTurnWithContext, nothing), + nothing, + get(runtime_options, :sessionId, nothing), + get(runtime_options, :thinkingBudgets, nothing), + get(runtime_options, :transport, "auto"), + get(runtime_options, :maxRetryDelayMs, nothing), + get(runtime_options, :toolExecution, EXECUTION_PARALLEL), + ) + end +end + +# ============================================================================ +# Agent methods +# ============================================================================ + +""" + subscribe(agent, listener) + +Subscribe to agent lifecycle events. + +# Arguments +- `agent`: The agent instance +- `listener`: A function that takes (event::AgentEvent, signal::AbortSignal) + +# Returns +- A function that unsubscribes the listener +""" +function subscribe(agent::Agent, listener::Function)::Function + push!(agent.listeners, (listener, Ref{Bool}(true))) + return () -> begin + filter!(x -> x[1] != listener, agent.listeners) + end +end + +""" + get_state(agent) + +Get the current agent state. +""" +function get_state(agent::Agent)::AgentState + return agent._state +end + +""" + steer(agent, message) + +Queue a message to be injected after the current assistant turn finishes. +""" +function steer(agent::Agent, message::AgentMessage) + enqueue!(agent.steering_queue, message) +end + +""" + followUp(agent, message) + +Queue a message to run only after the agent would otherwise stop. +""" +function followUp(agent::Agent, message::AgentMessage) + enqueue!(agent.follow_up_queue, message) +end + +""" + clearSteeringQueue(agent) + +Remove all queued steering messages. +""" +function clearSteeringQueue(agent::Agent) + clear!(agent.steering_queue) +end + +""" + clearFollowUpQueue(agent) + +Remove all queued follow-up messages. +""" +function clearFollowUpQueue(agent::Agent) + clear!(agent.follow_up_queue) +end + +""" + clearAllQueues(agent) + +Remove all queued steering and follow-up messages. +""" +function clearAllQueues(agent::Agent) + clearSteeringQueue(agent) + clearFollowUpQueue(agent) +end + +""" + hasQueuedMessages(agent) + +Returns true when either queue still contains pending messages. +""" +function hasQueuedMessages(agent::Agent)::Bool + return hasItems(agent.steering_queue) || hasItems(agent.follow_up_queue) +end + +""" + abort(agent) + +Abort the current run, if one is active. +""" +function abort(agent::Agent) + if !isnothing(agent.active_run) + # TODO: Implement abort signal + end +end + +""" + waitForIdle(agent) + +Resolve when the current run and all awaited event listeners have finished. +""" +function waitForIdle(agent::Agent)::Promise + if isnothing(agent.active_run) + return Promise() + end + return agent.active_run.promise +end + +""" + reset(agent) + +Clear transcript state, runtime state, and queued messages. +""" +function reset!(agent::Agent) + agent._state.messages = AgentMessage[] + agent._state.is_streaming = false + agent._state.streaming_message = nothing + agent._state.pending_tool_calls = Set{String}() + agent._state.error_message = nothing + clearFollowUpQueue(agent) + clearSteeringQueue(agent) +end + +""" + prompt(agent, input[, images]) + +Start a new prompt from text, a single message, or a batch of messages. +""" +function prompt(agent::Agent, input::Union{String, AgentMessage, Vector{AgentMessage}}, images::Vector{ImageContent}=ImageContent[])::Nothing + if !isnothing(agent.active_run) + throw(ErrorException( + "Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion." + )) + end + messages = normalizePromptInput(agent, input, images) + runPromptMessages(agent, messages) +end + +function normalizePromptInput(agent::Agent, input::Vector{AgentMessage}, images::Vector{ImageContent})::Vector{AgentMessage} + return input +end + +function normalizePromptInput(agent::Agent, input::AgentMessage, images::Vector{ImageContent})::Vector{AgentMessage} + return [input] +end + +function normalizePromptInput(agent::Agent, input::String, images::Vector{ImageContent})::Vector{AgentMessage} + content::Vector{MessageContent} = [TextContent(input)] + if !isempty(images) + append!(content, images) + end + return [UserMessage("user", content, Int64(Dates.now(Dates.UTC).datetime))] +end + +function runPromptMessages(agent::Agent, messages::Vector{AgentMessage})::Nothing + # TODO: Implement run with lifecycle + return nothing +end + +""" + continue(agent) + +Continue from the current transcript. The last message must be a user or tool-result message. +""" +function continue!(agent::Agent)::Nothing + if !isnothing(agent.active_run) + throw(ErrorException("Agent is already processing. Wait for completion before continuing.")) + end + + last_message = agent._state.messages[end] + if isnothing(last_message) + throw(ErrorException("No messages to continue from")) + end + + if last_message.role == "assistant" + queued_steering = drain(agent.steering_queue) + if !isempty(queued_steering) + runPromptMessages(agent, queued_steering) + return nothing + end + + queued_follow_ups = drain(agent.follow_up_queue) + if !isempty(queued_follow_ups) + runPromptMessages(agent, queued_follow_ups) + return nothing + end + + throw(ErrorException("Cannot continue from message role: assistant")) + end + + # TODO: Implement run continuation + return nothing +end + +""" + createContextSnapshot(agent) + +Create a snapshot of the current context for use in the agent loop. +""" +function createContextSnapshot(agent::Agent)::AgentContext + return AgentContext( + agent._state.system_prompt, + copy(agent._state.messages), + copy(agent._state.tools), + ) +end + +""" + createLoopConfig(agent, options) + +Create the loop configuration for the agent. +""" +function createLoopConfig(agent::Agent, options::Dict{String, Any}=Dict{String, Any}())::AgentLoopConfig + skip_initial_steering_poll = get(options, "skipInitialSteeringPoll", false) + return AgentLoopConfig( + agent._state.model, + agent._state.thinking_level == THINKING_OFF ? nothing : agent._state.thinking_level, + agent.session_id, + agent.on_payload, + agent.on_response, + agent.transport, + agent.thinking_budgets, + agent.max_retry_delay_ms, + agent.tool_execution, + agent.before_tool_call, + agent.after_tool_call, + isnothing(agent.prepare_next_turn_with_context) && isnothing(agent.prepare_next_turn) ? nothing : function(context) + if !isnothing(agent.prepare_next_turn_with_context) + return agent.prepare_next_turn_with_context(context, getSignal(agent)) + end + return isnothing(agent.prepare_next_turn) ? nothing : agent.prepare_next_turn(getSignal(agent)) + end, + agent.convert_to_llm, + agent.transform_context, + agent.get_api_key, + function() + if skip_initial_steering_poll + skip_initial_steering_poll = false + return AgentMessage[] + end + return drain(agent.steering_queue) + end, + function() + return drain(agent.follow_up_queue) + end, + ) +end + +""" + getSignal(agent) + +Get the active abort signal for the current run, if any. +""" +function getSignal(agent::Agent)::Union{Nothing, Base.Atomic{Bool}} + if isnothing(agent.active_run) + return nothing + end + return agent.active_run.abort_controller +end + +end diff --git a/src/agent_loop.jl b/src/agent_loop.jl new file mode 100644 index 0000000..456cec2 --- /dev/null +++ b/src/agent_loop.jl @@ -0,0 +1,861 @@ +""" + agent_loop.jl - Low-level agent loop implementation + +This module implements the core agentLoop functionality that works with AgentMessage +throughout, transforming to Message[] only at the LLM call boundary. +""" + +module AgentLoop + +using ..Types: * +using ..StreamFn: * + +# ============================================================================ +# Event sink type +# ============================================================================ + +const AgentEventSink = Function + +# ============================================================================ +# Main agent loop function +# ============================================================================ + +function agentLoop( + prompts::Vector{AgentMessage}, + context::AgentContext, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + stream_fn::StreamFn, +)::EventStream + stream = createAgentStream() + + Threads.@spawn begin + messages = runAgentLoop( + prompts, + context, + config, + (event) -> push!(stream, event), + signal, + stream_fn, + ) + end(stream, messages) + end + + return stream +end + +# ============================================================================ +# Continue agent loop function +# ============================================================================ + +function agentLoopContinue( + context::AgentContext, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + stream_fn::StreamFn, +)::EventStream + if isempty(context.messages) + throw(ErrorException("Cannot continue: no messages in context")) + end + + if context.messages[end].role == "assistant" + throw(ErrorException("Cannot continue from message role: assistant")) + end + + stream = createAgentStream() + + Threads.@spawn begin + messages = runAgentLoopContinue( + context, + config, + (event) -> push!(stream, event), + signal, + stream_fn, + ) + end(stream, messages) + end + + return stream +end + +# ============================================================================ +# Run agent loop function +# ============================================================================ + +function runAgentLoop( + prompts::Vector{AgentMessage}, + context::AgentContext, + config::AgentLoopConfig, + emit::AgentEventSink, + signal::Union{Nothing, AbortSignal}, + stream_fn::StreamFn, +)::Vector{AgentMessage} + new_messages::Vector{AgentMessage} = copy(prompts) + current_context::AgentContext = AgentContext( + context.system_prompt, + vcat(context.messages, copy(prompts)), + context.tools, + ) + + emit(AgentStartEvent()) + emit(TurnStartEvent()) + for prompt in prompts + emit(MessageStartEvent(prompt)) + emit(MessageEndEvent(prompt)) + end + + runLoop( + current_context, + new_messages, + config, + signal, + emit, + stream_fn, + ) + return new_messages +end + +# ============================================================================ +# Run agent loop continue function +# ============================================================================ + +function runAgentLoopContinue( + context::AgentContext, + config::AgentLoopConfig, + emit::AgentEventSink, + signal::Union{Nothing, AbortSignal}, + stream_fn::StreamFn, +)::Vector{AgentMessage} + if isempty(context.messages) + throw(ErrorException("Cannot continue: no messages in context")) + end + + if context.messages[end].role == "assistant" + throw(ErrorException("Cannot continue from message role: assistant")) + end + + new_messages::Vector{AgentMessage} = [] + current_context::AgentContext = context + + emit(AgentStartEvent()) + emit(TurnStartEvent()) + + runLoop( + current_context, + new_messages, + config, + signal, + emit, + stream_fn, + ) + return new_messages +end + +# ============================================================================ +# Create agent stream function +# ============================================================================ + +function createAgentStream()::EventStream + return EventStream( + (event::AgentEvent) -> event isa AgentEndEvent, + (event::AgentEvent) -> event isa AgentEndEvent ? event.messages : AgentMessage[], + ) +end + +# ============================================================================ +# Main loop logic shared by agentLoop and agentLoopContinue +# ============================================================================ + +function runLoop( + initial_context::AgentContext, + new_messages::Vector{AgentMessage}, + initial_config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, + stream_function::StreamFn, +)::Nothing + current_context::AgentContext = initial_context + config::AgentLoopConfig = initial_config + first_turn::Bool = true + pending_messages::Vector{AgentMessage} = getSteeringMessages(config) do + get_steering_messages(config) + end + + while true + has_more_tool_calls::Bool = true + + while has_more_tool_calls || !isempty(pending_messages) + if !first_turn + emit(TurnStartEvent()) + else + first_turn = false + end + + if !isempty(pending_messages) + for message in pending_messages + emit(MessageStartEvent(message)) + emit(MessageEndEvent(message)) + push!(current_context.messages, message) + push!(new_messages, message) + end + pending_messages = AgentMessage[] + end + + message = streamAssistantResponse( + current_context, + config, + signal, + emit, + stream_function, + ) + push!(new_messages, message) + + if message.stop_reason in ("error", "aborted") + emit(TurnEndEvent(message, ToolResultMessage[])) + emit(AgentEndEvent(new_messages)) + return + end + + tool_calls = filter( + (c) -> c isa ToolCall, + message.content, + ) + + tool_results::Vector{ToolResultMessage} = [] + has_more_tool_calls = false + if !isempty(tool_calls) + executed_tool_batch = + message.stop_reason == "length" + ? failToolCallsFromTruncatedMessage(tool_calls, emit) + : executeToolCalls( + current_context, + message, + config, + signal, + emit, + ) + append!(tool_results, executed_tool_batch.messages) + has_more_tool_calls = !executed_tool_batch.terminate + + for result in tool_results + push!(current_context.messages, result) + push!(new_messages, result) + end + end + + emit(TurnEndEvent(message, tool_results)) + + next_turn_context = PrepareNextTurnContext( + message, + tool_results, + current_context, + new_messages, + ) + next_turn_snapshot = prepare_next_turn(config, next_turn_context) + + if !isnothing(next_turn_snapshot) + current_context = next_turn_snapshot.context + config = AgentLoopConfig( + model = next_turn_snapshot.model, + reasoning = next_turn_snapshot.thinking_level, + convert_to_llm = config.convert_to_llm, + transform_context = config.transform_context, + get_api_key = config.get_api_key, + should_stop_after_turn = config.should_stop_after_turn, + prepare_next_turn = config.prepare_next_turn, + get_steering_messages = config.get_steering_messages, + get_follow_up_messages = config.get_follow_up_messages, + tool_execution = config.tool_execution, + before_tool_call = config.before_tool_call, + after_tool_call = config.after_tool_call, + max_tokens = config.max_tokens, + temperature = config.temperature, + reasoning = config.reasoning, + cache_retention = config.cache_retention, + session_id = config.session_id, + headers = config.headers, + metadata = config.metadata, + transport = config.transport, + signal = signal, + api_key = config.api_key, + on_payload = config.on_payload, + on_response = config.on_response, + max_retry_delay_ms = config.max_retry_delay_ms, + ) + end + + if should_stop_after_turn(config, next_turn_context) + emit(AgentEndEvent(new_messages)) + return + end + + pending_messages = getSteeringMessages(config) do + get_steering_messages(config) + end + end + + follow_up_messages = getFollowUpMessages(config) do + get_follow_up_messages(config) + end + + if !isempty(follow_up_messages) + pending_messages = follow_up_messages + continue + end + + break + end + + emit(AgentEndEvent(new_messages)) +end + +# ============================================================================ +# Helper types +# ============================================================================ + +struct PrepareNextTurnContext + message::AssistantMessage + tool_results::Vector{ToolResultMessage} + context::AgentContext + new_messages::Vector{AgentMessage} +end + +struct AgentLoopTurnUpdate + context::Union{AgentContext, Nothing} + model::Union{Model, Nothing} + thinking_level::Union{ThinkingLevel, Nothing} +end + +# ============================================================================ +# Helper functions for getting messages from queues +# ============================================================================ + +macro getSteeringMessages(config) + :(get_steering_messages($(esc(config)))) +end + +macro getFollowUpMessages(config) + :(get_follow_up_messages($(esc(config)))) +end + +function get_steering_messages(config::AgentLoopConfig)::Vector{AgentMessage} + return isnothing(config.get_steering_messages) ? AgentMessage[] : config.get_steering_messages() +end + +function get_follow_up_messages(config::AgentLoopConfig)::Vector{AgentMessage} + return isnothing(config.get_follow_up_messages) ? AgentMessage[] : config.get_follow_up_messages() +end + +function prepare_next_turn(config::AgentLoopConfig, context::PrepareNextTurnContext)::Union{AgentLoopTurnUpdate, Nothing} + return isnothing(config.prepare_next_turn) ? nothing : config.prepare_next_turn(context) +end + +function should_stop_after_turn(config::AgentLoopConfig, context::PrepareNextTurnContext)::Bool + return isnothing(config.should_stop_after_turn) ? false : config.should_stop_after_turn(context) +end + +# ============================================================================ +# Stream assistant response function +# ============================================================================ + +function streamAssistantResponse( + context::AgentContext, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, + stream_function::StreamFn, +)::AssistantMessage + messages::Vector{AgentMessage} = context.messages + + if !isnothing(config.transform_context) + messages = config.transform_context(messages, signal) + end + + llm_messages::Vector{Message} = config.convert_to_llm(messages) + + llm_context::Context = Context( + context.system_prompt, + llm_messages, + context.tools, + ) + + resolved_api_key::Union{String, Nothing} = + !isnothing(config.get_api_key) + ? config.get_api_key(config.model.provider) + : nothing + + response = stream_function( + config.model, + llm_context, + merge( + config, + Dict(:apiKey => resolved_api_key, :signal => signal), + ), + ) + + partial_message::Union{AssistantMessage, Nothing} = nothing + added_partial::Bool = false + + for event in response + if event.type == "start" + partial_message = event.partial + push!(context.messages, partial_message) + added_partial = true + emit(MessageStartEvent(copy(partial_message))) + elseif event.type in ("text_start", "text_delta", "text_end", "thinking_start", "thinking_delta", "thinking_end", "toolcall_start", "toolcall_delta", "toolcall_end") + if !isnothing(partial_message) + partial_message = event.partial + context.messages[end] = partial_message + emit(MessageUpdateEvent(copy(partial_message), event)) + end + elseif event.type in ("done", "error") + final_message = response.result() + if added_partial + context.messages[end] = final_message + else + push!(context.messages, final_message) + end + if !added_partial + emit(MessageStartEvent(copy(final_message))) + end + emit(MessageEndEvent(final_message)) + return final_message + end + end + + final_message = response.result() + if added_partial + context.messages[end] = final_message + else + push!(context.messages, final_message) + emit(MessageStartEvent(copy(final_message))) + end + emit(MessageEndEvent(final_message)) + return final_message +end + +# ============================================================================ +# Fail tool calls from truncated message +# ============================================================================ + +struct ExecutedToolCallBatch + messages::Vector{ToolResultMessage} + terminate::Bool +end + +function failToolCallsFromTruncatedMessage( + tool_calls::Vector{ToolCall}, + emit::AgentEventSink, +)::ExecutedToolCallBatch + messages::Vector{ToolResultMessage} = [] + + for tool_call in tool_calls + emit(ToolExecutionStartEvent(tool_call.id, tool_call.name, tool_call.arguments)) + + finalized = FinalizedToolCallOutcome( + tool_call, + createErrorToolResult( + "Tool call \"$(tool_call.name)\" was not executed: the response hit the output token limit, so its arguments may be truncated. Re-issue the tool call with complete arguments.", + ), + true, + ) + + emitToolExecutionEnd(finalized, emit) + tool_result_message = createToolResultMessage(finalized) + emitToolResultMessage(tool_result_message, emit) + push!(messages, tool_result_message) + end + + return ExecutedToolCallBatch(messages, false) +end + +# ============================================================================ +# Execute tool calls +# ============================================================================ + +function executeToolCalls( + current_context::AgentContext, + assistant_message::AssistantMessage, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, +)::ExecutedToolCallBatch + tool_calls = filter( + (c) -> c isa ToolCall, + assistant_message.content, + ) + + has_sequential_tool_call = any( + (tc) -> begin + tool = findfirst((t) -> t.name == tc.name, current_context.tools) + !isnothing(tool) && tool.execution_mode == EXECUTION_SEQUENTIAL + end, + tool_calls, + ) + + if config.tool_execution == EXECUTION_SEQUENTIAL || has_sequential_tool_call + return executeToolCallsSequential( + current_context, + assistant_message, + tool_calls, + config, + signal, + emit, + ) + end + return executeToolCallsParallel( + current_context, + assistant_message, + tool_calls, + config, + signal, + emit, + ) +end + +# ============================================================================ +# Execute tool calls sequentially +# ============================================================================ + +function executeToolCallsSequential( + current_context::AgentContext, + assistant_message::AssistantMessage, + tool_calls::Vector{ToolCall}, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, +)::ExecutedToolCallBatch + finalized_calls::Vector{FinalizedToolCallOutcome} = [] + messages::Vector{ToolResultMessage} = [] + + for tool_call in tool_calls + emit(ToolExecutionStartEvent(tool_call.id, tool_call.name, tool_call.arguments)) + + preparation = prepareToolCall(current_context, assistant_message, tool_call, config, signal) + + finalized = if preparation.kind == "immediate" + FinalizedToolCallOutcome(tool_call, preparation.result, preparation.is_error) + else + executed = executePreparedToolCall(preparation, signal, emit) + finalizeExecutedToolCall( + current_context, + assistant_message, + preparation, + executed, + config, + signal, + ) + end + + emitToolExecutionEnd(finalized, emit) + tool_result_message = createToolResultMessage(finalized) + emitToolResultMessage(tool_result_message, emit) + push!(finalized_calls, finalized) + push!(messages, tool_result_message) + + if !isnothing(signal) && signal.aborted + break + end + end + + return ExecutedToolCallBatch(messages, shouldTerminateToolBatch(finalized_calls)) +end + +# ============================================================================ +# Execute tool calls in parallel +# ============================================================================ + +function executeToolCallsParallel( + current_context::AgentContext, + assistant_message::AssistantMessage, + tool_calls::Vector{ToolCall}, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, +)::ExecutedToolCallBatch + finalized_calls::Vector{Union{FinalizedToolCallOutcome, Function}} = [] + + for tool_call in tool_calls + emit(ToolExecutionStartEvent(tool_call.id, tool_call.name, tool_call.arguments)) + + preparation = prepareToolCall(current_context, assistant_message, tool_call, config, signal) + + if preparation.kind == "immediate" + finalized = FinalizedToolCallOutcome( + tool_call, + preparation.result, + preparation.is_error, + ) + emitToolExecutionEnd(finalized, emit) + push!(finalized_calls, finalized) + if !isnothing(signal) && signal.aborted + break + end + continue + end + + push!(finalized_calls, () -> begin + executed = executePreparedToolCall(preparation, signal, emit) + finalized = finalizeExecutedToolCall( + current_context, + assistant_message, + preparation, + executed, + config, + signal, + ) + emitToolExecutionEnd(finalized, emit) + return finalized + end) + + if !isnothing(signal) && signal.aborted + break + end + end + + ordered_finalized_calls = map( + (entry) -> if entry isa Function + entry() + else + entry + end, + finalized_calls, + ) + + messages::Vector{ToolResultMessage} = [] + for finalized in ordered_finalized_calls + tool_result_message = createToolResultMessage(finalized) + emitToolResultMessage(tool_result_message, emit) + push!(messages, tool_result_message) + end + + return ExecutedToolCallBatch(messages, shouldTerminateToolBatch(ordered_finalized_calls)) +end + +# ============================================================================ +# Prepared tool call types +# ============================================================================ + +struct PreparedToolCall + kind::String + tool_call::ToolCall + tool::AgentTool + args::Any +end + +struct ImmediateToolCallOutcome + kind::String + result::AgentToolResultMutable + is_error::Bool +end + +struct ExecutedToolCallOutcome + result::AgentToolResultMutable + is_error::Bool +end + +struct FinalizedToolCallOutcome + tool_call::ToolCall + result::AgentToolResultMutable + is_error::Bool +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function shouldTerminateToolBatch(finalized_calls::Vector{FinalizedToolCallOutcome})::Bool + return !isempty(finalized_calls) && all( + (finalized) -> finalized.result.terminate === true, + finalized_calls, + ) +end + +function prepareToolCallArguments(tool::AgentTool, tool_call::ToolCall)::ToolCall + if isnothing(tool.prepare_arguments) + return tool_call + end + prepared_arguments = tool.prepare_arguments(tool_call.arguments) + if prepared_arguments === tool_call.arguments + return tool_call + end + return ToolCall( + tool_call.type, + tool_call.id, + tool_call.name, + prepared_arguments, + tool_call.partial_json, + ) +end + +function prepareToolCall( + current_context::AgentContext, + assistant_message::AssistantMessage, + tool_call::ToolCall, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, +)::Union{PreparedToolCall, ImmediateToolCallOutcome} + tool = findfirst((t) -> t.name == tool_call.name, current_context.tools) + if isnothing(tool) + return ImmediateToolCallOutcome("immediate", createErrorToolResult("Tool $(tool_call.name) not found"), true) + end + + try + prepared_tool_call = prepareToolCallArguments(tool, tool_call) + validated_args = validateToolArguments(tool, prepared_tool_call) + + if !isnothing(config.before_tool_call) + before_result = config.before_tool_call( + BeforeToolCallContext(assistant_message, tool_call, validated_args, current_context), + signal, + ) + if !isnothing(signal) && signal.aborted + return ImmediateToolCallOutcome("immediate", createErrorToolResult("Operation aborted"), true) + end + if !isnothing(before_result) && before_result.block + reason = isnothing(before_result.reason) ? "Tool execution was blocked" : before_result.reason + return ImmediateToolCallOutcome("immediate", createErrorToolResult(reason), true) + end + end + + if !isnothing(signal) && signal.aborted + return ImmediateToolCallOutcome("immediate", createErrorToolResult("Operation aborted"), true) + end + + return PreparedToolCall("prepared", tool_call, tool, validated_args) + catch error + return ImmediateToolCallOutcome("immediate", createErrorToolResult(string(error)), true) + end +end + +function executePreparedToolCall( + prepared::PreparedToolCall, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, +)::ExecutedToolCallOutcome + update_events::Vector{Future} = [] + accepting_updates::Bool = true + + try + result = prepared.tool.execute( + prepared.tool_call.id, + prepared.args, + signal, + (partial_result) -> begin + if !accepting_updates + return + end + push!( + update_events, + Threads.@spawn begin + emit( + ToolExecutionUpdateEvent( + prepared.tool_call.id, + prepared.tool_call.name, + prepared.tool_call.arguments, + partial_result, + ), + ) + end, + ) + end, + ) + accepting_updates = false + wait.(update_events) + return ExecutedToolCallOutcome(result, false) + catch error + accepting_updates = false + wait.(update_events) + return ExecutedToolCallOutcome(createErrorToolResult(string(error)), true) + finally + accepting_updates = false + end +end + +function finalizeExecutedToolCall( + current_context::AgentContext, + assistant_message::AssistantMessage, + prepared::PreparedToolCall, + executed::ExecutedToolCallOutcome, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, +)::FinalizedToolCallOutcome + result = executed.result + is_error = executed.is_error + + if !isnothing(config.after_tool_call) + try + after_result = config.after_tool_call( + AfterToolCallContext( + assistant_message, + prepared.tool_call, + prepared.args, + result, + is_error, + current_context, + ), + signal, + ) + if !isnothing(after_result) + result = AgentToolResultMutable( + isnothing(after_result.content) ? result.content : after_result.content, + isnothing(after_result.details) ? result.details : after_result.details, + isnothing(after_result.usage) ? result.usage : after_result.usage, + result.added_tool_names, + isnothing(after_result.terminate) ? result.terminate : after_result.terminate, + ) + is_error = isnothing(after_result.is_error) ? is_error : after_result.is_error + end + catch error + result = createErrorToolResult(string(error)) + is_error = true + end + end + + return FinalizedToolCallOutcome(prepared.tool_call, result, is_error) +end + +function createErrorToolResult(message::String)::AgentToolResultMutable + return AgentToolResultMutable([TextContent(message)], Dict{String, Any}(), nothing, nothing, nothing) +end + +function emitToolExecutionEnd(finalized::FinalizedToolCallOutcome, emit::AgentEventSink)::Nothing + emit(ToolExecutionEndEvent( + finalized.tool_call.id, + finalized.tool_call.name, + finalized.result, + finalized.is_error, + )) + return nothing +end + +function createToolResultMessage(finalized::FinalizedToolCallOutcome)::ToolResultMessage + return ToolResultMessage( + "toolResult", + finalized.tool_call.id, + finalized.tool_call.name, + isnothing(finalized.result.content) ? MessageContent[] : finalized.result.content, + finalized.result.details, + finalized.result.usage, + finalized.result.added_tool_names, + finalized.is_error, + Dates.now(Dates.UTC).datetime, + ) +end + +function emitToolResultMessage(tool_result_message::ToolResultMessage, emit::AgentEventSink)::Nothing + emit(MessageStartEvent(tool_result_message)) + emit(MessageEndEvent(tool_result_message)) + return nothing +end + +# ============================================================================ +# Validation helper +# ============================================================================ + +function validateToolArguments(tool::AgentTool, tool_call::ToolCall)::Any + # Simplified validation - in a full implementation, this would use TypeBox-like validation + return tool_call.arguments +end + +end diff --git a/src/harness_types.jl b/src/harness_types.jl new file mode 100644 index 0000000..0a583cd --- /dev/null +++ b/src/harness_types.jl @@ -0,0 +1,1083 @@ +""" + harness_types.jl - Extended types for AgentHarness + +This module defines the extended types used by the AgentHarness. +""" + +module HarnessTypes + +using ..Types: * +using ..Session: Session + +# ============================================================================ +# Result type +# ============================================================================ + +abstract type Result{TValue, TError} end + +struct Ok{TValue, TError} <: Result{TValue, TError} + value::TValue +end + +struct Err{TValue, TError} <: Result{TValue, TError} + error::TError +end + +function ok{TValue, TError}(value::TValue)::Ok{TValue, TError} + return Ok{TValue, TError}(value) +end + +function err{TValue, TError}(error::TError)::Err{TValue, TError} + return Err{TValue, TError}(error) +end + +function getOrThrow{TValue, TError}(result::Result{TValue, TError})::TValue + if result isa Ok + return result.value + else + throw(result.error) + end +end + +function getOrUndefined{TValue<:AbstractDict, TError}(result::Result{TValue, TError})::Union{TValue, Nothing} + if result isa Ok + return result.value + else + return nothing + end +end + +function toError(error::Any)::Error + if error isa Error + return error + elseif error isa AbstractString + return ErrorException(error) + else + try + return ErrorException(string(error)) + catch + return ErrorException("Unknown error") + end + end +end + +# ============================================================================ +# Skill types +# ============================================================================ + +mutable struct Skill + name::String + description::String + content::String + filePath::String + disableModelInvocation::Bool +end + +mutable struct PromptTemplate + name::String + description::Union{String, Nothing} + content::String +end + +mutable struct AgentHarnessResources{TSkill<:Skill, TPromptTemplate<:PromptTemplate} + promptTemplates::Union{Vector{TPromptTemplate}, Nothing} + skills::Union{Vector{TSkill}, Nothing} +end + +# ============================================================================ +# Tool types +# ============================================================================ + +mutable struct AgentHarnessTool{TContext, TParameters, TDetails} + name::String + label::String + description::String + parameters::TParameters + execute::Function + prepareArguments::Union{Function, Nothing} + executionMode::Union{ToolExecutionMode, Nothing} +end + +mutable struct AgentHarnessToolContextSource{TContext} + context::Union{TContext, Function} +end + +# ============================================================================ +# Stream options +# ============================================================================ + +mutable struct AgentHarnessStreamOptions + transport::Union{String, Nothing} + timeout_ms::Union{Int64, Nothing} + max_retries::Union{Int64, Nothing} + max_retry_delay_ms::Union{Int64, Nothing} + headers::Union{Dict{String, String}, Nothing} + metadata::Union{Dict{String, Any}, Nothing} + cache_retention::Union{String, Nothing} +end + +mutable struct AgentHarnessStreamOptionsPatch + transport::Union{String, Nothing} + timeout_ms::Union{Int64, Nothing} + max_retries::Union{Int64, Nothing} + max_retry_delay_ms::Union{Int64, Nothing} + cache_retention::Union{String, Nothing} + headers::Union{Dict{String, String}, Nothing} + metadata::Union{Dict{String, Any}, Nothing} +end + +# ============================================================================ +# File system types +# ============================================================================ + +const FileKind = String +const FILE_KIND_FILE = "file" +const FILE_KIND_DIRECTORY = "directory" +const FILE_KIND_SYMLINK = "symlink" + +const FileErrorCode = String +const FILE_ERROR_ABORTED = "aborted" +const FILE_ERROR_NOT_FOUND = "not_found" +const FILE_ERROR_PERMISSION_DENIED = "permission_denied" +const FILE_ERROR_NOT_DIRECTORY = "not_directory" +const FILE_ERROR_IS_DIRECTORY = "is_directory" +const FILE_ERROR_INVALID = "invalid" +const FILE_ERROR_NOT_SUPPORTED = "not_supported" +const FILE_ERROR_UNKNOWN = "unknown" + +mutable struct FileError <: Exception + code::FileErrorCode + message::String + path::Union{String, Nothing} + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Execution error types +# ============================================================================ + +const ExecutionErrorCode = String +const EXECUTION_ERROR_ABORTED = "aborted" +const EXECUTION_ERROR_TIMEOUT = "timeout" +const EXECUTION_ERROR_SHELL_UNAVAILABLE = "shell_unavailable" +const EXECISION_ERROR_SPAWN_ERROR = "spawn_error" +const EXECUTION_ERROR_CALLBACK_ERROR = "callback_error" +const EXECUTION_ERROR_UNKNOWN = "unknown" + +mutable struct ExecutionError <: Exception + code::ExecutionErrorCode + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Compaction error types +# ============================================================================ + +const CompactionErrorCode = String +const COMPACTION_ERROR_ABORTED = "aborted" +const COMPACTION_ERROR_SUMMARIZATION_FAILED = "summarization_failed" +const COMPACTION_ERROR_INVALID_SESSION = "invalid_session" +const COMPACTION_ERROR_UNKNOWN = "unknown" + +mutable struct CompactionError <: Exception + code::CompactionErrorCode + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Branch summary error types +# ============================================================================ + +const BranchSummaryErrorCode = String +const BRANCH_SUMMARY_ERROR_ABORTED = "aborted" +const BRANCH_SUMMARY_ERROR_SUMMARIZATION_FAILED = "summarization_failed" +const BRANCH_SUMMARY_ERROR_INVALID_SESSION = "invalid_session" + +mutable struct BranchSummaryError <: Exception + code::BranchSummaryErrorCode + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Session error types +# ============================================================================ + +const SessionErrorCode = String +const SESSION_ERROR_NOT_FOUND = "not_found" +const SESSION_ERROR_INVALID_SESSION = "invalid_session" +const SESSION_ERROR_INVALID_ENTRY = "invalid_entry" +const SESSION_ERROR_INVALID_FORK_TARGET = "invalid_fork_target" +const SESSION_ERROR_STORAGE = "storage" +const SESSION_ERROR_UNKNOWN = "unknown" + +mutable struct SessionError <: Exception + code::SessionErrorCode + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Agent harness error types +# ============================================================================ + +const AgentHarnessErrorCode = String +const AGENT_HARNESS_ERROR_BUSY = "busy" +const AGENT_HARNESS_ERROR_INVALID_STATE = "invalid_state" +const AGENT_HARNESS_ERROR_INVALID_ARGUMENT = "invalid_argument" +const AGENT_HARNESS_ERROR_SESSION = "session" +const AGENT_HARNESS_ERROR_HOOK = "hook" +const AGENT_HARNESS_ERROR_AUTH = "auth" +const AGENT_HARNESS_ERROR_COMPACTION = "compaction" +const AGENT_HARNESS_ERROR_BRANCH_SUMMARY = "branch_summary" +const AGENT_HARNESS_ERROR_UNKNOWN = "unknown" + +mutable struct AgentHarnessError <: Exception + code::AgentHarnessErrorCode + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# File system interface +# ============================================================================ + +abstract type FileSystem end + +function absolutePath(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{String, FileError} + return err(FileError("not_supported", "absolutePath not implemented", path, nothing)) +end + +function joinPath(fs::FileSystem, parts::Vector{String}, abortSignal::Union{Nothing, Any})::Result{String, FileError} + return err(FileError("not_supported", "joinPath not implemented", nothing, nothing)) +end + +function readTextFile(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{String, FileError} + return err(FileError("not_supported", "readTextFile not implemented", path, nothing)) +end + +function readTextLines( + fs::FileSystem, + path::String, + options::Dict{String, Any}, +)::Result{Vector{String}, FileError} + return err(FileError("not_supported", "readTextLines not implemented", path, nothing)) +end + +function readBinaryFile(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{Vector{UInt8}, FileError} + return err(FileError("not_supported", "readBinaryFile not implemented", path, nothing)) +end + +function writeFile(fs::FileSystem, path::String, content::Union{String, Vector{UInt8}}, abortSignal::Union{Nothing, Any})::Result{Nothing, FileError} + return err(FileError("not_supported", "writeFile not implemented", path, nothing)) +end + +function appendFile(fs::FileSystem, path::String, content::Union{String, Vector{UInt8}}, abortSignal::Union{Nothing, Any})::Result{Nothing, FileError} + return err(FileError("not_supported", "appendFile not implemented", path, nothing)) +end + +function fileInfo(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{FileInfo, FileError} + return err(FileError("not_supported", "fileInfo not implemented", path, nothing)) +end + +function listDir(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{Vector{FileInfo}, FileError} + return err(FileError("not_supported", "listDir not implemented", path, nothing)) +end + +function canonicalPath(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{String, FileError} + return err(FileError("not_supported", "canonicalPath not implemented", path, nothing)) +end + +function exists(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{Bool, FileError} + return err(FileError("not_supported", "exists not implemented", path, nothing)) +end + +function createDir( + fs::FileSystem, + path::String, + options::Dict{String, Any}, +)::Result{Nothing, FileError} + return err(FileError("not_supported", "createDir not implemented", path, nothing)) +end + +function remove( + fs::FileSystem, + path::String, + options::Dict{String, Any}, +)::Result{Nothing, FileError} + return err(FileError("not_supported", "remove not implemented", path, nothing)) +end + +function createTempDir(fs::FileSystem, prefix::String="tmp-", abortSignal::Union{Nothing, Any})::Result{String, FileError} + return err(FileError("not_supported", "createTempDir not implemented", nothing, nothing)) +end + +function createTempFile(fs::FileSystem, options::Dict{String, Any})::Result{String, FileError} + return err(FileError("not_supported", "createTempFile not implemented", nothing, nothing)) +end + +function cleanup(fs::FileSystem)::Nothing + return nothing +end + +# ============================================================================ +# Shell interface +# ============================================================================ + +mutable struct ShellExecOptions + cwd::Union{String, Nothing} + env::Union{Dict{String, String}, Nothing} + inheritEnv::Bool + timeout::Union{Int64, Nothing} + abortSignal::Union{Any, Nothing} + onStdout::Union{Function, Nothing} + onStderr::Union{Function, Nothing} +end + +abstract type Shell end + +function exec(shell::Shell, command::String, options::Dict{String, Any})::Result{Dict{String, Any}, ExecutionError} + return err(ExecutionError("not_supported", "exec not implemented", nothing)) +end + +function cleanup(shell::Shell)::Nothing + return nothing +end + +# ============================================================================ +# Execution environment +# ============================================================================ + +abstract type ExecutionEnv <: FileSystem, Shell end + +# ============================================================================ +# Session tree entry types +# ============================================================================ + +abstract type SessionTreeEntry end + +struct MessageEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + message::AgentMessage +end + +struct ThinkingLevelChangeEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + thinking_level::String +end + +struct ModelChangeEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + provider::String + model_id::String +end + +struct ActiveToolsChangeEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + active_tool_names::Vector{String} +end + +struct CompactionEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + summary::String + first_kept_entry_id::Union{String, Nothing} + tokens_before::Int64 + retained_tail::Union{Vector{AgentMessage}, Nothing} + details::Union{Any, Nothing} + usage::Union{Usage, Nothing} + from_hook::Bool +end + +struct BranchSummaryEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + from_id::String + summary::String + details::Union{Any, Nothing} + usage::Union{Usage, Nothing} + from_hook::Bool +end + +struct CustomEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + custom_type::String + data::Union{Any, Nothing} +end + +struct CustomMessageEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + custom_type::String + content::String + details::Union{Any, Nothing} + display::Bool +end + +struct LabelEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + target_id::String + label::Union{String, Nothing} +end + +struct SessionInfoEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + name::Union{String, Nothing} +end + +struct LeafEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + target_id::Union{String, Nothing} +end + +# ============================================================================ +# Session context +# ============================================================================ + +struct SessionContext + messages::Vector{AgentMessage} + thinking_level::String + model::Union{Dict{String, String}, Nothing} + active_tool_names::Union{Vector{String}, Nothing} +end + +# ============================================================================ +# Session stats +# ============================================================================ + +struct SessionStats + message_count::Int64 + cached_tokens::Int64 + uncached_tokens::Int64 + total_tokens::Int64 + cost_total::Float64 +end + +# ============================================================================ +# Session metadata +# ============================================================================ + +mutable struct SessionMetadata + id::String + created_at::String +end + +mutable struct JsonlSessionMetadata <: SessionMetadata + id::String + created_at::String + cwd::String + path::String + parent_session_path::Union{String, Nothing} + metadata::Union{Dict{String, Any}, Nothing} +end + +# ============================================================================ +# Session storage interface +# ============================================================================ + +abstract type SessionStorage{T<:SessionMetadata} end + +function getMetadata(storage::SessionStorage)::Promise{T} + return Promise() +end + +function getLeafId(storage::SessionStorage)::Promise{Union{String, Nothing}} + return Promise() +end + +function setLeafId(storage::SessionStorage, leaf_id::String)::Promise{Nothing} + return Promise() +end + +function createEntryId(storage::SessionStorage)::Promise{String} + return Promise() +end + +function appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Promise{Nothing} + return Promise() +end + +function getEntry(storage::SessionStorage, id::String)::Promise{Union{SessionTreeEntry, Nothing}} + return Promise() +end + +function findEntries(storage::SessionStorage, type::String)::Promise{Vector{SessionTreeEntry}} + return Promise() +end + +function getLabel(storage::SessionStorage, id::String)::Promise{Union{String, Nothing}} + return Promise() +end + +function getSessionName(storage::SessionStorage)::Promise{Union{String, Nothing}} + return Promise() +end + +function getSessionStats(storage::SessionStorage)::Promise{SessionStats} + return Promise() +end + +function getPathToRootOrCompaction(storage::SessionStorage, leaf_id::String)::Promise{Vector{SessionTreeEntry}} + return Promise() +end + +function getEntries(storage::SessionStorage, options::Dict{String, Any})::Promise{Vector{SessionTreeEntry}} + return Promise() +end + +# ============================================================================ +# Session repo interface +# ============================================================================ + +abstract type SessionRepo< + TMetadata<:SessionMetadata, + TCreateOptions, + TListOptions +> end + +function create(repo::SessionRepo, options::TCreateOptions)::Promise{Session} + return Promise() +end + +function open(repo::SessionRepo, metadata::TMetadata)::Promise{Session} + return Promise() +end + +function list(repo::SessionRepo, options::TListOptions)::Promise{Vector{TMetadata}} + return Promise() +end + +function delete(repo::SessionRepo, metadata::TMetadata)::Promise{Nothing} + return Promise() +end + +function fork(repo::SessionRepo, source::TMetadata, options::Dict{String, Any})::Promise{Session} + return Promise() +end + +# ============================================================================ +# Pending session write +# ============================================================================ + +mutable struct PendingSessionWrite + type::String + message::Union{AgentMessage, Nothing} + provider::Union{String, Nothing} + model_id::Union{String, Nothing} + thinking_level::Union{String, Nothing} + active_tool_names::Union{Vector{String}, Nothing} + custom_type::Union{String, Nothing} + data::Union{Any, Nothing} + content::Union{String, Nothing} + display::Union{Bool, Nothing} + target_id::Union{String, Nothing} + label::Union{String, Nothing} + name::Union{String, Nothing} +end + +# ============================================================================ +# Queue update event +# ============================================================================ + +mutable struct QueueUpdateEvent + type::String + steer::Vector{AgentMessage} + followUp::Vector{AgentMessage} + nextTurn::Vector{AgentMessage} +end + +# ============================================================================ +# Save point event +# ============================================================================ + +mutable struct SavePointEvent + type::String + had_pending_mutations::Bool +end + +# ============================================================================ +# Abort event +# ============================================================================ + +mutable struct AbortEvent + type::String + cleared_steer::Vector{AgentMessage} + cleared_follow_up::Vector{AgentMessage} +end + +# ============================================================================ +# Settled event +# ============================================================================ + +mutable struct SettledEvent + type::String + next_turn_count::Int64 +end + +# ============================================================================ +# Before agent start event +# ============================================================================ + +mutable struct BeforeAgentStartEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} + type::String + prompt::String + images::Union{Vector{ImageContent}, Nothing} + system_prompt::String + resources::AgentHarnessResources{TSkill, TPromptTemplate} +end + +# ============================================================================ +# Context event +# ============================================================================ + +mutable struct ContextEvent + type::String + messages::Vector{AgentMessage} +end + +# ============================================================================ +# Before provider request event +# ============================================================================ + +mutable struct BeforeProviderRequestEvent + type::String + model::Model + session_id::String + stream_options::AgentHarnessStreamOptions +end + +# ============================================================================ +# Before provider payload event +# ============================================================================ + +mutable struct BeforeProviderPayloadEvent + type::String + model::Model + payload::Any +end + +# ============================================================================ +# After provider response event +# ============================================================================ + +mutable struct AfterProviderResponseEvent + type::String + status::Int64 + headers::Dict{String, String} +end + +# ============================================================================ +# Tool call event +# ============================================================================ + +mutable struct ToolCallEvent + type::String + tool_call_id::String + tool_name::String + input::Dict{String, Any} +end + +# ============================================================================ +# Tool result event +# ============================================================================ + +mutable struct ToolResultEvent + type::String + tool_call_id::String + tool_name::String + input::Dict{String, Any} + content::Vector{MessageContent} + details::Any + is_error::Bool + usage::Union{Usage, Nothing} +end + +# ============================================================================ +# Session before compact event +# ============================================================================ + +mutable struct SessionBeforeCompactEvent + type::String + preparation::Any + branch_entries::Vector{SessionTreeEntry} + custom_instructions::Union{String, Nothing} + signal::Any +end + +# ============================================================================ +# Session compact event +# ============================================================================ + +mutable struct SessionCompactEvent + type::String + compaction_entry::CompactionEntry + from_hook::Bool +end + +# ============================================================================ +# Session before tree event +# ============================================================================ + +mutable struct SessionBeforeTreeEvent + type::String + preparation::Any + signal::Any +end + +# ============================================================================ +# Session tree event +# ============================================================================ + +mutable struct SessionTreeEvent + type::String + new_leaf_id::Union{String, Nothing} + old_leaf_id::Union{String, Nothing} + summary_entry::Union{BranchSummaryEntry, Nothing} + from_hook::Union{Bool, Nothing} +end + +# ============================================================================ +# Retry scheduled event +# ============================================================================ + +mutable struct RetryScheduledEvent + type::String + operation::String + attempt::Int64 + max_attempts::Int64 + delay_ms::Int64 + error_message::String +end + +# ============================================================================ +# Retry attempt start event +# ============================================================================ + +mutable struct RetryAttemptStartEvent + type::String + operation::String +end + +# ============================================================================ +# Retry finished event +# ============================================================================ + +mutable struct RetryFinishedEvent + type::String + operation::String +end + +# ============================================================================ +# Model update event +# ============================================================================ + +mutable struct ModelUpdateEvent + type::String + model::Model + previous_model::Union{Model, Nothing} + source::String +end + +# ============================================================================ +# Thinking level update event +# ============================================================================ + +mutable struct ThinkingLevelUpdateEvent + type::String + level::ThinkingLevel + previous_level::ThinkingLevel +end + +# ============================================================================ +# Tools update event +# ============================================================================ + +mutable struct ToolsUpdateEvent + type::String + tool_names::Vector{String} + previous_tool_names::Vector{String} + active_tool_names::Vector{String} + previous_active_tool_names::Vector{String} + source::String +end + +# ============================================================================ +# Resources update event +# ============================================================================ + +mutable struct ResourcesUpdateEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} + type::String + resources::AgentHarnessResources{TSkill, TPromptTemplate} + previous_resources::AgentHarnessResources{TSkill, TPromptTemplate} +end + +# ============================================================================ +# Agent harness own events +# ============================================================================ + +abstract type AgentHarnessOwnEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} end + +# ============================================================================ +# Agent harness event +# ============================================================================ + +abstract type AgentHarnessEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} <: AgentEvent, AgentHarnessOwnEvent{TSkill, TPromptTemplate} end + +# ============================================================================ +# Before agent start result +# ============================================================================ + +mutable struct BeforeAgentStartResult + messages::Union{Vector{AgentMessage}, Nothing} + system_prompt::Union{String, Nothing} +end + +# ============================================================================ +# Context result +# ============================================================================ + +mutable struct ContextResult + messages::Vector{AgentMessage} +end + +# ============================================================================ +# Before provider request result +# ============================================================================ + +mutable struct BeforeProviderRequestResult + stream_options::Union{AgentHarnessStreamOptionsPatch, Nothing} +end + +# ============================================================================ +# Before provider payload result +# ============================================================================ + +mutable struct BeforeProviderPayloadResult + payload::Any +end + +# ============================================================================ +# Tool call result +# ============================================================================ + +mutable struct ToolCallResult + block::Union{Bool, Nothing} + reason::Union{String, Nothing} +end + +# ============================================================================ +# Tool result patch +# ============================================================================ + +mutable struct ToolResultPatch + content::Union{Vector{MessageContent}, Nothing} + details::Union{Any, Nothing} + is_error::Union{Bool, Nothing} + usage::Union{Usage, Nothing} + terminate::Union{Bool, Nothing} +end + +# ============================================================================ +# Session before compact result +# ============================================================================ + +mutable struct SessionBeforeCompactResult + cancel::Union{Bool, Nothing} + compaction::Union{CompactResult, Nothing} +end + +# ============================================================================ +# Session before tree result +# ============================================================================ + +mutable struct SessionBeforeTreeResult + cancel::Union{Bool, Nothing} + summary::Union{Dict{String, Any}, Nothing} + custom_instructions::Union{String, Nothing} + replace_instructions::Union{Bool, Nothing} + label::Union{String, Nothing} +end + +# ============================================================================ +# Agent harness event result map +# ============================================================================ + +# ============================================================================ +# Agent harness prompt options +# ============================================================================ + +mutable struct AgentHarnessPromptOptions + images::Union{Vector{ImageContent}, Nothing} +end + +# ============================================================================ +# Abort result +# ============================================================================ + +mutable struct AbortResult + cleared_steer::Vector{AgentMessage} + cleared_follow_up::Vector{AgentMessage} +end + +# ============================================================================ +# Compact result +# ============================================================================ + +mutable struct CompactResult + summary::String + first_kept_entry_id::Union{String, Nothing} + tokens_before::Int64 + usage::Union{Usage, Nothing} + retained_tail::Union{Vector{AgentMessage}, Nothing} + details::Union{Any, Nothing} +end + +# ============================================================================ +# Navigate tree result +# ============================================================================ + +mutable struct NavigateTreeResult + cancelled::Bool + editor_text::Union{String, Nothing} + summary_entry::Union{BranchSummaryEntry, Nothing} +end + +# ============================================================================ +# Compaction settings +# ============================================================================ + +mutable struct CompactionSettings + enabled::Bool + reserve_tokens::Int64 + keep_recent_tokens::Int64 +end + +const DEFAULT_COMPACTION_SETTINGS = CompactionSettings(true, 16384, 20000) + +# ============================================================================ +# Compaction preparation +# ============================================================================ + +mutable struct CompactionPreparation + first_kept_entry_id::String + messages_to_summarize::Vector{AgentMessage} + turn_prefix_messages::Vector{AgentMessage} + retained_tail::Vector{AgentMessage} + is_split_turn::Bool + tokens_before::Int64 + previous_summary::Union{String, Nothing} + file_ops::Any + settings::CompactionSettings +end + +# ============================================================================ +# File operations +# ============================================================================ + +mutable struct FileOperations + read::Set{String} + written::Set{String} + edited::Set{String} +end + +# ============================================================================ +# Tree preparation +# ============================================================================ + +mutable struct TreePreparation + target_id::String + old_leaf_id::Union{String, Nothing} + common_ancestor_id::Union{String, Nothing} + entries_to_summarize::Vector{SessionTreeEntry} + user_wants_summary::Bool + custom_instructions::Union{String, Nothing} + replace_instructions::Union{Bool, Nothing} + label::Union{String, Nothing} +end + +# ============================================================================ +# Generate branch summary options +# ============================================================================ + +mutable struct GenerateBranchSummaryOptions + model::Model + api_key::String + headers::Union{Dict{String, String}, Nothing} + signal::Any + custom_instructions::Union{String, Nothing} + replace_instructions::Union{Bool, Nothing} + reserve_tokens::Int64 +end + +# ============================================================================ +# Branch summary result +# ============================================================================ + +mutable struct BranchSummaryResult + summary::String + usage::Union{Usage, Nothing} + read_files::Vector{String} + modified_files::Vector{String} +end + +# ============================================================================ +# Agent harness system prompt +# ============================================================================ + +mutable struct AgentHarnessSystemPrompt{TC<:Any, TSkill<:Skill, TPromptTemplate<:PromptTemplate, TTool<:AgentHarnessTool} + value::Union{String, Function} +end + +# ============================================================================ +# Agent harness options +# ============================================================================ + +mutable struct AgentHarnessOptions{TC<:Any, TSkill<:Skill, TPromptTemplate<:PromptTemplate, TTool<:AgentHarnessTool} + session::Session + models::Any + tools::Union{Vector{TTool}, Nothing} + resources::Union{AgentHarnessResources{TSkill, TPromptTemplate}, Nothing} + system_prompt::Union{AgentHarnessSystemPrompt{TC, TSkill, TPromptTemplate, TTool}, Nothing} + stream_options::Union{AgentHarnessStreamOptions, Nothing} + retry::Union{Any, Nothing} + model::Model + thinking_level::Union{ThinkingLevel, Nothing} + active_tool_names::Union{Vector{String}, Nothing} + steering_mode::Union{QueueMode, Nothing} + follow_up_mode::Union{QueueMode, Nothing} + tool_context::Union{AgentHarnessToolContextSource{TC}, Nothing} +end + +end diff --git a/src/interface.jl b/src/interface.jl deleted file mode 100644 index 8c1ca5c..0000000 --- a/src/interface.jl +++ /dev/null @@ -1,1400 +0,0 @@ -module interface - -export addNewMessage, conversation, decisionMaker, reflector, generatechat, - generalconversation, detectWineryName, generateSituationReport - -using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization, - DataFrames, Serde -using GeneralUtils -using ..type, ..util, ..llmfunction - -# ------------------------------------------------------------------------------------------------ # -# pythoncall setting # -# ------------------------------------------------------------------------------------------------ # -# Ref: https://github.com/JuliaPy/PythonCall.jl/issues/252 -# by setting the following variables, PythonCall.jl will use: -# 1. system's python and packages installed by system (via apt install) -# or 2. conda python and packages installed by conda -# if these setting are not set (comment out), PythonCall will use its own python and packages that -# installed by CondaPkg.jl (from env_preparation.jl) -# ENV["JULIA_CONDAPKG_BACKEND"] = "Null" # set condapkg backend = none -# systemPython = split(read(`which python`, String), "\n")[1] # system's python path -# ENV["JULIA_PYTHONCALL_EXE"] = systemPython # find python location with $> which python ex. raw"/root/conda/bin/python" - -# using PythonCall -# const py_agents = PythonCall.pynew() -# const py_llms = PythonCall.pynew() -# function __init__() -# # PythonCall.pycopy!(py_cv2, pyimport("cv2")) - -# # equivalent to from urllib.request import urlopen in python -# PythonCall.pycopy!(py_agents, pyimport("langchain.agents")) -# PythonCall.pycopy!(py_llms, pyimport("langchain.llms")) -# end - -# ---------------------------------------------- 100 --------------------------------------------- # - - -macro executeStringFunction(functionStr, args...) - # Parse the function string into an expression - func_expr = Meta.parse(functionStr) - - # Create a new function with the parsed expression - function_to_call = eval(Expr(:function, - Expr(:call, func_expr, args...), func_expr.args[2:end]...)) - - # Call the newly created function with the provided arguments - function_to_call(args...) -end - - - -""" Think and choose action - -# Arguments - - `config::T1` - config - - `state::T2` - a game state - -# Keyword Arguments - -# Return - - `thoughtdict::Dict` - -# Example -```jldoctest -julia> result = decisionMaker(agent) - -OrderedDict{String, Any} with 4 entries: - "plan" => "The user provided an image of a sparkling white wine (Asolo Prosecco Bella Principessa from Italy) and requested a search for similar wines in the inventory. According to store guidelines, I must st… - "action_name" => "SEARCH_WINE_DATABASE" - "action_input" => "Sparkling white wine from Italy" - "action_result" => "1) winery: Terrazze dell Etna, wine_name: Rose Brut. -``` -""" -function decisionMaker(a::T; recentevents::Integer=20, maxattempt=3 - ) where {T<:agent} - @info "YiemAgent decisionMaker() start " @__LINE__ - # lessonDict = copy(JSON.parsefile("lesson.json")) - - # lesson = - # if isempty(lessonDict) - # "" - # else - # lessons = Dict{String, Any}() - # for (k, v) in lessonDict - # lessons[k] = lessonDict[k][:lesson] - # end - - # """ - # You have attempted to help the user before and failed, either because your reasoning for the - # recommendation was incorrect or your response did not exactly match the user expectation. - # The following lesson(s) give a plan to avoid failing to help the user in the same way you - # did previously. Use them to improve your strategy to help the user. - - # Here are some lessons in JSON format: - # $(JSON.json(lessons)) - - # When providing the thought and action for the current trial, that into account these failed - # trajectories and make sure not to repeat the same mistakes and incorrect answers. - # """ - # end - - # recentevents_ind = GeneralUtils.recentElementsIndex( - # length(a.memory["events"]), recentevents; includelatest=true) - - requiredKeys = ["plan", "action_name", "action_input"] - context = - """ - - - """ - - # add context to text of the latest message (in the front). - # use for loop because in openai format, each msg may contain both text and image. - for d in a.chathistory[end]["content"] - if d["type"] == "text" - d["text"] = context * d["text"] - break - end - end - errornote = "N/A" - response = nothing # placeholder for show when error msg show up - - """ - { - "model": "your-model.gguf", - "messages": [ ... ], - "response_format": { - "type": "json_schema", - "json_schema": { - "name": "agent_action", - "strict": true, - "schema": { - "type": "object", - "properties": { - "think": { - "type": "string", - "description": "Your step-by-step reasoning process. Explain why you are choosing this action." - }, - "action_name": { - "type": "string", - "enum": ["search_web", "get_weather", "calculate_math"], - "description": "The exact name of the tool to execute." - }, - "action_input": { - "type": "object", - "properties": { - "query": { "type": ["string", "null"], "description": "For search_web" }, - "location": { "type": ["string", "null"], "description": "For get_weather" }, - "equation": { "type": ["string", "null"], "description": "For calculate_math" } - }, - "required": ["query", "location", "equation"], - "additionalProperties": false - } - }, - "required": ["think", "action_name", "action_input"], - "additionalProperties": false - } - } - } - } - """ - - # strict output format - response_format = Dict( - "type"=> "json_schema", - "json_schema"=> Dict( - "name"=> "user_profile", - "strict"=> true, - "schema"=> Dict( - "type"=> "object", - "properties"=> Dict( - "plan"=> Dict("type"=> "string"), - "action_name"=> Dict("type"=> "string"), - "action_input"=> Dict("type"=> "string"), - ), - "required"=> ["plan", "action_name", "action_input"], - "additionalProperties"=> false - ) - ) - ) - - msg = Dict( - "model"=> "gemma-4-E4B-it-UD-Q4_K_XL", - "messages"=> a.chathistory, - "temperature"=> 0.7, - "response_format"=> response_format, - ) - - for attempt in 1:maxattempt - response = a.context.text2textInstructLLM(a.id, msg) - response = GeneralUtils.remove_french_accents(response) - # think, response = GeneralUtils.extractthink(response) - - # dollar sign in Julia means string interpolation - while occursin('$', response) - response = replace(response, '$' => "USD") - end - - # responsedict = nothing - # try - # responsedict = Serde.parse_yaml(response) - # catch e - # println("\nERROR YiemAgent decisionMaker() Error: $e --(not qualify response)-> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - # continue - # end - - # # check whether all answer's key points are in responsedict - # println("\n---") - # println(responsedict) - # println("---\n") - # ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) - - # if !ispass - # errornote = errormsg - # println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)-> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - # continue - # end - - responsedict = JSON.parse(response) - - if responsedict["action_input"] == "CHAT_BOX" && - occursin("similar", responsedict["action_input"]) - - continue - end - - # if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"] - # errornote = "Your previous attempt didn't use the given functions" - # println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # pprintln(responsedict) - @info "YiemAgent decisionMaker() end " @__LINE__ - return responsedict - end - - # in case decisionMaker failed, force to use generatechat!() - responsedict = OrderedDict( - "plan"=> "N/A", - "action_name"=> "CHAT_BOX", - "action_input"=> "N/A" - ) - return responsedict -end - - - -""" Assigns a scalar value to each new child node to be used for selec- -tion and backpropagation. This value effectively quantifies the agent's progress in task completion, -serving as a heuristic to steer the search algorithm towards the most promising regions of the tree. - -# Arguments - - `state<:AbstractDict` - one of Yiem's agent - - `text2textInstructLLM::Function` - A function that handles communication to LLM service - -# Return - - `score::Integer` - -# Example -```jldoctest -julia> -``` - -# Signature -""" -function evaluator(a::T1, timeline, decisiondict, evaluateecontext - ) where {T1<:agent} - - systemmsg = - """ - - - You are a master sommelier of an online wine store. - - - - Under your supervision, a trainee sommelier is engaging with a store customer. Each time the customer speaks, the trainee will assess the situation, determine the next course of action, and pause to await your guidance before proceeding. - - - - Improve a trainee sommelier decision based on the store policy and guidelines while ensuring seamless interactions between the trainee and customers. - - - - trajectory: A conversation between your trainee and the customer that have occurred up until now - - evaluatee_context: The context that evaluatee use to make a decision - - evaluatee_decision: The decision made by the evaluatee, consists of the following elements: - "plan" is the trainee's plan - "action_name" is the name of the action taken, which can be one of the available tool name. - "action_input" is the input to the action. - - - - Use only infomation provided by the store policy and guidelines as a bedrocks for your response. - - - - The trainee's plan, action_name, and action_input must be logically consistent - - The trainee's action_input should be in a proper format as specified by the tools. - - The trainee's action name and action input should make sense. For example, if the trainee isn't finished talking, he shouldn't use the END_CONVER_GUIDELINE tool. - - - 1) trajectory_evaluation: Analyze the trajectory of a solution to answer the user's original question. - - Evaluate the correctness of each section and the overall trajectory based on the given question. - - Provide detailed reasoning and analysis, focusing on the latest thought, action, and observation. - - Incomplete trajectory are acceptable if the thoughts and actions up to that point are correct, even if the final answer isn't reached. - - Do not generate additional thoughts or actions. - 2) decision_evaluation: - - Examine how the trainee's decisions align with the store's policies and guidelines before proceeding. - 3) suggestion: Based store policy and guidelines, provide a suggestion for the immediate decision step only. - 4) approval: Can be "yes" or "no". "no" if the suggestion contradict the trainee's decision; otherwise, it is "yes". - - - - { - "trajectory_evaluation": "...", - "decision_evaluation": "...", - "suggestion": "...", - "approval": "...", - } - - - Let's begin! - """ - requiredKeys = [:trajectory_evaluation, :decision_evaluation, :approval, :suggestion] - errornote = "N/A" - - for attempt in 1:10 - evaluateecontext = replace(evaluateecontext, "" => "") - evaluateecontext = replace(evaluateecontext, "" => "") - - context = - """ - - - $timeline - - - $evaluateecontext - - - {plan: $(decisiondict["plan"]), action_name: $(decisiondict["action_name"]), action_input: $(decisiondict["action_input"])} - - P.S. $errornote - - """ - - unformatPrompt = - [ - Dict("name" => "system", "text" => systemmsg), - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(unformatPrompt, a.llmFormatName) - # add info - prompt = prompt * context - - response = a.context.text2textInstructLLM(prompt; senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) - response = GeneralUtils.remove_french_accents(response) - # response = replace(response, '$'=>"USD") - think, response = GeneralUtils.extractthink(response) - - responsedict = nothing - try - responsedict = copy(JSON.parsefile(response)) - catch - println("\nERROR YiemAgent generatechat() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # check whether all answer's key points are in responsedict - ispass, errormsg = checkAgentResponse_JSON(responsedict, requiredKeys) - if !ispass - errornote = errormsg - println("\nERROR YiemAgent evaluator() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - continue - end - - # if accepted_as_answer ∉ ["yes", "no"] # [PENDING] add errornote into the prompt - # error("generated accepted_as_answer has wrong format") - # end - - println("\nEvaluator() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - pprintln(Dict(responsedict)) - return responsedict - end - error("Evaluator failed to generate an evaluation, Response: \n$response\n<|End of error|>") -end - -""" Chat with llm. - -# Example userinput - -image_path = "test/large_image.png" -image_bytes = read(image_path) -base64_string = base64encode(image_bytes) - -# 2. Match the MIME type according to your file extension (e.g., png, jpeg) -mime_type = "image/png" -data1_uri = "data:;base64," - -# 3. Construct payload with the Data URI -message => Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => "Describe this image for me"), - Dict( - "type" => "image_url", - "image_url" => Dict("url" => data_uri) - ) - ] - ) - -""" -function conversation(a::sommelier; userinput::Union{Dict{String, Any}, JSON.Object{String, Any}}, - maximumMsg=50, max_think_loop::Integer=3) - - @info "YiemAgent conversation() start " @__LINE__ - userinput = GeneralUtils.dictify(userinput; keytype=String, sort_order=["text"]) - - # find text in usermsg - usertext = nothing - for (i, d) in enumerate(userinput["content"]) - if d["type"] == "text" - d["text"] = GeneralUtils.remove_french_accents(d["text"]) - usertext = d["text"] - end - end - - if usertext == "newtopic" - clearhistory(a) - return "Okay. What shall we talk about?" - else - - # add usermsg to a.chathistory but how do I handle images? - addNewMessage(a, "user", userinput; maximumMsg=maximumMsg) - - # thinking loop until AI wants to communicate with the user - loopcount = 0 - while true - loopcount += 1 - if loopcount > max_think_loop - - thoughtdict, result_raw = generatechat!(a) - assistant_response = Dict{String, Any}( - "role" => "assistant", - "content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),] - ) - addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg) - - items_info = [] - send_item_ind = [] # index of the item being send to frontend - if haskey(a.memory["shortmem"], "items_info") - for (i, item) in enumerate(a.memory["shortmem"]["items_info"]) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)), item name: $(item["wine_name"]) " @__LINE__ - if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"]) - push!(items_info, deepcopy(item)) - push!(send_item_ind, i) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ - end - end - # remove sent items - deleteat!(a.memory["shortmem"]["items_info"], send_item_ind) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ - end - - response_to_frontend = Dict{String, Any}( - "role" => "assistant", - "content" => [ - Dict("type" => "text", "text" => thoughtdict["action_input"]), - Dict( - "type" => "items_info", - "items_info" => items_info - ), - ] - ) - - return response_to_frontend - end - - - thoughtdict, result_raw = think(a) - - if thoughtdict["action_name"] ∈ ["CHAT_BOX"] - assistant_response = Dict{String, Any}( - "role" => "assistant", - "content" => [Dict("type" => "text", "text" => thoughtdict["action_input"]),] - ) - addNewMessage(a, "assistant", assistant_response; maximumMsg=maximumMsg) - - items_info = [] - send_item_ind = [] # index of the item being send to frontend - if haskey(a.memory["shortmem"], "items_info") - for (i, item) in enumerate(a.memory["shortmem"]["items_info"]) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)), item name: $(item["wine_name"]) " @__LINE__ - if haskey(item, "wine_name") && occursin(item["wine_name"], thoughtdict["action_input"]) - push!(items_info, deepcopy(item)) - push!(send_item_ind, i) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ - end - end - # remove sent items - deleteat!(a.memory["shortmem"]["items_info"], send_item_ind) - @info "YiemAgent conversation() shortmem: $(length(a.memory["shortmem"]["items_info"])), items_info: $(length(items_info)) " @__LINE__ - end - - response_to_frontend = Dict{String, Any}( - "role" => "assistant", - "content" => [ - Dict("type" => "text", "text" => thoughtdict["action_input"]), - Dict( - "type" => "items_info", - "items_info" => items_info - ), - ] - ) - - """ intended message to send to frontend should have the following format. - response_to_frontend = Dict{String, Any}( - "role" => "assistant", - "content" => [ - Dict("type" => "text", "text" => "assistant_text_response"), - Dict( - "type" => "items_info", - "items_info" => [ - Dict( - "wine_name"=> "wine name 1", - "wine_id"=> "...", - "image"=> base64 encoded image, - ... - ), - Dict( - "wine_name"=> "wine name 2", - "wine_id"=> "...", - "image"=> base64 encoded image, - ... - ), - ] - ), - ] - ) - """ - - - return response_to_frontend - else # still in action - - action_name = thoughtdict["action_name"] - action_input = thoughtdict["action_input"] - - action_call = Dict{String, Any}( - "role" => "action_call", - "content" => [Dict("type" => "text", "text" => "{action_name: $action_name, action_input: $action_input}"),] - ) - - addNewMessage(a, "action_call", action_call; maximumMsg=maximumMsg) - - action_result = thoughtdict["action_result"] - actionresult = Dict{String, Any}( - "role" => "action_result", - "content" => [Dict("type" => "text", "text" => "$action_result"),] - ) - - addNewMessage(a, "actionresult", actionresult; maximumMsg=maximumMsg) - @info "YiemAgent conversation() end think count $loopcount " @__LINE__ - end - end - end -end - - -""" -# Arguments - -# Return - -# Example -```jldoctest -julia> -``` - -""" -function think(a::T)::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - # a.memory[:recap] = generateSituationReport(a, a.context["text"2textInstructLLM]; skiprecent=0) - @info "YiemAgent think() start " @__LINE__ - thoughtdict = decisionMaker(a) - @info "YiemAgent think() 1 " @__LINE__ - @show thoughtdict - println("---\n") - - result_raw = nothing - if thoughtdict["action_name"] ∈ ["CHAT_BOX"] - - # sometime CHAT_BOX input is too short. - # if thoughtdict["action_input] < 20 character, use generatechat!() - if length(thoughtdict["action_input"]) < 20 - thoughtdict, result_raw = generatechat!(a) - else - thoughtdict["action_result"] = "Action result is the next user dialogue." - result_raw = thoughtdict["action_input"] - end - - elseif thoughtdict["action_name"] == "END_CONVER_GUIDELINE" - - thoughtdict, result_raw = end_conversation_guideline!(a, thoughtdict) - - elseif thoughtdict["action_name"] ∈ ["WINE_PRESENTATION_GUIDELINE"] - - thoughtdict, result_raw = wine_presentation_guideline!(a, thoughtdict) - - elseif thoughtdict["action_name"] == "SEARCH_WINE_DATABASE" - - thoughtdict, result_raw = search_wine_database!(a, thoughtdict; useSQLLLM=false) - if result_raw !== nothing && result_raw isa Vector - if haskey(a.memory["shortmem"], "items_info") - append!(a.memory["shortmem"]["items_info"], result_raw) - else - a.memory["shortmem"]["items_info"] = result_raw - end - end - - else - - error("condition is not defined ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - @info "YiemAgent think() end " @__LINE__ - # @show thoughtdict - println("---\n") - return (thoughtdict=thoughtdict, result_raw=result_raw) -end - -function chatbox!(a::T, thoughtdict::AbstractDict - )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - thoughtdict["action_result"] = "Action result is the next user dialogue." - return (thoughtdict=thoughtdict, result_raw=nothing) -end - -function end_conversation_guideline!(a::T, thoughtdict::AbstractDict - )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - - guideline = - """ - - - Provide customer with store contact info and business hours - - Invite customer to comeback - - Business Hours: everyday 9.00-20.00 - Tel. 0863055790 - - - """ - thoughtdict["action_result"] = guideline - - return (thoughtdict=thoughtdict, result_raw=nothing) -end - -function wine_presentation_guideline!(a::T, thoughtdict::AbstractDict - )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - - guideline = - """ - - - Provide detailed introductions of the wines you've found to the user. - - Explain how the wine could match the user's intention and what its effects might mean for the user's experience. - - If multiple wines are available, highlight their differences and provide a comprehensive comparison of how each option aligns with the user's intention and what the potential effects of each option could mean for the user's experience. - - Provide your personal recommendation and provide a brief explanation of why you recommend it. - - People don't describe wine quality level in numbers so use convertion_table if neccessary - - Intensity level: - 1 to 2: May correspond to "light-bodied" or a similar description. - 2 to 3: May correspond to "med light bodied", "medium light" or a similar description. - 3 to 4: May correspond to "medium bodied" or a similar description. - 4 to 5: May correspond to "med full bodied", "medium full" or a similar description. - 4 to 5: May correspond to "full bodied" or a similar description. - Sweetness level: - 1 to 2: May correspond to "dry", "no sweet" or a similar description. - 2 to 3: May correspond to "off dry", "less sweet" or a similar description. - 3 to 4: May correspond to "semi sweet" or a similar description. - 4 to 5: May correspond to "sweet" or a similar description. - 4 to 5: May correspond to "very sweet" or a similar description. - Tannin level: - 1 to 2: May correspond to "low tannin" or a similar description. - 2 to 3: May correspond to "semi low tannin" or a similar description. - 3 to 4: May correspond to "medium tannin" or a similar description. - 4 to 5: May correspond to "semi high tannin" or a similar description. - 4 to 5: May correspond to "high tannin" or a similar description. - Acidity level: - 1 to 2: May correspond to "low acidity" or a similar description. - 2 to 3: May correspond to "semi low acidity" or a similar description. - 3 to 4: May correspond to "medium acidity" or a similar description. - 4 to 5: May correspond to "semi high acidity" or a similar description. - 4 to 5: May correspond to "high acidity" or a similar description. - - - """ - thoughtdict["action_result"] = guideline - - return (thoughtdict=thoughtdict, result_raw=nothing) -end - - -#PENDING -function generatechat!(a::T; maxattempt::Integer=10 - )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - @info "YiemAgent generatechat!() start " @__LINE__ - # lessonDict = copy(JSON.parsefile("lesson.json")) - - # lesson = - # if isempty(lessonDict) - # "" - # else - # lessons = Dict{String, Any}() - # for (k, v) in lessonDict - # lessons[k] = lessonDict[k][:lesson] - # end - - # """ - # You have attempted to help the user before and failed, either because your reasoning for the - # recommendation was incorrect or your response did not exactly match the user expectation. - # The following lesson(s) give a plan to avoid failing to help the user in the same way you - # did previously. Use them to improve your strategy to help the user. - - # Here are some lessons in JSON format: - # $(JSON.json(lessons)) - - # When providing the thought and action for the current trial, that into account these failed - # trajectories and make sure not to repeat the same mistakes and incorrect answers. - # """ - # end - - # recentevents_ind = GeneralUtils.recentElementsIndex( - # length(a.memory["events"]), recentevents; includelatest=true) - - systemmsg = - """ - # store_policy - - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - - If you found wines in the store's database, they are in stock. - - You can only recommend wines that are currently in our inventory - - Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences. - - Ask the user one question at a time. - - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. - - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. - - Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future. - - Spicy foods should be paired only with light red wines. - - We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user immediately if they are looking for these types of wines. Do not sell our wines as such. - - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. - - # store_guidelines - - Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting. - - Customer may provide images for you to look up. - - Encourage the customer to explore different options and try new things. - - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - - Your store carries only wine. - - Vintage 0 means non-vintage. - - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. - - # situation - You are continuing the conversation with the user. - - # your role - Your name is $(a.name). You are a helpful sommelier for website-based $(a.retailername)'s wine store. You are working under your mentor supervision. - - # objective - - Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences. - - Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences. - - # your responsibility includes - - According to the store's policy and guidelines, continuing conversation with the customer using CHAT_BOX action. - - Keep the conversation with the customer going smoothly - - # your responsibility does NOT includes - - Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. - - Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. - - Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - - # you should then respond to the user with interleaving plan, action_name, action_input in JSON format - 1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. - 2) "action_name", action_name must be CHAT_BOX. - 3) "action_input", Dialogue you want to chat with the user according to your plan. - After the action is executed you gets "action_result". It is the output from the action you selected. - - # available actions - "CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to chat with the user according to your plan. - """ - - system_msg = Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => systemmsg), - ] - ) - - chathistory = deepcopy(a.chathistory[2:end]) # use deep copy because I want to replace system msg - pushfirst!(chathistory, system_msg) - - requiredKeys = ["plan", "action_name", "action_input"] - - errornote = "N/A" - response = nothing # placeholder for show when error msg show up - - for attempt in 1:maxattempt - if attempt > 1 - println("\nYiemAgent generatechat() attempt $attempt/$maxattempt ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - response_format = Dict( - "type"=> "json_schema", - "json_schema"=> Dict( - "name"=> "user_profile", - "strict"=> true, - "schema"=> Dict( - "type"=> "object", - "properties"=> Dict( - "plan"=> Dict( - "type"=> "string", - "description" => "Based on the current situation, state a complete action plan to complete the task and rationale. Be specific.", - ), - "action_name"=> Dict( - "type"=> "string", - "description" => "action_name must be CHAT_BOX", - ), - "action_input"=> Dict( - "type"=> "string", - "description" => "Dialogue you want to chat with the user according to your plan.", - ), - ), - "required"=> ["plan", "action_name", "action_input"], - "additionalProperties"=> false - ) - ) - ) - - msg = Dict( - "model" => "gemma-4-E4B-it-UD-Q4_K_XL", - "messages" => chathistory, - "temperature" => 0.7, - "response_format"=> response_format, - ) - - response = a.context.text2textInstructLLM(a.id, msg) - response = GeneralUtils.clean_json_response(response) - response = GeneralUtils.remove_french_accents(response) - think, response = GeneralUtils.extractthink(response) - - response = strip(response) - - responsedict = nothing - if occursin(requiredKeys[2], response) - try - _responsedict = JSON.parse(response) - responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys) - catch - println("\nERROR YiemAgent generatechat() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - else - println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - continue - end - - - # check whether all answer's key points are in responsedict - ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) - if !ispass - errornote = errormsg - println("\nERROR YiemAgent generatechat() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - continue - end - - # if responsedict["action_name"] ∉ ["CHAT_BOX", "SEARCH_WINE_DATABASE", "PRESENT_WINE_GUIDELINE", "END_CONVER_GUIDELINE"] - # errornote = "Your previous attempt didn't use the given functions" - # println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)--> $(responsedict["action_name"])", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # continue - # end - - # println("\nYiem decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # pprintln(responsedict) - responsedict["action_result"] = "Action result is the next user dialogue." - @info "YiemAgent generatechat!() end " @__LINE__ - return (thoughtdict=responsedict, result_raw=responsedict["action_input"]) - end - @info "YiemAgent generatechat() failed to generate a thought " @__LINE__ - error("YiemAgent generatechat() failed to generate a thought ", response) -end - - -function generatequestion(a, text2textInstructLLM::Function, timeline)::String - systemmsg = - """ - Your role: - Your name is $(a.name). You are a helpful English-speaking, website-based sommelier for $(a.retailername)'s online store currently talking with the user. - Your goal includes: - 1) Help the user select the best wines from your inventory that align with the user's preferences - 2) Thanks the user when they don't need any further assistance and invite them to comeback next time - - Your responsibility includes: - 1) From your point of view as a sommelier helping the user, ask yourself multiple questions based on the current situation - - Your responsibility does NOT includes: - 1) Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. - 2) Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. - 3) Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - - At each round of conversation, you will be given the info: - Additional info: ... - Your recent events: latest 5 events of the situation - - You must follow the following guidelines: - - Your question should be specific, self-contained and not require any additional context. - - Once the user has chose their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. - - You should follow the following guidelines: - - Focus on the latest conversation - - If the user interrupts, prioritize the user - - If you don't already know, find out the user's budget - - If you don't already know, find out the type of wine the user is looking for, such as red, white, sparkling, rose, dessert, fortified - - If you don't already know, find out the occasion for which the user is buying wine - - If you don't already know, find out the characteristics of wine the user is looking for, such as tannin, sweetness, intensity, acidity - - If you don't already know, find out what food will be served with wine - - If you haven't already, introduce the wines you found in the database to the user first - - Generally speaking, your inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - - All wines in your inventory are always in stock. - - Engage in conversation to indirectly investigate the customer's intention, budget and preferences before checking your inventory. - - Do not ask the user about wine's flavor e.g. floral, citrusy, nutty or some thing similar as these terms cannot be used to search the database. - - Once the user has selected their wine, ask the user if they need any further assistance. Do not offer any additional services. If the user doesn't need any further assistance, say goodbye and invite them to come back next time. - - Medium and full-bodied red wines are bad with spicy foods. - - If a customer requests information about discounts, quantity, rewards programs, promotions, delivery options, boxes, gift wrapping, packaging, or personalized messages, please inform them that they can contact our sales team at the store. - - You should then respond to the user with: - 1) Thought: State your thought about the current situation - 2) Q: "Ask yourself" at least three, but no more than five, questions about the situation from your perspective. - 3) A: Given the situation, "answer to yourself" the best you can. Do not generate any extra text after you finish answering all questions - - You must only respond in format as described below: - Q1: ... - A1: ... - Q2: ... - A2: ... - ... - - Here are some examples: - Q: What the user is looking for? - A: The user is asking for a MPV car with 7-seat - Q: What do I know? - A: The user is looking for a car with 7-seat. Our dealer sell these kind of cars - Q: What brands the user prefer? - A: I don't know. The user didn't mentioned that. Let's find out. - Q: What else do I need to know before proceeding? - A: I don't know about the user budget, car's color, and other user's preferences yet. Let's find out more about the user's preferences. - Q: I'm still lacking information regarding the user's preferences for the powertrain. I've asked the user twice already, but perhaps they're not familiar with this. What should I do. - A: I'll proceed without asking the user about the powertrain. - Q: The user is buying for her husband, should I dig in to get more information? - A: Yes, I should. So that I have better idea about the user's preferences. - Q: Why the user saying this? - A: The user does not want an SUV because it does not have sliding doors - Q: The user is asking for a cappuccino. Do I have it at my cafe? - A: No I don't have. - Q: Since I don't have a cappuccino but I have a Late, should I ask if they are okay with that? - A: Yes, I should. - Q: Are they allergic to milk? - A: Since they mentioned a cappuccino before, it seems they are not allergic to milk. - Q: Have I checked the inventory yet? - A: No. I need more information from the user including ... - Q: What else do I need to know? - A: ... - Q: Should I present my item to the user? - A: Not yet, I will need to check my inventory first. - Q: Should I check our inventory now? - A: ... - Q: What the user intend to do with the car? - A: I don't know yet. Let's ask the user. - Q: What do I have in our inventory? - A: ... - Q: Which items are within the user price range? And which items are out of the user price rance? - A: ... - Q: Do I have what the user is looking for in our stock? - A: ... - Q: Am I certain about the information I'm going to share with the user, or should I verify the information first? - A: ... - Q: What should I do? - A: ... - Q: What shouldn't I do? - A: ... - Q: what kind of car suitable for off-road trip? - A: A four-wheel drive SUV is a good choice for off-road trips. - Q: What car specification would satisfy the user's needs? - A: The user is seeking an eco-friendly vehicle that accommodates seven passengers, including seniors and children, with prioritized accessibility and efficient refueling. While electric vehicles (EVs) offer eco-friendly benefits, their long charging times make hybrid models more practical for fast refueling. Additionally, a lower ground level is essential for ease of entry/exit for seniors and children. A hybrid multi-purpose vehicle (MPV) emerges as the optimal solution, balancing sustainability, seating capacity, accessibility, and refueling efficiency. - - Let's begin! - """ - - header = ["Q1:"] - dictkey = ["q1"] - - # context = - # if length(a.memory["shortmem"]["available_wine"]) != 0 - # "Available wines you've found in your inventory so far: $(availableWineToText(a.memory["shortmem"]["available_wine"]))" - # else - # "N/A" - # end - database_search_result = a.memory["shortmem"]["db_search_result"] - - # recent_ind = GeneralUtils.recentElementsIndex(length(a.memory[:events]), recent) - # recentevents = a.memory[:events][recent_ind] - # timeline = createTimeline(recentevents; eventindex=recent_ind) - errornote = "N/A" - response = nothing # store for show when error msg show up - - # recap = - # if length(a.memory[:recap]) <= recent - # "N/A" - # else - # recapkeys = keys(a.memory[:recap]) - # recapkeys_vec = [i for i in recapkeys] - # recapkeys_vec = recapkeys_vec[1:end-recent] - # tempmem = OrderedDict() - # for (k, v) in a.memory[:recap] - # if k ∈ recapkeys_vec - # tempmem[k] = v - # end - # end - - # GeneralUtils.dictToString(tempmem) - # end - - llmkwargs=Dict( - :num_ctx => 32768, - :temperature => 0.5, - ) - - for attempt in 1:10 - if attempt > 1 - println("\nYiemAgent generatequestion() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - usermsg = - """ - Additional info: $database_search_result - Your recent events: $timeline - P.S. $errornote - """ - - _prompt = - [ - Dict("name" => "system", "text" => systemmsg), - Dict("name" => "user", "text" => usermsg) - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) - - response = text2textInstructLLM(prompt; - modelsize="medium", llmkwargs=llmkwargs, senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) - think, response = GeneralUtils.extractthink(response) - - # make sure generatequestion() don't have wine name that is not from retailer inventory - # check whether an agent recommend wines before checking inventory or recommend wines - # outside its inventory - # ask LLM whether there are any winery mentioned in the response - mentioned_winery = detectWineryName(a, response) - if mentioned_winery != "None" - mentioned_winery = String.(strip.(split(mentioned_winery, ","))) - - # check whether the wine is in event - isWineInEvent = false - for winename in mentioned_winery - for event in a.memory["events"] - if event["observation"] !== nothing && occursin(winename, event["observation"]) - isWineInEvent = true - break - end - end - end - - # if wine is mentioned but not in timeline or shortmem, - # then the agent is not supposed to recommend the wine - if isWineInEvent == false - errornote = "Your previous attempt mentioned wines that are not in your inventory which is not allowed." - println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - end - - q_number = count("Q", response) - - # check for valid response - if q_number < 1 - errornote = "Your previous attempt has too few questions." - println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - # check whether "A1" is in the response, if not error. - elseif !occursin("A1:", response) - errornote = "Your previous attempt does not have A1:" - println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # check whether response has all header - detected_kw = GeneralUtils.detectKeywordVariation(header, response) - if 0 ∈ values(detected_kw) - errornote = "\nYour previous attempt did not have all points according to the required response format" - println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "\nYour previous attempt has duplicated points according to the required response format" - println("\nERROR YiemAgent generatequestion() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) - response = "Q1: " * responsedict["q1"] - println("\nYiemAgent generatequestion() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - try pprintln(response) catch e println(response) end - - return response - end - error("YiemAgent generatequestion() failed to generate a response ", response) -end - - -function generateSituationReport(a, text2textInstructLLM::Function; skiprecent::Integer=0 - )::OrderedDict - - systemmsg = - """ - You are an assistant being in the given events. - Your task is to writes a summary for each event seperately into an ongoing, interleaving series. - - At each round of conversation, you will be given the situation: - Total events: number of events you need to summarize. - Events timeline: ... - Context: ... - - You should follow the following guidelines: - - Use the word "user" and "assistant" instead of their name in the report - - You should then respond to the user with the following: - Event: a detailed summary for each event without exaggerated details. - - You must only respond in format as described below: - Event_1: ... - Event_2: ... - ... - - Here are some examples: - Event_1: The user ask me about where to buy a toy. - Event_2: I told the user to go to the store at 2nd floor. - - Event_1: The user greets the assistant by saying 'hello'. - Event_2: The assistant respond warmly and inquire about how he can assist the user. - - Let's begin! - """ - - header = ["Event_$i:" for i in eachindex(a.memory["events"])] - dictkey = lowercase.(["Event_$i" for i in eachindex(a.memory["events"])]) - - ind = GeneralUtils.nonRecentElementsIndex(length(a.memory["events"]), skiprecent) - events = a.memory["events"][ind] - timeline = createTimeline(events) - - errornote = "N/A" - response = nothing # store for show when error msg show up - for attempt in 1:10 - if attempt > 1 # use to prevent LLM generate the same respond over and over - println("\nYiemAgent generateSituationReport() attempt $attempt/10 ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - end - - usermsg = """ - Total events: $(length(events)) - Events timeline: $timeline - P.S. $errornote - """ - - _prompt = - [ - Dict("name" => "system", "text" => systemmsg), - Dict("name" => "user", "text" => usermsg) - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, "qwen3") - - response = text2textInstructLLM(prompt; senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, "qwen3") - - # check whether response has all header - detected_kw = GeneralUtils.detectKeywordVariation(header, response) - kwvalue = [i for i in values(detected_kw)] - zeroind = findall(x -> x == 0, kwvalue) - missingkeys = [header[i] for i in zeroind] - if 0 ∈ values(detected_kw) - errornote = "$missingkeys are missing in your previous attempt" - println("\nERROR YiemAgent generateSituationReport() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum(values(detected_kw)) > length(header) - errornote = "Your previous response has duplicated events" - println("\nERROR YiemAgent generateSituationReport() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) - - println("\ngenerateSituationReport() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - try pprintln(response) catch e println(response) end - - return responsedict - end - error("generateSituationReport failed to generate a response ", response) -end - - -function detectWineryName(a, text) - systemmsg = - """ - You are a sommelier of a wine store. - Your task is to identify and list any winery names mentioned in the provided text. - - At each round of conversation, you will be given the situation: - Text: a text describing the situation. - - Tips: - - Winery usually contains Château, Chateau, Domaine, Côte, Cotes, St. de, or a combination of these words. - - You should then respond to the user with: - Winery_names: A list of winery names mentioned in the text or "None" if no winery name is mentioned. - - You must only respond in format as described below: - Winery_names: ... - - Here are some examples: - Winery_names: Domaine Courbis, Chateau Lafite Rothschild, Matarromera Domaine Roulot, Château, Cotes - - Let's begin! - """ - - header = ["Winery_names:"] - dictkey = ["winery_names"] - - response = nothing # placeholder for show when error msg show up - - for attempt in 1:10 - usermsg = """ - Text: $text - """ - _prompt = - [ - Dict("name" => "system", "text" => systemmsg), - Dict("name" => "user", "text" => usermsg) - ] - - # put in model format - prompt = GeneralUtils.formatLLMtext(_prompt, a.llmFormatName) - - response = a.context.text2textInstructLLM(prompt; senderId=a.id) - response = GeneralUtils.deFormatLLMtext(response, a.llmFormatName) - think, response = GeneralUtils.extractthink(response) - println("\ndetectWineryName() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - try pprintln(response) catch e println(response) end - - # check whether response has all header - detected_kw = GeneralUtils.detectKeywordVariation(header, response) - missingkeys = [k for (k, v) in detected_kw if v === nothing] - - if !isempty(missingkeys) - errornote = "$missingkeys are missing from your previous response" - println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - elseif sum([length(i) for i in values(detected_kw)]) > length(header) - errornote = "\nYour previous attempt has duplicated points according to the required response format" - println("\nERROR YiemAgent rolegenerator() $errornote ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - responsedict = GeneralUtils.textToDict(response, header; - dictKey=dictkey, symbolkey=true) - - result = responsedict["winery_names"] - - return result - end - error("detectWineryName failed to generate a response") - end - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -end # module interface - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/llmfunction.jl b/src/llmfunction.jl deleted file mode 100644 index 55ef96f..0000000 --- a/src/llmfunction.jl +++ /dev/null @@ -1,1647 +0,0 @@ -module llmfunction - -export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recommendbox, - virtualWineUserRecommendbox, userChatbox, userRecommendbox, extractWineAttributes_1, - extractWineAttributes_2, paraphrase, SQLexecution - -using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures, - Base64, Serde, LibPQ, NATS -using GeneralUtils, SQLLLM -using ..type, ..util - -# ---------------------------------------------- 100 --------------------------------------------- # - - -""" Chatbox for chatting with virtual wine customer. - -# Arguments - - `a::T1` - one of Yiem's agent - - `input::T2` - text to be send to virtual wine customer - -# Return - - `response::String` - response of virtual wine customer -# Example -```jldoctest -julia> -``` - -# TODO - - [] update docstring - - [] add reccommend() to compare wine - -# Signature -""" -function virtualWineUserRecommendbox(a::T1, input - )::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:agent} - - # put in model format - virtualWineCustomer = a.config["externalservice"]["virtualWineCustomer_1"] - llminfo = virtualWineCustomer["llminfo"] - prompt = - if llminfo["name"] == "llama3instruct" - formatLLMtext_llama3instruct("assistant", input) - else - error("llm model name is not defied yet $(@__LINE__)") - end - - # send formatted input to user using GeneralUtils.sendReceiveMqttMsg - msgMeta = GeneralUtils.generate_msgMeta( - virtualWineCustomer["mqtttopic"], - senderName= "virtualWineUserRecommendbox", - senderId= a.id, - receiverName= "virtualWineCustomer", - mqttBroker= a.config["mqttServerInfo"]["broker"], - mqttBrokerPort= a.config["mqttServerInfo"]["port"], - msgId = "dummyid" #CHANGE remove after testing finished - ) - - outgoingMsg = Dict( - "msgMeta"=> msgMeta, - "payload"=> Dict( - "text"=> prompt, - ) - ) - - result = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120) - response = result["response"] - - return (response["text"], response["select"], response["reward"], response["isterminal"]) -end - - - -""" Chatbox for chatting with virtual wine customer. - -# Arguments - - `a::T1` - one of Yiem's agent - - `input::T2` - text to be send to virtual wine customer - -# Return - - `response::String` - response of virtual wine customer -# Example -```jldoctest -julia> -``` - -# TODO - - [] update docs - - [x] write a prompt for virtual customer - -# Signature -""" -function virtualWineUserChatbox(config::T1, input::T2, virtualCustomerChatHistory - )::Union{Tuple{String, Number, Number, Bool}, Tuple{String, Nothing, Number, Bool}} where {T1<:AbstractDict, T2<:AbstractString} - - previouswines = - """ - You have the following wines previously: - - """ - - systemmsg = - """ - You find yourself in a well-stocked wine store, engaged in a conversation with the store's knowledgeable sommelier. - You're on a quest to find a bottle of wine that aligns with your specific preferences and requirements. - - The ideal wine you're seeking should meet the following criteria: - 1. It should fit within your budget. - 2. It should be suitable for the occasion you're planning. - 3. It should pair well with the food you intend to serve. - 4. It should be of a particular type of wine you prefer. - 5. It should possess certain characteristics, including: - - The level of sweetness. - - The intensity of its flavor. - - The amount of tannin it contains. - - Its acidity level. - - Here's the criteria details: - { - "budget": 50, - "occasion": "graduation ceremony", - "food pairing": "Thai food", - "type of wine": "red", - "wine sweetness level": "dry", - "wine intensity level": "full-bodied", - "wine tannin level": "low", - "wine acidity level": "medium", - } - - You should only respond with "text", "select", "reward", "isterminal" steps. - "text" is your conversation. - "select" is an integer. Choose an option when presented with choices, or leave it null if none of the options satisfy you or if no choices are available. - "reward" is an integer, it can be three number: - 1) 1 if you find the right wine. - 2) 0 if you don’t find the ideal wine. - 3) -1 if you’re dissatisfied with the sommelier’s response. - "isterminal" can be false if you still want to talk with the sommelier, true otherwise. - - You should only respond in JSON format as describe below: - { - "text": "your conversation", - "select": null, - "reward": 0, - "isterminal": false - } - - Here are some examples: - - sommelier: "What's your budget? - you: - { - "text": "My budget is 30 USD.", - "select": null, - "reward": 0, - "isterminal": false - } - - sommelier: "The first option is Zena Crown and the second one is Buano Red." - you: - { - "text": "I like the 2nd option.", - "select": 2, - "reward": 1, - "isterminal": true - } - - Let's begin! - """ - -pushfirst!(virtualCustomerChatHistory, Dict("name"=> "system", "text"=> systemmsg)) - - # replace the :user key in chathistory to allow the virtual wine customer AI roleplay - chathistory::Vector{Dict{String, Any}} = Vector{Dict{String, Any}}() - for i in virtualCustomerChatHistory - newdict = Dict() - newdict["name"] = - if i["name"] == "user" - "you" - elseif i["name"] == "assistant" - "sommelier" - else - i["name"] - end - - newdict["text"] = i["text"] - push!(chathistory, newdict) - end - - push!(chathistory, Dict("name"=> "assistant", "text"=> input)) - - # put in model format - prompt = formatLLMtext(chathistory, "llama3instruct") - prompt *= - """ - <|start_header_id|>you<|end_header_id|> - {"text" - """ - - pprint(prompt) - externalService = config["externalservice"]["text2textinstruct"] - - # send formatted input to user using GeneralUtils.sendReceiveMqttMsg - msgMeta = GeneralUtils.generate_msgMeta( - externalService["mqtttopic"], - senderName= "virtualWineUserChatbox", - senderId= string(uuid4()), - receiverName= "text2textinstruct", - mqttBroker= config["mqttServerInfo"]["broker"], - mqttBrokerPort= config["mqttServerInfo"]["port"], - msgId = string(uuid4()) # remove after testing finished - ) - - outgoingMsg = Dict( - "msgMeta"=> msgMeta, - "payload"=> Dict( - "text"=> prompt, - ) - ) - - attempt = 0 - for attempt in 1:5 - try - response = GeneralUtils.sendReceiveMqttMsg(outgoingMsg; timeout=120) - _responseJsonStr = response["response"]["text"] - expectedJsonExample = - """ - Here is an expected JSON format: - { - "text": "...", - "select": "...", - "reward": "...", - "isterminal": "..." - } - """ - responseJsonStr = jsoncorrection(config, _responseJsonStr, expectedJsonExample) - responseDict = copy(JSON.parsefile(responseJsonStr)) - - text::AbstractString = responseDict["text"] - select::Union{Nothing, Number} = responseDict["select"] == "null" ? nothing : responseDict["select"] - reward::Number = responseDict["reward"] - isterminal::Bool = responseDict["isterminal"] - - if text != "" - # pass test - else - error("virtual customer not answer correctly") - end - - return (text, select, reward, isterminal) - catch e - io = IOBuffer() - showerror(io, e) - errorMsg = String(take!(io)) - st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println("") - @warn "Error occurred: $errorMsg\n$st" - println("") - end - end - error("virtualWineUserChatbox failed to get a response") -end - -""" Search wine in stock. - -# Arguments - - `a::T1` - one of ChatAgent's agent. - - `thoughtdict::AbstractDict` -# Return - A JSON string of available wine - -# Example -```jldoctest -julia> using ChatAgent -julia> agent = YiemAgent.sommelier(...) -julia> thoughtdict = - OrderedDict{String, Any}( - "plan" => "The user is asking a very specific question about a wine (Brunello di Montalcino from Tenuta CastelGiocondo). Although the policy suggests gathering budget, wine type, and occasion, the user has provided enough specific information (name, region, producer) to attempt a direct search in the database. I will use the SEARCH_WINE_DATABASE action to check if this specific wine is in our inventory.", - "action_name" => "SEARCH_WINE_DATABASE", - "action_input" => "Brunello di Montalcino from Tenuta CastelGiocondo") -``` -""" -function search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=false - )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - - println("\ncheckinventory order: $(thoughtdict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - if useSQLLLM - # add suppport for similarSQLVectorDB - textresult, result_raw = SQLLLM.query( - inventoryquery, - a.context.executeSQL, - a.context.text2textInstructLLM; - insertSQLVectorDB=a.context.insertSQLVectorDB, - similarSQLVectorDB=a.context.similarSQLVectorDB, - llmFormatName="qwen3") - thoughtdict["action_result"] = textresult - else - - # direct query with possible sql instead of SQLLLM. - hard_conditions, vector_search = wine_search_term_classification(a, thoughtdict["action_input"]) - - # do hard filter - # sql = generatesql(a, inventoryquery) - sql = predefined_wine_search_sql(hard_conditions) - @info "\nsql: $sql, \nvector_search: $vector_search" - textresult, sql_result_df, success, _ = SQLexecution(a.context.executeSQL, sql) - - # do vector search - vector_search_str = "" - for i in vector_search - vector_search_str = vector_search_str * " " * i["value"] - end - vector_search_str = String(strip(vector_search_str)) - vector_search_str = GeneralUtils.removestring(vector_search_str, ["%"]) - @show vector_search - @show vector_search_str - - - config = a.context.agentconfig - host_url, _port = split(config["externalservice"]["sommpanion_db"]["url"], ':') - port = parse(Int, _port) - dbname = "winedb" - user = config["externalservice"]["sommpanion_db"]["user"] - password = config["externalservice"]["sommpanion_db"]["password"] - pg_conn_str = "host=$host_url port=$port dbname=$dbname user=$user password=$password" - - #WORKING - # df = GeneralUtils.find_text_vector_similarity( - # vector_search_str, - # "wine", - # "tasting_notes_embedding", - # GeneralUtils.execute_postgres_sql(pg_conn_str, sql), #BUG input pair (F, arg) - # a.context.getTextEmbedding([vector_search_str]) #BUG input pair (F, arg) - # ) - - - # @show df - # error(888888) - - - items = nothing - if sql_result_df !== nothing - result_vec = GeneralUtils.dfToVectorDict(sql_result_df) - - # get image - for d in result_vec - image_url_json_str = d["image_url"] - image_url_json_obj = JSON.parse(image_url_json_str) - base_url = "http://192.168.88.106:8080/" - if haskey(image_url_json_obj, "bottle") - url = base_url * image_url_json_obj["bottle"] - image_data = HTTP.get(url) # vector{int} data - image_base64_string = base64encode(image_data.body) - d["image"] = image_base64_string - else - d["image"] = nothing - end - end - items = result_vec # image is added to each item - end - - thoughtdict["action_result"] = textresult - end - - return (thoughtdict=thoughtdict, result_raw=items) -end - - -function generatesql(a::T, searchterm::String, - ; maxattempt=10 - )::String where {T<:agent} - - systemmsg = - """ - # database_search_guidelines - - Keep SQL queries focused only on the provided information. - - Use wildcard character (%) to search more effectively. - - Do not create any table in the database. - - A junction table can be used to link tables together. Another use case is for filtering data. - - If you can't find a single table that can be used to answer the user's search term, try joining multiple tables to see if you can obtain the answer. - - Text information in the database usually stored in lower case. If your search returns empty, try using lower case to search. - - Overly strict condition usually yields empth result - - # situation - At each round of conversation, you will be given the following: - - user search term - - # objective - Consult the database_search_guidelines. Then find the data from a database to satisfy the user's search term. - - # your responsibility includes - Fulfill the objective. - - # you should then respond to the user with interleaving plan, action_name, action_input - 1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. - 2) "action_name", Must be "RUNSQL" - 3) "action_input", The input to the action you are about to perform according to your plan. - After the action is executed you gets "action_result". It is the output from the action you selected. - - # you should only respond in JSON format as described below - "plan": "...", - "action_name": "...", - "action_input": "..." - - # available_actions - "RUNSQL", which you can use to execute SQL against the database. - The input must be a single SQL query to be executed against the database. - For more effective text search, it's necessary to use case-insensitivity and the ILIKE operator. - Do not wrap the SQL as it will be executed against the database directly and SQL must be ended with ';'. - """ - - # table_schema = - # """ - # create table customer ( - # customer_id uuid primary key default gen_random_uuid (), - # customer_firstname varchar(128), - # customer_lastname varchar(128), - # customer_displayname varchar(128) not null, - # customer_username varchar(128), - # customer_password varchar(128), - # customer_gender varchar(128), - # country varchar(128), - # telephone varchar(128), - # email varchar(128) not null, - # customer_birthdate varchar(128), - # note text, - - # other_attributes jsonb, - # created_time timestamptz default current_timestamp, - # updated_time timestamptz default current_timestamp, - # description text - # ); - - # create table retailer ( - # retailer_id uuid primary key default gen_random_uuid (), - # retailer_name varchar(128) not null, - # retailer_username varchar(128) not null, - # retailer_password varchar(128) not null, - # retailer_address text not null, - # country varchar(128) not null, - # contact_person varchar(128) not null, - # telephone varchar(128) not null, - # email varchar(128) not null, - # note text, - - # other_attributes jsonb, - # created_time timestamptz default current_timestamp, - # updated_time timestamptz default current_timestamp, - # description text - # ); - - # create table food ( - # food_id uuid primary key default gen_random_uuid (), - # food_name varchar(128) not null, - # country varchar(128), - # spiciness integer, - # sweetness integer, - # sourness integer, - # savoriness integer, - # bitterness integer, - # serving_temperature integer, - # image_url jsonb, - # note text, - # other_attributes jsonb, - - # created_time timestamptz default current_timestamp, - # updated_time timestamptz default current_timestamp, - # description text - # ); - - # create table wine ( - # wine_id uuid primary key default gen_random_uuid (), - # seo_name varchar(128) not null, - # wine_name varchar(128) not null, - # winery varchar(128) not null, - # vintage integer not null, - # region varchar(128) not null, - # country varchar(128) not null, - # wine_type varchar(128) not null, - # grape varchar(128) not null, - # serving_temperature varchar(128) not null, - # intensity integer, - # sweetness integer, - # tannin integer, - # acidity integer, - # fizziness integer, - # tasting_notes text, - # image_url jsonb, - # manufacturer_sku text, - # note text, - # other_attributes jsonb, - - # created_time timestamptz default current_timestamp, - # updated_time timestamptz default current_timestamp, - # description text - # ); - - # create table wine_food ( - # wine_id uuid references wine(wine_id), - # food_id uuid references food(food_id), - # constraint wine_food_id primary key (wine_id, food_id), - - # created_time timestamptz default current_timestamp, - # updated_time timestamptz default current_timestamp - # ); - - # CREATE TABLE retailer_wine ( - # retailer_id uuid references retailer(retailer_id), - # wine_id uuid references wine(wine_id), - # constraint retailer_wine_id primary key (retailer_id, wine_id), - # price NUMERIC(10, 2), - # currency varchar(3) not null, - - # created_time timestamptz default current_timestamp, - # updated_time timestamptz default current_timestamp - # ); - - # CREATE TABLE retailer_food ( - # retailer_id uuid references retailer(retailer_id), - # food_id uuid references food(food_id), - # constraint retailer_food_id primary key (retailer_id, food_id), - # price NUMERIC(10, 2), - # currency varchar(3) not null, - - # created_time timestamptz default current_timestamp, - # updated_time timestamptz default current_timestamp - # ); - # """ - - requiredKeys = ["plan", "action_name", "action_input"] - errornote = "" - # provide similar sql only for the first attempt - # sql, distance = a.context.similarSQLVectorDB(searchterm) - - # similarSQL_ = sql !== nothing ? sql : "None" - # # if sql is really close, just use it - # if similarSQL_ != "None" && distance <= 0.1 - # return similarSQL_ - # end - - #CHANGE use find_related_tables_for_user_question and inject only related table schema instead - # of hard code table schema. CPU embedding is too slow. use embedding service on GPU. - related_tables = a.context.find_related_tables_for_user_question(searchterm) - table_schema = "" - for table in related_tables - _table_schema_str = GeneralUtils.get_db_table_schema_simple(a.context.pg_conn_str, table) - table_schema_str = sprint(show, _table_schema_str) * "\n" - table_schema = table_schema * table_schema_str - end - - context = - """ - - - $table_schema - - - """ - input = context * searchterm - - msg = Dict( - "model" => "gemma-4-E4B-it-UD-Q4_K_XL", - "messages" => [ - Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => systemmsg), - ] - ), - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => input), - ] - ), - ], - "temperature" => 0.7 - ) - - for attempt in 1:maxattempt - response = a.context.text2textInstructLLM("random_id", msg) - - response = GeneralUtils.clean_json_response(response) - - think, response = GeneralUtils.extractthink(response) - responsedict = nothing - try - _responsedict = JSON.parse(response) - responsedict = GeneralUtils.dictify(_responsedict, keytype=String, sort_order=requiredKeys) - catch - println("\nERROR decisionMaker() failed to parse response: $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # check whether all answer's key points are in responsedict - ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) - if !ispass - errornote = errormsg - println("\nERROR YiemAgent decisionMaker() $errornote --(not qualify response)> $responsedict ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - continue - end - - # remove backticks Error occurred: MethodError: no method matching occursin(::String, ::Vector{String}) - if occursin("```", responsedict["action_input"]) - sql = GeneralUtils.extract_triple_backtick_text(responsedict["action_input"])[1] - if sql[1:4] == "sql\n" - sql = sql[5:end] - end - sql = split(sql, ';') # some time there are comments in the sql - sql = sql[1] * ';' - - responsedict["action_input"] = sql - end - - toollist = ["RUNSQL"] - if responsedict["action_name"] ∉ toollist - errornote = "Your previous attempt has action_name that is not in the tool list" - println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote --(not qualify response)--> $(responsedict["action_name"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - for i in toollist - if occursin(i, responsedict["action_input"]) - errornote = "Your previous attempt has action_name in action_input which is not allowed" - println("\nERROR SQLLLM decisionMaker(). Attempt $attempt/$maxattempt. $errornote --(not qualify response)--> $(responsedict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - end - - # println("\nSQLLLM decisionMaker() ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - # pprintln(responsedict) - # println("---") - - return responsedict["action_input"] - end - error("SQLLLM DecisionMaker() failed to generate a thought \n", response) -end - - -""" -# Example -```jldoctest -julia> using ChatAgent -julia> agent = YiemAgent.sommelier(...) -julia> thoughtdict = - OrderedDict{String, Any}( - "plan" => "The user is asking a very specific question about a wine (Brunello di Montalcino from Tenuta CastelGiocondo). Although the policy suggests gathering budget, wine type, and occasion, the user has provided enough specific information (name, region, producer) to attempt a direct search in the database. I will use the SEARCH_WINE_DATABASE action to check if this specific wine is in our inventory.", - "action_name" => "SEARCH_WINE_DATABASE", - "action_input" => "Brunello di Montalcino from Tenuta CastelGiocondo") -``` -julia> predefined_wine_search_sql(agent, thoughtdict["action_input"]) -""" -function wine_search_term_classification(a::T, searchterm::String, - ; maxattempt=10 - ) where {T<:agent} - - systemmsg = - """ - - # situation - At each round of conversation, you will be given the following: - - user search term - - database tables schema - - # objective - Consult the provided database schema (tables and columns), please map a user's natural-language search term to the appropriate database columns and tables—identify the relevant fields, operators, and values (e.g., for SQL filtering). - - # your responsibility includes - Fulfill the objective. - - # You must output your response as a JSON object containing a single key: "extracted_info". - The "extracted_info" key must contain an array of objects. Each object must contain: - 1) "table_name": The name of the table. - 2) "column_name": The specific column being filtered. - 3) "operator": The comparison operator (e.g., "=", ">"). - 4) "value": The value to compare against. - - If the user does not specify any filters, return an empty array for "extracted_info": {"extracted_info": []}. - - # here are some example - - 4-wheel drive car with red color that will give me fast and furious emotion. No more than 7000 USD - - - { - "extracted_info": [ - { - "table_name": "car_info", - "column_name": "drive_type", - "operator": "=", - "value": "4-wheel" - }, - { - "table_name": "car_info", - "column_name": "color", - "operator": "=", - "value": "red" - }, - { - "table_name": "car_info", - "column_name": "drive_feeling", - "operator": "=", - "value": "fast and furious" - }, - { - "table_name": "price_list", - "column_name": "price", - "operator": "<", - "value": "7000" - } - } - - """ - - - # use find_related_tables_for_user_question and inject only related table schema for a given search term - # to LLM instead of giving LLM all tables schema. - related_tables = a.context.find_related_tables_for_user_question(searchterm) - table_schema = "" - for table in related_tables - _table_schema_str = get_db_table_schema_simple_with_samples(a.context.pg_conn_str, table) - - # _table_schema_str = GeneralUtils.get_db_table_schema_simple(a.context.pg_conn_str, table) - table_schema_str = sprint(show, _table_schema_str) * "\n" - table_schema = table_schema * table_schema_str - end - - context = - """ - - - $table_schema - - - """ - input = context * searchterm - - response_format = Dict( - "type" => "json_schema", - "json_schema" => Dict( - "name" => "extracted_conditions", - "strict" => true, - "schema" => Dict( - "type" => "object", - "properties" => Dict( - "extracted_info" => Dict( - "type" => "array", - "items" => Dict( - "type" => "object", - "properties" => Dict( - "table_name" => Dict( - "type" => "string", - "description" => "The name of the database table." - ), - "column_name" => Dict( - "type" => "string", - "description" => "The name of the column to filter on." - ), - "operator" => Dict( - "type" => "string", - "enum" => ["=", "!=", ">", "<", ">=", "<=", "LIKE", "IN", "IS NULL", "IS NOT NULL"], - "description" => "The SQL comparison operator." - ), - "value" => Dict( - "type" => ["string", "null"], - "description" => "The value to compare against. Use null for IS NULL/IS NOT NULL." - ) - ), - "required" => ["table_name", "column_name", "operator", "value"], - "additionalProperties" => false - ) - ) - ), - "required" => ["extracted_info"], - "additionalProperties" => false - ) - ) -) - - msg = Dict( - "model" => "gemma-4-E4B-it-UD-Q4_K_XL", - "messages" => [ - Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => systemmsg), - ] - ), - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => input), - ] - ), - ], - "temperature" => 0.7, - "response_format"=> response_format, - ) - - for attempt in 1:maxattempt - response = a.context.text2textInstructLLM("random_id", msg) - responsedict = JSON.parse(response) - # responsedict = nothing - # try - # responsedict = Serde.parse_yaml(response) - # catch e - # println("\nERROR YiemAgent predefined_wine_search_sql() Error: $e --(not qualify response)-> $response ", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - # continue - # end - - # println("\n ", table_schema) - println("\n ", responsedict) - @info "before BM25 " @__LINE__ - - # to ensure user input is correct - for entry in responsedict["extracted_info"] - table_name = entry["table_name"]::String - column_name = entry["column_name"]::String - - bucket = classify_column(a.context.pg_conn_str, table_name, column_name) - - if bucket == "fuzzy_correction" - words_catalog = GeneralUtils.harvest_entity_catalog(a.context.pg_conn_str, table_name, column_name) - resolved_word = GeneralUtils.resolve_entity(entry["value"], words_catalog; threshold=0.9) - entry["value"] = resolved_word - end - end - - # filter for column that will be used for hard condition (SQL where clause) - # column with non-standard operator will be used in vector search - vector_search_words = "" - hard_operators = ["=","<>","!=",">","<",">=","<=","!<","!>","<=>"] - - # Build new list of hard condition entries - hard_conditions = JSON.Object{String, Any}[] - vector_search = JSON.Object{String, Any}[] - for entry in responsedict["extracted_info"] - if entry["operator"] ∈ hard_operators - push!(hard_conditions, entry) - else - push!(vector_search, entry) - end - end - responsedict = hard_conditions - - println("") - @show responsedict - @info "predefined_wine_search_sql() " @__LINE__ - - return (hard_conditions=hard_conditions, vector_search=vector_search) - end - error("SQLLLM DecisionMaker() failed to generate a thought \n", response) -end - -function predefined_wine_search_sql(conditions::Vector{JSON.Object{String, Any}})::String - # 1. Base SQL structure - base_query = -""" -SELECT - w.winery, - w.wine_name, - w.wine_id, - w.vintage, - w.region, - w.country, - w.wine_type, - w.grape, - w.serving_temperature, - w.sweetness, - w.intensity, - w.tannin, - w.acidity, - w.tasting_notes, - rw.price, - rw.currency, - w.image_url, - r.retailer_name, - rw.retailer_id -FROM wine AS w -JOIN retailer_wine AS rw ON w.wine_id = rw.wine_id -JOIN retailer AS r ON rw.retailer_id = r.retailer_id -""" - - # 2. Dynamic WHERE Clause Builder - where_clauses = String[] - - # Iterate over each condition object in the array - for cond in conditions - table_name = String(cond["table_name"]) - column_name = String(cond["column_name"]) - op = String(cond["operator"]) - raw_val = cond["value"] - - # Determine table alias - alias = if table_name == "wine" - "w" - elseif table_name == "retailer_wine" - "rw" - else - continue - end - - # --- Value Type Handling --- - final_val = raw_val - - if op in ("=", "<", ">", "<=", ">=") - str_val = string(raw_val) - num_val = tryparse(Float64, str_val) - - if !isnothing(num_val) - final_val = isinteger(num_val) ? round(Int, num_val) : num_val - end - end - - # --- SQL Formatting --- - if isa(final_val, Number) - clause = "$(alias).$(column_name) $(op) $(final_val)" - else - escaped_val = replace(string(final_val), "'" => "''") - clause = "$(alias).$(column_name) $(op) '$(escaped_val)'" - end - - push!(where_clauses, clause) - end - - # 3. Assemble Final Query - where_sql = isempty(where_clauses) ? "" : "WHERE " * join(where_clauses, " AND ") - - return string(base_query, where_sql, ";") -end - -function SQLexecution(executeSQL::Function, sql::T - )::NamedTuple where {T<:AbstractString} - - try - # add LIMIT to the SQL to prevent loading large data - sql = strip(sql) - - # remove DISTINCT keyword because it is incompatible with RANDOM() - sql = replace(sql, "DISTINCT" => "") - - if sql[end] == ';' - if !occursin("LIMIT", sql) - sql = sql[1:end-1] * " ORDER BY RANDOM() LIMIT 2;" - end - else - sql = sql * ";" - end - result = executeSQL(sql) - df = DataFrame(result) - tablesize = size(df) - row, column = tablesize - if row == 0 - return (result_str="No records found. Try loosening your search criteria.", result_raw=nothing, success=true, errormsg=nothing) - elseif column > 30 - return (result_str="There are more than 30 columns. Please be more specific.", result_raw=df, success=true, errormsg=nothing) - else - df1 = - if row > 2 - # ramdom row to pick - df[sample(1:nrow(df), 2, replace=false), :] # random select 2 rows from df - else - df - end - result = GeneralUtils.dfToString(df1) - # println("\n~~~ SQLexecution() result: ", @__FILE__, " ", @__LINE__) - # println(sql) - # println(df1) - # println("\n") - return (result_str=result, result_raw=df1, success=true, errormsg=nothing) - end - catch e - io = IOBuffer() - showerror(io, e) - errorMsg = String(take!(io)) - st = sprint((io, v) -> show(io, "text/plain", v), stacktrace(catch_backtrace())) - println(errorMsg) - return (result_str=nothing, result_raw=nothing, success=false, errormsg=errorMsg) - end -end - -function DEPRECIATED_search_wine_database!(a::T, thoughtdict::AbstractDict; useSQLLLM::Bool=false - )::NamedTuple{(:thoughtdict, :result_raw), Tuple{OrderedDict, Any}} where {T<:agent} - - # XXX - predefined_wine_search_sql(a, thoughtdict["action_input"]) - - println("\ncheckinventory order: $(thoughtdict["action_input"]) ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - wineattributes_1 = extractWineAttributes_1(a, thoughtdict["action_input"]) - wineattributes_2 = extractWineAttributes_2(a, thoughtdict["action_input"]) - - retrieve_attributes = ["winery", "wine_name", "wine_id", "vintage", "region", "country", "wine_type", "grape", "serving_temperature", "sweetness", "intensity", "tannin", "acidity", "tasting_notes", "price", "currency", "image_url", "retailer_name", "retailer_id"] - _inventoryquery = "$(thoughtdict["action_input"]), $wineattributes_1, $wineattributes_2, retailer_name: $(a.retailername), retailerid: $(a.retailerid)" - inventoryquery = "Retrieves $retrieve_attributes of wines that match the following criteria - {$_inventoryquery}" - println("\ncheckinventory input: $inventoryquery ", @__FILE__, ":", @__LINE__, " $(Dates.now())") - - if useSQLLLM - # add suppport for similarSQLVectorDB - textresult, result_raw = SQLLLM.query( - inventoryquery, - a.context.executeSQL, - a.context.text2textInstructLLM; - insertSQLVectorDB=a.context.insertSQLVectorDB, - similarSQLVectorDB=a.context.similarSQLVectorDB, - llmFormatName="qwen3") - thoughtdict["action_result"] = textresult - else - - # direct query with possible sql instead of SQLLLM. - sql = generatesql(a, inventoryquery) - println("\nSQL: $sql ", @__FILE__, ":", @__LINE__, " $(Dates.now()) \n") - textresult, sql_result_df, success, _ = SQLexecution(a.context.executeSQL, sql) - - items = nothing - if sql_result_df !== nothing - result_vec = GeneralUtils.dfToVectorDict(sql_result_df) - - # get image - for d in result_vec - image_url_json_str = d["image_url"] - image_url_json_obj = JSON.parse(image_url_json_str) - base_url = "http://192.168.88.106:8080/" - if haskey(image_url_json_obj, "bottle") - url = base_url * image_url_json_obj["bottle"] - image_data = HTTP.get(url) # vector{int} data - image_base64_string = base64encode(image_data.body) - d["image"] = image_base64_string - else - d["image"] = nothing - end - end - items = result_vec # image is added to each item - end - - thoughtdict["action_result"] = textresult - end - - return (thoughtdict=thoughtdict, result_raw=items) -end - -""" - -# Arguments - - `v::Integer` - dummy variable - -# Return - -# Example -```jldoctest -julia> -``` -""" -function extractWineAttributes_1(a::T1, input::T2; maxattempt=10 - )::String where {T1<:agent, T2<:AbstractString} - - systemmsg = - """ - - At each round of conversation, the user provides the following: - - The query: the query provided by the user. - - - Extract information from the user's query as much as possible according to wine attributes extraction guidelines to fill out user's preference form. - - - Fulfill the objective. - - - - If specific information required in the preference form is not available in the query or there isn't any, mark with "N/A" to indicate this. - Additionally, words like 'any' or 'unlimited' mean no information is available. - - Do not generate other comments. - - - wine_name: name of the wine - winery: name of the winery - vintage: the year of the wine - country: a country where wine is produced. Can be "Austria", "Australia", "France", "Germany", "Italy", "Portugal", "Spain", "United States". Use "or" if there are multiple countries. - wine_type: can be one of: "red", "white", "sparkling", "rose", "dessert" or "fortified" - grape_varietal: the name of the primary grape used to make the wine - tasting_notes: a word describe the wine's flavor, such as "butter", "oak", "fruity", "raspberry", "earthy", "floral", etc - wine_price_min: minimum price range of wine. Example: For wine price 20, wine_price_min will be 0. For wine price 10 to 100, wine_price_min will be 10. - wine_price_max: maximum price range of wine. Example: For wine price 20, wine_price_max will be 20. For wine price 10 to 100, wine_price_max will be 100. - occasion: the occasion the user is having the wine for - food_to_be_paired_with_wine: food that the user will be served with the wine such as poultry, fish, steak, etc - _keyword suffice is the related keyword that appears in user's query. - - - "wine_name": "...", - "winery": "...", - "vintage": "...", - "country": "...", - "wine_type": "...", - "grape_varietal": "...", - "tasting_notes": "...", - "wine_price_min": "...", - "wine_price_max": "...", - "occasion": "...", - "food_to_be_paired_with_wine": "..." - - - User's query: red, Chenin Blanc, Riesling, 20 USD from Tuscany, Italy or Napa Valley, USA - "wine_name": "N/A", - "winery": "N/A", - "vintage": "N/A", - "country": "Italy or United States", - "wine_type": "red or white", - "grape_varietal": "Chenin Blanc or Riesling", - "tasting_notes": "citrus", - "wine_price_min": "0", - "wine_price_max": "20", - "occasion": "N/A", - "food_to_be_paired_with_wine": "N/A" - - User's query: Domaine du Collier Saumur Blanc 2019, France, white, Merlot - "wine_name": "Saumur Blanc", - "winery": "Domaine du Collier", - "vintage": "2019", - "country": "France", - "wine_type": "white", - "grape_varietal": "Merlot", - "tasting_notes": "N/A", - "wine_price_min": "N/A", - "wine_price_max": "N/A", - "occasion": "N/A", - "food_to_be_paired_with_wine": "N/A" - - """ - requiredKeys = ["wine_name", "winery", "vintage", "country", "wine_type", "grape_varietal", "tasting_notes", "wine_price_min", "wine_price_max", "occasion", "food_to_be_paired_with_wine"] - errornote = "" - context = - """ - - $errornote - - """ - - input = context * input - - msg = Dict( - "model" => "gemma-4-E4B-it-UD-Q4_K_XL", - "messages" => [ - Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => systemmsg), - ] - ), - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => input), - ] - ), - ], - "temperature" => 0.7 - ) - - for attempt in 1:maxattempt - response = a.context.text2textInstructLLM(a.id, msg) - response = GeneralUtils.clean_json_response(response) - - response = GeneralUtils.remove_french_accents(response) - think, response = GeneralUtils.extractthink(response) - responsedict = nothing - try - _responsedict = JSON.parse(response) - responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys) - catch - println("\nERROR YiemAgent extractWineAttributes_1() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - # check whether all answer's key points are in responsedict - ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) - if !ispass - errornote = errormsg - println("\nERROR YiemAgent extractWineAttributes_1() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - continue - end - removekeys = ["thought", "tasting_notes", "occasion", "food_to_be_paired_with_wine", "vintage"] - for i in removekeys - delete!(responsedict, i) - end - # remove (some text) - for (k, v) in responsedict - _v = replace(v, r"\(.*?\)" => "") - responsedict[k] = _v - end - - @info "YiemAgent extractWineAttributes_1() " @__LINE__ - @show responsedict - @info "---\n" @__LINE__ - - # check each attributes against each column in a database table with BM25 - for (k, v) in responsedict - if k ∉ ["wine_price_min", "wine_price_max"] - words_catalog = GeneralUtils.harvest_entity_catalog(a.context.pg_conn_str, "wine", k) - resolved_word = GeneralUtils.resolve_entity(v, words_catalog; threshold=0.9) - responsedict[k] = resolved_word - end - end - - result = "" - for (k, v) in responsedict - # some time LLM generate text with "(some comment)". this line removes it - if !occursin("N/A", v) && v != "" && !occursin("none", v) && !occursin("None", v) - result *= "$k: $v, " - end - end - - result = result[1:end-2] # remove the ending ", " - - @info "YiemAgent extractWineAttributes_1() " @__LINE__ - @show result - @info "---\n" @__LINE__ - - return result - end - error("extractWineAttributes_1() failed to get a response") -end - -""" - - TODO "French dry white wines with medium bod" the LLM does not recognize sweetness. use LLM self questioning to solve. - - TODO French Syrah, Viognier, under 100. LLM extract intensiry of 3-5. why? -""" -function extractWineAttributes_2(a::T1, input::T2)::String where {T1<:agent, T2<:AbstractString} - - conversiontable = - """ - - Intensity level: - 1 to 2: May correspond to "light-bodied" or a similar description. - 2 to 3: May correspond to "med light bodied", "medium light" or a similar description. - 3 to 4: May correspond to "medium bodied" or a similar description. - 4 to 5: May correspond to "med full bodied", "medium full" or a similar description. - 4 to 5: May correspond to "full bodied" or a similar description. - Sweetness level: - 1 to 2: May correspond to "dry", "no sweet" or a similar description. - 2 to 3: May correspond to "off dry", "less sweet" or a similar description. - 3 to 4: May correspond to "semi sweet" or a similar description. - 4 to 5: May correspond to "sweet" or a similar description. - 4 to 5: May correspond to "very sweet" or a similar description. - Tannin level: - 1 to 2: May correspond to "low tannin" or a similar description. - 2 to 3: May correspond to "semi low tannin" or a similar description. - 3 to 4: May correspond to "medium tannin" or a similar description. - 4 to 5: May correspond to "semi high tannin" or a similar description. - 4 to 5: May correspond to "high tannin" or a similar description. - Acidity level: - 1 to 2: May correspond to "low acidity" or a similar description. - 2 to 3: May correspond to "semi low acidity" or a similar description. - 3 to 4: May correspond to "medium acidity" or a similar description. - 4 to 5: May correspond to "semi high acidity" or a similar description. - 4 to 5: May correspond to "high acidity" or a similar description. - - """ - - systemmsg = - """ - - At each round of conversation, you will be given the following information: - conversion_table: a conversion table that maps descriptive words to their corresponding integer levels - query: the words from the user's query that describe their preferences - - - Fill out the user's preference form based on the corresponding words from the user's query according to the guidelines. - - - Fulfill the objective - - - - The preference form requires sweetness, acidity, tannin, intensity infomation - - If specific information required in the preference form is not available in the query or there isn't any, mark with 'N/A' to indicate this. - Additionally, words like 'any' or 'unlimited' mean no information is available. - - Use the conversion table to convert the descriptive word level of sweetness, intensity, tannin, and acidity into a corresponding integer. - - Do not generate other comments. - - - sweetness_keyword: The exact keywords in the user's query describing the sweetness level of the wine. - sweetness: ( S ), where ( S ) represents integers indicating the range of sweetness levels. Example: 1-2 - acidity_keyword: The exact keywords in the user's query describing the acidity level of the wine. - acidity: ( A ), where ( A ) represents integers indicating the range of acidity level. Example: 3-5 - tannin_keyword: The exact keywords in the user's query describing the tannin level of the wine. - tannin: ( T ), where ( T ) represents integers indicating the range of tannin level. Example: 1-3 - intensity_keyword: The exact keywords in the user's query describing the intensity level of the wine. - intensity: ( I ), where ( I ) represents integers indicating the range of intensity level. Example: 2-4 - - - "sweetness_keyword": "...", - "sweetness_min": "...", - "sweetness_max": "...", - "acidity_keyword": "...", - "acidity_min": "...", - "acidity_max": "...", - "tannin_keyword": "...", - "tannin_min": "...", - "tannin_max": "...", - "intensity_keyword": "...", - "intensity_min": "...", - "intensity_max": "..." - - - User's query: I want a wine with a medium-bodied, low acidity, medium tannin. - "sweetness_keyword": "N/A", - "sweetness_min": "N/A", - "sweetness_max": "N/A", - "acidity_keyword": "low acidity", - "acidity_min": 1, - "acidity_max": 2, - "tannin_keyword": "medium tannin", - "tannin_min": 3, - "tannin_max": 4, - "intensity_keyword": "medium-bodied", - "intensity_min": 3, - "intensity_max": 4 - - User's query: German red wine, under 100, pairs with spicy food. - "sweetness_keyword": "N/A", - "sweetness_min": "N/A", - "sweetness_max": "N/A", - "acidity_keyword": "N/A", - "acidity_min": "N/A", - "acidity_max": "N/A", - "tannin_keyword": "N/A", - "tannin_min": "N/A", - "tannin_max": "N/A", - "intensity_keyword": "N/A", - "intensity_min": "N/A", - "intensity_max": "N/A" - - """ - requiredKeys = ["sweetness_keyword", "sweetness_min", "sweetness_max", - "acidity_keyword", "acidity_min", "acidity_max", - "tannin_keyword", "tannin_min", "tannin_max", - "intensity_keyword", "intensity_min", "intensity_max"] - errornote = "" - context = - """ - - $conversiontable - $errornote - - """ - - input = context * input - - msg = Dict( - "model" => "gemma-4-E4B-it-UD-Q4_K_XL", - "messages" => [ - Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => systemmsg), - ] - ), - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => input), - ] - ), - ], - "temperature" => 0.7 - ) - - for attempt in 1:10 - response = a.context.text2textInstructLLM(a.id, msg) - response = GeneralUtils.clean_json_response(response) - - response = GeneralUtils.remove_french_accents(response) - think, response = GeneralUtils.extractthink(response) - responsedict = nothing - try - _responsedict = JSON.parse(response) - responsedict = GeneralUtils.dictify(_responsedict; keytype=String, sort_order=requiredKeys) - catch - println("\nERROR YiemAgent extractWineAttributes_2() failed to parse response: $response", @__FILE__, ":", @__LINE__, " $(Dates.now())") - continue - end - - # check whether all answer's key points are in responsedict - ispass, errormsg = GeneralUtils.checkAgentResponse_JSON(responsedict, requiredKeys) - if !ispass - errornote = errormsg - println("\nERROR YiemAgent extractWineAttributes_2() $errornote --(not qualify response)> $responsedict", @__FILE__, ":", @__LINE__, " $(Dates.now())\n") - continue - end - - # delete some key words from responsedict - for (k, v) in responsedict - if k ∈ ["sweetness_keyword", "acidity_keyword", "tannin_keyword", "intensity_keyword"] - delete!(responsedict, k) - end - end - - # get result in String. Reject "N/A" value - result = "" - for (k, v) in responsedict - if typeof(v) <: Number - result *= "$k: $v, " - elseif typeof(v) == String && !occursin("N/A", v) - result *= "$k: $v, " - end - end - result = result[1:end-2] # remove the ending ", " - - @info "YiemAgent extractWineAttributes_2() " @__LINE__ - @show result - @info "---\n" @__LINE__ - - return result - end - error("extractWineAttributes_2() failed to get a response") -end - - - - - - -function get_db_table_schema_simple_with_samples(pg_conn_str::String, table_name::String; - schema_name::String="public")::String - conn = LibPQ.Connection(pg_conn_str) - return get_db_table_schema_simple_with_samples(conn, table_name; schema_name=schema_name) -end - -function get_db_table_schema_simple_with_samples(conn, table_name::String; schema_name::String="public", sample_count::Int=3)::String - # 1. SQL query for catalog metadata - meta_sql = """ - SELECT - a.attname AS column_name, - format_type(a.atttypid, a.atttypmod) AS data_type, - pg_get_expr(def.adbin, def.adrelid) AS default_value, - COALESCE( - (SELECT pg_get_constraintdef(p.oid) - FROM pg_catalog.pg_constraint p - WHERE p.conrelid = c.oid AND a.attnum = ANY(p.conkey) - LIMIT 1), '' - ) AS constraint_definition - FROM pg_catalog.pg_attribute a - JOIN pg_catalog.pg_class c ON a.attrelid = c.oid - JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid - LEFT JOIN pg_catalog.pg_attrdef def ON def.adrelid = c.oid AND def.adnum = a.attnum - WHERE c.relname = \$1 - AND n.nspname = \$2 - AND a.attnum > 0 - AND NOT a.attisdropped - ORDER BY a.attnum; - """ - - meta_res = DataFrame(execute(conn, meta_sql, [table_name, schema_name])) - - if nrow(meta_res) == 0 - error("Table '$schema_name.$table_name' not found.") - end - - # 2. Build single dynamic query to fetch non-null samples for all columns - sample_selects = String[] - for row in eachrow(meta_res) - c_name = row.column_name - push!(sample_selects, """ - (SELECT json_agg(s."$c_name") - FROM ( - SELECT "$c_name" - FROM "$schema_name"."$table_name" - WHERE "$c_name" IS NOT NULL - LIMIT $sample_count - ) s - ) AS "$c_name" - """) - end - - sample_sql = "SELECT " * join(sample_selects, ",\n ") * ";" - sample_df = DataFrame(execute(conn, sample_sql)) - - # 3. Build DDL definitions with inline sample comments - ddl_lines = String[] - constraints = String[] - - for row in eachrow(meta_res) - col_name = row.column_name - data_type = row.data_type - default_val = ismissing(row.default_value) ? "" : " DEFAULT " * row.default_value - - col_def = " \"$col_name\" $data_type$default_val" - - # Fetch sample data for this column from the single-row sample DataFrame - samples_comment = "" - if nrow(sample_df) > 0 - raw_samples = sample_df[1, Symbol(col_name)] - samples_str = ismissing(raw_samples) || isnothing(raw_samples) ? "[]" : string(raw_samples) - samples_comment = " -- Samples: $samples_str" - end - - push!(ddl_lines, col_def * samples_comment) - - # Handle table-level constraints - con_def = ismissing(row.constraint_definition) ? "" : row.constraint_definition - if !isempty(con_def) && !(con_def in constraints) - push!(constraints, " " * con_def) - end - end - - all_definitions = vcat(ddl_lines, constraints) - body = join(all_definitions, ",\n") - - return "CREATE TABLE \"$schema_name\".\"$table_name\" (\n$body\n);" -end - - - -function classify_column(pg_conn_str::String, table_name::String, column_name::String; - sample_size::Integer=1000) - conn = LibPQ.Connection(pg_conn_str) - return classify_column(conn, table_name, column_name; sample_size=sample_size) -end - - -function classify_column(conn::LibPQ.Connection, table_name::String, column_name::String; sample_size::Int=1000) - # 1. Fetch BOTH data_type and udt_name (User Defined Type name) - meta_query = """ - SELECT data_type, udt_name - FROM information_schema.columns - WHERE table_name = lower('$(table_name)') - AND column_name = lower('$(column_name)'); - """ - - pg_type = "unknown" - udt_name = "unknown" - - try - df = DataFrame(LibPQ.execute(conn, meta_query)) - if !isempty(df) - pg_type = df[1, :data_type] - udt_name = df[1, :udt_name] - end - catch e - @error "Failed to fetch metadata for $table_name.$column_name" exception=e - return "error" - end - - # 2. FAST-TRACK: Check for pgvector FIRST - # pgvector registers as "USER-DEFINED" in data_type, but "vector" in udt_name - if udt_name == "vector" - return "semantic_search" - end - - # 3. FAST-TRACK: Hard rules for standard non-text Postgres types - if pg_type in ["integer", "bigint", "smallint", "numeric", "real", - "double precision", "boolean", "date", - "timestamp without time zone", "timestamp with time zone", "uuid"] - return "exact_or_range" - end - - # 4. SAMPLE: Get text statistics for remaining text columns - stats_query = """ - SELECT - COUNT(*)::int AS total_count, - COUNT(DISTINCT $(column_name)::text)::int AS unique_count, - COALESCE(AVG(LENGTH($(column_name)::text)), 0)::float AS avg_len, - COALESCE(STDDEV(LENGTH($(column_name)::text)), 0)::float AS std_len - FROM ( - SELECT $(column_name) - FROM $(table_name) - WHERE $(column_name) IS NOT NULL - LIMIT $sample_size - ) AS sampled_data; - """ - - try - df = DataFrame(LibPQ.execute(conn, stats_query)) - if isempty(df) || df[1, :total_count] == 0 - return "unknown" - end - - total = df[1, :total_count] - unique = df[1, :unique_count] - avg_len = df[1, :avg_len] - std_len = df[1, :std_len] - ratio = unique / total - - # 5. HEURISTICS: Route the column_name to the correct text bucket - return classify_text_column(unique, ratio, avg_len, std_len) - - catch e - @warn "Failed to sample column_name $table_name.$column_name" exception=e - return "unknown" - end -end - -# The Decision Tree for Text Columns (Unchanged, but kept for completeness) -function classify_text_column(unique_count::Integer, ratio::Float64, avg_len::Float64, std_len::Float64) - if avg_len > 60 && std_len > 25 - return "full_text_search" - end - if ratio > 0.90 && avg_len < 40 - return "exact_or_regex" - end - if unique_count <= 100 - return "fuzzy_correction" - end - if ratio > 0.10 && avg_len < 35 - return "fuzzy_correction" - end - if avg_len < 60 - return "fuzzy_correction" - end - return "full_text_search" -end - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -end # module llmfunction \ No newline at end of file diff --git a/src/messages.jl b/src/messages.jl new file mode 100644 index 0000000..bf29b07 --- /dev/null +++ b/src/messages.jl @@ -0,0 +1,183 @@ +""" + messages.jl - Custom message types and LLM conversion + +This module provides custom message types and the convertToLlm function. +""" + +module Messages + +using ..Types: * + +const COMPACTION_SUMMARY_PREFIX = """The conversation history before this point was compacted into the following summary: + + +""" + +const COMPACTION_SUMMARY_SUFFIX = """ +""" + +const BRANCH_SUMMARY_PREFIX = """The following is a summary of a branch that this conversation came back from: + + +""" + +const BRANCH_SUMMARY_SUFFIX = """""" + +# ============================================================================ +# Custom message types +# ============================================================================ + +mutable struct BashExecutionMessage + role::String + command::String + output::String + exit_code::Union{Int64, Nothing} + cancelled::Bool + truncated::Bool + full_output_path::Union{String, Nothing} + timestamp::Timestamp + exclude_from_context::Bool +end + +mutable struct CustomMessage{T} + role::String + custom_type::String + content::Union{String, Vector{MessageContent}} + display::Bool + details::Union{T, Nothing} + timestamp::Timestamp +end + +mutable struct BranchSummaryMessage + role::String + summary::String + from_id::String + timestamp::Timestamp +end + +mutable struct CompactionSummaryMessage + role::String + summary::String + tokens_before::Int64 + timestamp::Timestamp +end + +# ============================================================================ +# Bash execution to text conversion +# ============================================================================ + +function bashExecutionToText(msg::BashExecutionMessage)::String + text = "Ran `$(msg.command)`\n" + if !isempty(msg.output) + text *= "```\n$(msg.output)\n```" + else + text *= "(no output)" + end + if msg.cancelled + text *= "\n\n(command cancelled)" + elseif !isnothing(msg.exit_code) && msg.exit_code != 0 + text *= "\n\nCommand exited with code $(msg.exit_code)" + end + if msg.truncated && !isnothing(msg.full_output_path) + text *= "\n\n[Output truncated. Full output: $(msg.full_output_path)]" + end + return text +end + +# ============================================================================ +# Message creation functions +# ============================================================================ + +function createBranchSummaryMessage(summary::String, from_id::String, timestamp::String)::BranchSummaryMessage + return BranchSummaryMessage( + "branchSummary", + summary, + from_id, + Int64(Dates.now(Dates.UTC).datetime), + ) +end + +function createCompactionSummaryMessage(summary::String, tokens_before::Int64, timestamp::String)::CompactionSummaryMessage + return CompactionSummaryMessage( + "compactionSummary", + summary, + tokens_before, + Int64(Dates.now(Dates.UTC).datetime), + ) +end + +function createCustomMessage(custom_type::String, content::Union{String, Vector{MessageContent}}, display::Bool, details::Union{Any, Nothing}, timestamp::String)::CustomMessage + return CustomMessage( + "custom", + custom_type, + content, + display, + details, + Int64(Dates.now(Dates.UTC).datetime), + ) +end + +# ============================================================================ +# Convert to LLM messages +# ============================================================================ + +function convertToLlm(messages::Vector{AgentMessage})::Vector{Message} + result::Vector{Message} = Message[] + + for m in messages + converted = convertToLlmMessage(m) + if !isnothing(converted) + push!(result, converted) + end + end + + return result +end + +function convertToLlmMessage(m::BashExecutionMessage)::Union{UserMessage, Nothing} + if m.exclude_from_context + return nothing + end + return UserMessage( + "user", + [TextContent(bashExecutionToText(m))], + m.timestamp, + ) +end + +function convertToLlmMessage(m::CustomMessage)::Union{UserMessage, Nothing} + content = if m.content isa String + [TextContent(m.content)] + else + m.content + end + return UserMessage("user", content, m.timestamp) +end + +function convertToLlmMessage(m::BranchSummaryMessage)::UserMessage + text = BRANCH_SUMMARY_PREFIX * m.summary * BRANCH_SUMMARY_SUFFIX + return UserMessage("user", [TextContent(text)], m.timestamp) +end + +function convertToLlmMessage(m::CompactionSummaryMessage)::UserMessage + text = COMPACTION_SUMMARY_PREFIX * m.summary * COMPACTION_SUMMARY_SUFFIX + return UserMessage("user", [TextContent(text)], m.timestamp) +end + +function convertToLlmMessage(m::UserMessage)::UserMessage + return m +end + +function convertToLlmMessage(m::AssistantMessage)::AssistantMessage + return m +end + +function convertToLlmMessage(m::ToolResultMessage)::ToolResultMessage + return m +end + +function convertToLlmMessage(m::AgentMessage)::Union{Message, Nothing} + return nothing +end + +end diff --git a/src/prompt_templates.jl b/src/prompt_templates.jl new file mode 100644 index 0000000..1defbdd --- /dev/null +++ b/src/prompt_templates.jl @@ -0,0 +1,335 @@ +""" + prompt_templates.jl - Prompt template loading and formatting + +This module provides utilities for loading prompt templates and formatting invocations. +""" + +module PromptTemplates + +using ..Types: * +using ..HarnessTypes: ExecutionEnv, toError, Result, ok, err + +# ============================================================================ +# Prompt template diagnostic types +# ============================================================================ + +const PromptTemplateDiagnosticCode = String +const PROMPT_TEMPLATE_DIAGNOSTIC_FILE_INFO_FAILED = "file_info_failed" +const PROMPT_TEMPLATE_DIAGNOSTIC_LIST_FAILED = "list_failed" +const PROMPT_TEMPLATE_DIAGNOSTIC_READ_FAILED = "read_failed" +const PROMPT_TEMPLATE_DIAGNOSTIC_PARSE_FAILED = "parse_failed" + +mutable struct PromptTemplateDiagnostic + type::String + code::PromptTemplateDiagnosticCode + message::String + path::String +end + +# ============================================================================ +# Prompt template frontmatter +# ============================================================================ + +mutable struct PromptTemplateFrontmatter + description::Union{String, Nothing} + argument_hint::Union{String, Nothing} + extra::Dict{String, Any} +end + +# ============================================================================ +# Load prompt templates from paths +# ============================================================================ + +function loadPromptTemplates( + env::ExecutionEnv, + paths::Union{String, Vector{String}}, +)::Tuple{Vector{PromptTemplate}, Vector{PromptTemplateDiagnostic}} + prompt_templates::Vector{PromptTemplate} = PromptTemplate[] + diagnostics::Vector{PromptTemplateDiagnostic} = PromptTemplateDiagnostic[] + + path_list = if paths isa String + [paths] + else + paths + end + + for path in path_list + info_result = fileInfo(env, path, nothing) + if !info_result.ok + if info_result.error.code != "not_found" + push!(diagnostics, PromptTemplateDiagnostic( + "warning", + "file_info_failed", + info_result.error.message, + path, + )) + end + continue + end + + info = info_result.value + kind = getFileKind(env, info, diagnostics) + + if kind == "directory" + result = loadTemplatesFromDir(env, info.path) + append!(prompt_templates, result.prompt_templates) + append!(diagnostics, result.diagnostics) + elseif kind == "file" && endswith(info.name, ".md") + result = loadTemplateFromFile(env, info.path) + if !isnothing(result.prompt_template) + push!(prompt_templates, result.prompt_template) + end + append!(diagnostics, result.diagnostics) + end + end + + return prompt_templates, diagnostics +end + +function getFileKind(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{PromptTemplateDiagnostic})::Union{String, Nothing} + if info.kind == "file" || info.kind == "directory" + return info.kind + end + + canonical_path = canonicalPath(env, info.path, nothing) + if !canonical_path.ok + if canonical_path.error.code != "not_found" + push!(diagnostics, PromptTemplateDiagnostic( + "warning", + "file_info_failed", + canonical_path.error.message, + info.path, + )) + end + return nothing + end + + target = fileInfo(env, canonical_path.value, nothing) + if !target.ok + if target.error.code != "not_found" + push!(diagnostics, PromptTemplateDiagnostic( + "warning", + "file_info_failed", + target.error.message, + info.path, + )) + end + return nothing + end + + if target.value.kind == "file" || target.value.kind == "directory" + return target.value.kind + end + + return nothing +end + +# ============================================================================ +# Load templates from directory +# ============================================================================ + +function loadTemplatesFromDir( + env::ExecutionEnv, + dir::String, +)::Tuple{Vector{PromptTemplate}, Vector{PromptTemplateDiagnostic}} + prompt_templates::Vector{PromptTemplate} = PromptTemplate[] + diagnostics::Vector{PromptTemplateDiagnostic} = PromptTemplateDiagnostic[] + + entries_result = listDir(env, dir, nothing) + if !entries_result.ok + push!(diagnostics, PromptTemplateDiagnostic( + "warning", + "list_failed", + entries_result.error.message, + dir, + )) + return prompt_templates, diagnostics + end + + entries = entries_result.value + + for entry in sort(entries, by=e -> e.name) + kind = getFileKind(env, entry, diagnostics) + if kind != "file" || !endswith(entry.name, ".md") + continue + end + + result = loadTemplateFromFile(env, entry.path) + if !isnothing(result.prompt_template) + push!(prompt_templates, result.prompt_template) + end + append!(diagnostics, result.diagnostics) + end + + return prompt_templates, diagnostics +end + +# ============================================================================ +# Load template from file +# ============================================================================ + +function loadTemplateFromFile( + env::ExecutionEnv, + file_path::String, +)::Tuple{Union{PromptTemplate, Nothing}, Vector{PromptTemplateDiagnostic}} + diagnostics::Vector{PromptTemplateDiagnostic} = PromptTemplateDiagnostic[] + + raw_content = readTextFile(env, file_path, nothing) + if !raw_content.ok + push!(diagnostics, PromptTemplateDiagnostic( + "warning", + "read_failed", + raw_content.error.message, + file_path, + )) + return nothing, diagnostics + end + + # TODO: Parse frontmatter + # parsed = parseFrontmatter(rawContent.value); + # if !parsed.ok { + # diagnostics.push({ + # type: "warning", + # code: "parse_failed", + # message: parsed.error.message, + # path: filePath, + # }); + # return { promptTemplate: null, diagnostics }; + # } + + # const { frontmatter, body } = parsed.value; + # const firstLine = body.split("\n").find((line) => line.trim()); + # let description = typeof frontmatter.description === "string" ? frontmatter.description : ""; + # if (!description && firstLine) { + # description = firstLine.slice(0, 60); + # if (firstLine.length > 60) description += "..."; + # } + + # return { + # promptTemplate: { + # name: basenameEnvPath(filePath).replace(/\.md$/i, ""), + # description, + # content: body, + # }, + # diagnostics, + # }; + + return nothing, diagnostics +end + +# ============================================================================ +# Parse command arguments +# ============================================================================ + +function parseCommandArgs(args_string::String)::Vector{String} + args::Vector{String} = String[] + current::String = "" + in_quote::Union{String, Nothing} = nothing + + for i in 1:length(args_string) + char = args_string[i] + if !isnothing(in_quote) + if char == in_quote + in_quote = nothing + else + current *= char + end + elseif char == '"' || char == '\'' + in_quote = char + elseif char == ' ' || char == '\t' + if !isempty(current) + push!(args, current) + current = "" + end + else + current *= char + end + end + + if !isempty(current) + push!(args, current) + end + + return args +end + +# ============================================================================ +# Substitute arguments +# ============================================================================ + +function substituteArgs(content::String, args::Vector{String})::String + result = content + + # Substitute $1, $2, etc. + result = replace(result, r"\$(\d+)" => s -> begin + idx = parse(Int, s[1]) + if idx > 0 && idx <= length(args) + return args[idx] + end + return "" + end) + + # Substitute ${@:N} and ${@:N:L} + result = replace(result, r"\$\{@:(\d+)(?::(\d+))?\}" => s -> begin + m = match(r"\$\{@:(\d+)(?::(\d+))?\}", s) + if !isnothing(m) + start = parse(Int, m.captures[1]) - 1 + if start < 0 + start = 0 + end + if !isnothing(m.captures[2]) + length = parse(Int, m.captures[2]) + return join(args[start+1:start+length], " ") + end + return join(args[start+1:end], " ") + end + return s + end) + + # Substitute $ARGUMENTS and $@ + all_args = join(args, " ") + result = replace(result, "$ARGUMENTS" => all_args) + result = replace(result, "$@" => all_args) + + return result +end + +# ============================================================================ +# Format prompt template invocation +# ============================================================================ + +function formatPromptTemplateInvocation(template::PromptTemplate, args::Vector{String}=String[])::String + return substituteArgs(template.content, args) +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function basenameEnvPath(path::String)::String + normalized = rtrim(path, '/') + slash_index = findlast('/', normalized) + if isnothing(slash_index) + return normalized + end + return normalized[slash_index+1:end] +end + +function findlast(pattern::Char, s::String)::Union{Int64, Nothing} + for i in length(s):-1:1 + if s[i] == pattern + return i + end + end + return nothing +end + +function rtrim(s::String, chars::String)::String + idx = length(s) + while idx >= 1 && s[idx] in chars + idx -= 1 + end + return s[1:idx] +end + +end diff --git a/src/session/jsonl_repo.jl b/src/session/jsonl_repo.jl new file mode 100644 index 0000000..1cc8a0d --- /dev/null +++ b/src/session/jsonl_repo.jl @@ -0,0 +1,148 @@ +""" + session/jsonl_repo.jl - JSONL session repository + +This module provides a JSONL-based session repository implementation. +""" + +module JsonlRepo + +using ..Types: * +using ..SessionStorage: SessionStorage, SessionMetadata +using ..JsonlStorage: JsonlSessionStorage, headerToSessionMetadata +using ..MemoryRepo: createSessionId, createTimestamp, getEntriesToFork, toSession +using ..HarnessTypes: SessionRepo, SessionForkOptions + +# ============================================================================ +# JSONL session repository +# ============================================================================ + +mutable struct JsonlSessionRepo <: SessionRepo{ + JsonlSessionMetadata, + JsonlSessionCreateOptions, + JsonlSessionListOptions +} + fs::Any + sessions_root_input::String + sessions_root::Union{String, Nothing} + + function JsonlSessionRepo(; sessions_root::String, fs::Any) + new(fs, sessions_root, nothing) + end +end + +# ============================================================================ +# Session repo methods +# ============================================================================ + +function create(repo::JsonlSessionRepo, options::JsonlSessionCreateOptions)::Session + id = if haskey(options, :id) && !isnothing(options[:id]) + options[:id] + else + createSessionId() + end + created_at = createTimestamp() + + session_dir = getSessionDir(repo, options.cwd) + + file_path = createSessionFilePath(repo, options.cwd, id, created_at) + + storage = JsonlSessionStorage( + file_path, + SessionHeader( + "session", + 3, + id, + created_at, + options.cwd, + get(options, :parentSessionPath, nothing), + get(options, :metadata, nothing), + ), + SessionTreeEntry[], + nothing, + ) + + return toSession(storage) +end + +function open(repo::JsonlSessionRepo, metadata::JsonlSessionMetadata)::Session + # TODO: Open existing file + return toSession(JsonlSessionStorage( + metadata.path, + SessionHeader( + "session", + 3, + metadata.id, + metadata.created_at, + metadata.cwd, + metadata.parent_session_path, + metadata.metadata, + ), + SessionTreeEntry[], + nothing, + )) +end + +function list(repo::JsonlSessionRepo, options::JsonlSessionListOptions=JsonlSessionListOptions())::Vector{JsonlSessionMetadata} + # TODO: List sessions + return JsonlSessionMetadata[] +end + +function delete(repo::JsonlSessionRepo, metadata::JsonlSessionMetadata)::Nothing + # TODO: Delete session file + return nothing +end + +function fork(repo::JsonlSessionRepo, source::JsonlSessionMetadata, options::Dict{String, Any})::Session + # TODO: Fork session + return create(repo, JsonlSessionCreateOptions( + cwd=get(options, "cwd", ""), + id=get(options, "id", createSessionId()), + )) +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function getSessionsRoot(repo::JsonlSessionRepo)::String + if isnothing(repo.sessions_root) + repo.sessions_root = getFileSystemResultOrThrow( + absolutePath(repo.fs, repo.sessions_root_input), + "Failed to resolve sessions root $(repo.sessions_root_input)", + ) + end + return repo.sessions_root +end + +function getSessionDir(repo::JsonlSessionRepo, cwd::String)::String + return getFileSystemResultOrThrow( + joinPath(repo.fs, [getSessionsRoot(repo), encodeCwd(cwd)]), + "Failed to resolve session directory for $(cwd)", + ) +end + +function encodeCwd(cwd::String)::String + result = replace(cwd, r"^[/\\]" => "") + result = replace(result, r"[/\\:]" => "-") + return "--$(result)--" +end + +function createSessionFilePath(repo::JsonlSessionRepo, cwd::String, session_id::String, timestamp::String)::String + return getFileSystemResultOrThrow( + joinPath(repo.fs, [ + getSessionDir(repo, cwd), + "$(replace(timestamp, r"[:.]" => "-"))_$(session_id).jsonl", + ]), + "Failed to resolve session file path for $(session_id)", + ) +end + +function getFileSystemResultOrThrow(result::Result, message::String) + if !result.ok + code = result.error.code == "not_found" ? "not_found" : "storage" + throw(SessionError(code, "$(message): $(result.error.message)", result.error)) + end + return result.value +end + +end diff --git a/src/session/jsonl_storage.jl b/src/session/jsonl_storage.jl new file mode 100644 index 0000000..8c9b88a --- /dev/null +++ b/src/session/jsonl_storage.jl @@ -0,0 +1,290 @@ +""" + session/jsonl_storage.jl - JSONL session storage + +This module provides JSONL-based session storage implementation. +""" + +module JsonlStorage + +using ..Types: * +using ..SessionStorage: SessionStorage, SessionMetadata + +# ============================================================================ +# Session header +# ============================================================================ + +mutable struct SessionHeader + type::String + version::Int64 + id::String + timestamp::String + cwd::String + parent_session::Union{String, Nothing} + metadata::Union{Dict{String, Any}, Nothing} +end + +# ============================================================================ +# JSONL session storage +# ============================================================================ + +mutable struct JsonlSessionStorage{T<:SessionMetadata} <: SessionStorage{T} + file_path::String + metadata::T + entries::Vector{SessionTreeEntry} + by_id::Dict{String, SessionTreeEntry} + labels_by_id::Dict{String, String} + current_leaf_id::Union{String, Nothing} + + function JsonlSessionStorage{T}( + file_path::String, + header::SessionHeader, + entries::Vector{SessionTreeEntry}, + leaf_id::Union{String, Nothing}, + ) where T + by_id = Dict{String, SessionTreeEntry}((e.id, e) for e in entries) + labels_by_id = Dict{String, String}() + + for entry in entries + if entry isa LabelEntry && !isnothing(entry.label) + labels_by_id[entry.target_id] = entry.label + end + end + + new( + file_path, + header, + entries, + by_id, + labels_by_id, + leaf_id, + ) + end +end + +# ============================================================================ +# Session storage methods +# ============================================================================ + +function getMetadata(storage::JsonlSessionStorage)::T + return storage.metadata +end + +function getLeafId(storage::JsonlSessionStorage)::Union{String, Nothing} + if !isnothing(storage.current_leaf_id) && !haskey(storage.by_id, storage.current_leaf_id) + throw(SessionError("invalid_session", "Entry $(storage.current_leaf_id) not found")) + end + return storage.current_leaf_id +end + +function setLeafId(storage::JsonlSessionStorage, leaf_id::Union{String, Nothing})::Nothing + if !isnothing(leaf_id) && !haskey(storage.by_id, leaf_id) + throw(SessionError("not_found", "Entry $(leaf_id) not found")) + end + + entry = LeafEntry( + "leaf", + generateEntryId(storage.by_id), + storage.current_leaf_id, + create_timestamp(), + leaf_id, + ) + + # TODO: Write to file + # getFileSystemResultOrThrow( + # await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`), + # `Failed to append session leaf ${entry.id}`, + # ); + + push!(storage.entries, entry) + storage.by_id[entry.id] = entry + storage.current_leaf_id = leaf_id + return nothing +end + +function createEntryId(storage::JsonlSessionStorage)::String + return generateEntryId(storage.by_id) +end + +function appendEntry(storage::JsonlSessionStorage, entry::SessionTreeEntry)::Nothing + # TODO: Write to file + # getFileSystemResultOrThrow( + # await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`), + # `Failed to append session entry ${entry.id}`, + # ); + + push!(storage.entries, entry) + storage.by_id[entry.id] = entry + + if entry isa LabelEntry + updateLabelCache(storage.labels_by_id, entry) + end + + storage.current_leaf_id = leafIdAfterEntry(entry) + return nothing +end + +function getEntry(storage::JsonlSessionStorage, id::String)::Union{SessionTreeEntry, Nothing} + return get(storage.by_id, id, nothing) +end + +function findEntries(storage::JsonlSessionStorage, type::String)::Vector{SessionTreeEntry} + return filter(entry -> entry.type == type, storage.entries) +end + +function getLabel(storage::JsonlSessionStorage, id::String)::Union{String, Nothing} + return get(storage.labels_by_id, id, nothing) +end + +function getSessionName(storage::JsonlSessionStorage)::Union{String, Nothing} + entries = findEntries(storage, "session_info") + if isempty(entries) + return nothing + end + return strip(entries[end].name) +end + +function getSessionStats(storage::JsonlSessionStorage)::SessionStats + message_count = 0 + cached_tokens = 0 + uncached_tokens = 0 + total_tokens = 0 + cost_total = 0.0 + + for entry in storage.entries + if entry isa MessageEntry + message_count += 1 + end + + usage = if entry isa MessageEntry && entry.message.role == "assistant" + entry.message.usage + elseif entry isa CompactionEntry || entry isa BranchSummaryEntry + entry.usage + else + nothing + end + + if !isnothing(usage) && + usage.input isa Int64 && + usage.output isa Int64 && + usage.cache_read isa Int64 && + usage.cache_write isa Int64 && + usage.cost.total isa Float64 + + cached_tokens += usage.cache_read + uncached_tokens += usage.input + usage.cache_write + total_tokens += usage.input + usage.output + usage.cache_read + usage.cache_write + cost_total += usage.cost.total + end + end + + return SessionStats( + message_count, + cached_tokens, + uncached_tokens, + total_tokens, + cost_total, + ) +end + +function getPathToRootOrCompaction(storage::JsonlSessionStorage, leaf_id::Union{String, Nothing})::Vector{SessionTreeEntry} + if isnothing(leaf_id) + return SessionTreeEntry[] + end + + path::Vector{SessionTreeEntry} = SessionTreeEntry[] + stop_at_entry_id::Union{String, Nothing} = nothing + current = get(storage.by_id, leaf_id, nothing) + + if isnothing(current) + throw(SessionError("not_found", "Entry $(leaf_id) not found")) + end + + while !isnothing(current) + unshift!(path, current) + + if !isnothing(stop_at_entry_id) && current.id == stop_at_entry_id + break + end + + if current isa CompactionEntry + if !isnothing(current.retained_tail) + break + end + stop_at_entry_id = current.first_kept_entry_id + end + + if isnothing(current.parent_id) + break + end + + parent = get(storage.by_id, current.parent_id, nothing) + if isnothing(parent) + throw(SessionError("invalid_session", "Entry $(current.parent_id) not found")) + end + + current = parent + end + + return path +end + +function getEntries(storage::JsonlSessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry} + start = get(options, "afterEntrySeq", 0) + end_idx = if haskey(options, "limit") + start + options["limit"] + else + nothing + end + + if isnothing(end_idx) + return copy(storage.entries[start+1:end]) + end + + return copy(storage.entries[start+1:end_idx]) +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function updateLabelCache(labels_by_id::Dict{String, String}, entry::SessionTreeEntry)::Nothing + if entry isa LabelEntry + label = strip(get(entry, :label, nothing)) + if !isnothing(label) && !isempty(label) + labels_by_id[entry.target_id] = label + else + delete!(labels_by_id, entry.target_id) + end + end + return nothing +end + +function generateEntryId(by_id::Dict{String, SessionTreeEntry})::String + for i in 1:100 + id = uuidv7()[end-7:end] + if !haskey(by_id, id) + return id + end + end + return uuidv7() +end + +function leafIdAfterEntry(entry::SessionTreeEntry)::Union{String, Nothing} + if entry isa LeafEntry + return entry.target_id + end + return entry.id +end + +function headerToSessionMetadata(header::SessionHeader, path::String)::JsonlSessionMetadata + return JsonlSessionMetadata( + header.id, + header.timestamp, + header.cwd, + path, + header.parent_session, + header.metadata, + ) +end + +end diff --git a/src/session/memory_repo.jl b/src/session/memory_repo.jl new file mode 100644 index 0000000..d901248 --- /dev/null +++ b/src/session/memory_repo.jl @@ -0,0 +1,133 @@ +""" + session/memory_repo.jl - In-memory session repository + +This module provides an in-memory session repository implementation for testing. +""" + +module MemoryRepo + +using ..Types: * +using ..SessionStorage: SessionStorage, SessionMetadata +using ..MemoryStorage: InMemorySessionStorage + +# ============================================================================ +# In-memory session repository +# ============================================================================ + +mutable struct InMemorySessionRepo <: SessionRepo{SessionMetadata, Dict{String, Any}, Nothing} + sessions::Dict{String, Session} + + function InMemorySessionRepo() + new(Dict{String, Session}()) + end +end + +# ============================================================================ +# Session repo methods +# ============================================================================ + +function create(repo::InMemorySessionRepo, options::Dict{String, Any}=Dict{String, Any}())::Session + metadata = SessionMetadata( + if haskey(options, :id) && !isnothing(options[:id]) + options[:id] + else + createSessionId() + end, + createTimestamp(), + ) + + storage = InMemorySessionStorage{SessionMetadata}(metadata=metadata) + session = toSession(storage) + + repo.sessions[metadata.id] = session + + return session +end + +function open(repo::InMemorySessionRepo, metadata::SessionMetadata)::Session + session = get(repo.sessions, metadata.id, nothing) + if isnothing(session) + throw(SessionError("not_found", "Session not found: $(metadata.id)")) + end + return session +end + +function list(repo::InMemorySessionRepo)::Vector{SessionMetadata} + return [getMetadata(session) for session in values(repo.sessions)] +end + +function delete(repo::InMemorySessionRepo, metadata::SessionMetadata)::Nothing + delete!(repo.sessions, metadata.id) + return nothing +end + +function fork(repo::InMemorySessionRepo, source::SessionMetadata, options::Dict{String, Any})::Session + source_session = open(repo, source) + forked_entries = getEntriesToFork(getStorage(source_session), options) + + metadata = SessionMetadata( + if haskey(options, :id) && !isnothing(options[:id]) + options[:id] + else + createSessionId() + end, + createTimestamp(), + ) + + storage = InMemorySessionStorage{SessionMetadata}( + entries=forked_entries, + metadata=metadata, + ) + + session = toSession(storage) + repo.sessions[metadata.id] = session + + return session +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function createSessionId()::String + return uuidv7() +end + +function createTimestamp()::String + return create_timestamp() +end + +function toSession(storage::SessionStorage)::Session + return Session(storage) +end + +function getEntriesToFork(storage::SessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry} + if !haskey(options, :entryId) || isnothing(options[:entryId]) + return getEntries(storage, Dict{String, Any}()) + end + + target = getEntry(storage, options[:entryId]) + if isnothing(target) + throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) not found")) + end + + effective_leaf_id::Union{String, Nothing} + position = get(options, "position", "before") + + if position == "at" + effective_leaf_id = target.id + else + if target isa MessageEntry && target.message.role != "user" + throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) is not a user message")) + end + effective_leaf_id = target.parent_id + end + + return getPathToRootOrCompaction(storage, effective_leaf_id) +end + +function getStorage(session::Session)::SessionStorage + return session.storage +end + +end diff --git a/src/session/memory_storage.jl b/src/session/memory_storage.jl new file mode 100644 index 0000000..235efc1 --- /dev/null +++ b/src/session/memory_storage.jl @@ -0,0 +1,227 @@ +""" + session/memory_storage.jl - In-memory session storage + +This module provides an in-memory session storage implementation for testing and temporary use. +""" + +module MemoryStorage + +using ..Types: * +using ..SessionStorage: SessionStorage, SessionMetadata +using ..JsonlStorage: updateLabelCache, generateEntryId, leafIdAfterEntry + +# ============================================================================ +# In-memory session storage +# ============================================================================ + +mutable struct InMemorySessionStorage{T<:SessionMetadata} <: SessionStorage{T} + metadata::T + entries::Vector{SessionTreeEntry} + by_id::Dict{String, SessionTreeEntry} + labels_by_id::Dict{String, String} + leaf_id::Union{String, Nothing} + + function InMemorySessionStorage{T}(; + entries::Vector{SessionTreeEntry}=SessionTreeEntry[], + metadata::Union{T, Nothing]=nothing, + ) where T + by_id = Dict{String, SessionTreeEntry}((e.id, e) for e in entries) + labels_by_id = Dict{String, String}() + + leaf_id = nothing + for entry in entries + if entry isa LabelEntry + updateLabelCache(labels_by_id, entry) + end + leaf_id = leafIdAfterEntry(entry) + end + + if !isnothing(leaf_id) && !haskey(by_id, leaf_id) + throw(SessionError("invalid_session", "Entry $(leaf_id) not found")) + end + + new( + if isnothing(metadata) + T(uuidv7(), create_timestamp()) + else + metadata + end, + copy(entries), + by_id, + labels_by_id, + leaf_id, + ) + end +end + +# ============================================================================ +# Session storage methods +# ============================================================================ + +function getMetadata(storage::InMemorySessionStorage)::T + return storage.metadata +end + +function getLeafId(storage::InMemorySessionStorage)::Union{String, Nothing} + if !isnothing(storage.leaf_id) && !haskey(storage.by_id, storage.leaf_id) + throw(SessionError("invalid_session", "Entry $(storage.leaf_id) not found")) + end + return storage.leaf_id +end + +function setLeafId(storage::InMemorySessionStorage, leaf_id::Union{String, Nothing})::Nothing + if !isnothing(leaf_id) && !haskey(storage.by_id, leaf_id) + throw(SessionError("not_found", "Entry $(leaf_id) not found")) + end + + entry = LeafEntry( + "leaf", + generateEntryId(storage.by_id), + storage.leaf_id, + create_timestamp(), + leaf_id, + ) + + push!(storage.entries, entry) + storage.by_id[entry.id] = entry + storage.leaf_id = leaf_id + return nothing +end + +function createEntryId(storage::InMemorySessionStorage)::String + return generateEntryId(storage.by_id) +end + +function appendEntry(storage::InMemorySessionStorage, entry::SessionTreeEntry)::Nothing + push!(storage.entries, entry) + storage.by_id[entry.id] = entry + + if entry isa LabelEntry + updateLabelCache(storage.labels_by_id, entry) + end + + storage.leaf_id = leafIdAfterEntry(entry) + return nothing +end + +function getEntry(storage::InMemorySessionStorage, id::String)::Union{SessionTreeEntry, Nothing} + return get(storage.by_id, id, nothing) +end + +function findEntries(storage::InMemorySessionStorage, type::String)::Vector{SessionTreeEntry} + return filter(entry -> entry.type == type, storage.entries) +end + +function getLabel(storage::InMemorySessionStorage, id::String)::Union{String, Nothing} + return get(storage.labels_by_id, id, nothing) +end + +function getSessionName(storage::InMemorySessionStorage)::Union{String, Nothing} + entries = findEntries(storage, "session_info") + if isempty(entries) + return nothing + end + return strip(entries[end].name) +end + +function getSessionStats(storage::InMemorySessionStorage)::SessionStats + message_count = 0 + cached_tokens = 0 + uncached_tokens = 0 + total_tokens = 0 + cost_total = 0.0 + + for entry in storage.entries + if entry isa MessageEntry + message_count += 1 + end + + usage = if entry isa MessageEntry && entry.message.role == "assistant" + entry.message.usage + elseif entry isa CompactionEntry || entry isa BranchSummaryEntry + entry.usage + else + nothing + end + + if !isnothing(usage) && + usage.input isa Int64 && + usage.output isa Int64 && + usage.cache_read isa Int64 && + usage.cache_write isa Int64 && + usage.cost.total isa Float64 + + cached_tokens += usage.cache_read + uncached_tokens += usage.input + usage.cache_write + total_tokens += usage.input + usage.output + usage.cache_read + usage.cache_write + cost_total += usage.cost.total + end + end + + return SessionStats( + message_count, + cached_tokens, + uncached_tokens, + total_tokens, + cost_total, + ) +end + +function getPathToRootOrCompaction(storage::InMemorySessionStorage, leaf_id::Union{String, Nothing})::Vector{SessionTreeEntry} + if isnothing(leaf_id) + return SessionTreeEntry[] + end + + path::Vector{SessionTreeEntry} = SessionTreeEntry[] + stop_at_entry_id::Union{String, Nothing} = nothing + current = get(storage.by_id, leaf_id, nothing) + + if isnothing(current) + throw(SessionError("not_found", "Entry $(leaf_id) not found")) + end + + while !isnothing(current) + unshift!(path, current) + + if !isnothing(stop_at_entry_id) && current.id == stop_at_entry_id + break + end + + if current isa CompactionEntry + if !isnothing(current.retained_tail) + break + end + stop_at_entry_id = current.first_kept_entry_id + end + + if isnothing(current.parent_id) + break + end + + parent = get(storage.by_id, current.parent_id, nothing) + if isnothing(parent) + throw(SessionError("invalid_session", "Entry $(current.parent_id) not found")) + end + + current = parent + end + + return path +end + +function getEntries(storage::InMemorySessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry} + start = get(options, "afterEntrySeq", 0) + end_idx = if haskey(options, "limit") + start + options["limit"] + else + nothing + end + + if isnothing(end_idx) + return copy(storage.entries[start+1:end]) + end + + return copy(storage.entries[start+1:end_idx]) +end + +end diff --git a/src/session/repo_utils.jl b/src/session/repo_utils.jl new file mode 100644 index 0000000..fb79916 --- /dev/null +++ b/src/session/repo_utils.jl @@ -0,0 +1,65 @@ +""" + session/repo_utils.jl - Session repository utilities + +This module provides shared utilities for session repository implementations. +""" + +module RepoUtils + +using ..Types: * +using ..SessionStorage: SessionStorage, SessionMetadata +using ..Session: Session + +# ============================================================================ +# Helper functions +# ============================================================================ + +function createSessionId()::String + return uuidv7() +end + +function createTimestamp()::String + return create_timestamp() +end + +function toSession{T<:SessionMetadata}(storage::SessionStorage{T})::Session{T} + return Session(storage) +end + +function getFileSystemResultOrThrow{TValue}(result::Result{TValue, FileError}, message::String)::TValue + if !result.ok + code = result.error.code == "not_found" ? "not_found" : "storage" + throw(SessionError(code, "$(message): $(result.error.message)", result.error)) + end + return result.value +end + +function getEntriesToFork( + storage::SessionStorage, + options::Dict{String, Any}, +)::Vector{SessionTreeEntry} + if !haskey(options, :entryId) || isnothing(options[:entryId]) + return getEntries(storage, Dict{String, Any}()) + end + + target = getEntry(storage, options[:entryId]) + if isnothing(target) + throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) not found")) + end + + effective_leaf_id::Union{String, Nothing} + position = get(options, "position", "before") + + if position == "at" + effective_leaf_id = target.id + else + if target isa MessageEntry && target.message.role != "user" + throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) is not a user message")) + end + effective_leaf_id = target.parent_id + end + + return getPathToRootOrCompaction(storage, effective_leaf_id) +end + +end diff --git a/src/session/session.jl b/src/session/session.jl new file mode 100644 index 0000000..72efce9 --- /dev/null +++ b/src/session/session.jl @@ -0,0 +1,422 @@ +""" + session/session.jl - Session management + +This module provides the Session class for managing conversation history with branch support. +""" + +module Session + +using ..Types: * +using ..SessionStorage: SessionStorage +using ..Messages: * +using ..HarnessTypes: * + +# ============================================================================ +# Session context build options +# ============================================================================ + +mutable struct SessionContextBuildOptions + entry_transforms::Union{Vector{Function}, Nothing} + entry_projectors::Union{Dict{String, Function}, Nothing} +end + +# ============================================================================ +# Default context entry transform +# ============================================================================ + +function defaultContextEntryTransform(path_entries::Vector{SessionTreeEntry})::Vector{SessionTreeEntry} + compaction = nothing + for entry in path_entries + if entry isa CompactionEntry + compaction = entry + break + end + end + + if isnothing(compaction) + return copy(path_entries) + end + + entries::Vector{SessionTreeEntry} = [compaction] + compaction_idx = findfirst( + (entry) -> entry isa CompactionEntry && entry.id == compaction.id, + path_entries, + ) + + if !isnothing(compaction.retained_tail) + for i in compaction_idx+1:length(path_entries) + push!(entries, path_entries[i]) + end + return entries + end + + if !isnothing(compaction.first_kept_entry_id) + found_first_kept = false + for i in 1:compaction_idx-1 + entry = path_entries[i] + if entry.id == compaction.first_kept_entry_id + found_first_kept = true + end + if found_first_kept + push!(entries, entry) + end + end + end + + for i in compaction_idx+1:length(path_entries) + push!(entries, path_entries[i]) + end + + return entries +end + +# ============================================================================ +# Build context entries +# ============================================================================ + +function buildContextEntries( + path_entries::Vector{SessionTreeEntry}, + options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), +)::Vector{SessionTreeEntry} + entries = defaultContextEntryTransform(path_entries) + + if !isnothing(options.entry_transforms) + for transform in options.entry_transforms + entries = transform(entries) + end + end + + return entries +end + +# ============================================================================ +# Session entry to context messages +# ============================================================================ + +function sessionEntryToContextMessages( + entry::SessionTreeEntry, + index::Int64, + entries::Vector{SessionTreeEntry}, + options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), +)::Vector{AgentMessage} + if entry isa MessageEntry + return [entry.message] + end + + if entry isa CustomMessageEntry + return [createCustomMessage( + entry.custom_type, + entry.content, + entry.display, + entry.details, + entry.timestamp, + )] + end + + if entry isa CompactionEntry + messages = [createCompactionSummaryMessage( + entry.summary, + entry.tokens_before, + entry.timestamp, + )] + if !isnothing(entry.retained_tail) + append!(messages, entry.retained_tail) + end + return messages + end + + if entry isa BranchSummaryEntry + return [createBranchSummaryMessage( + entry.summary, + entry.from_id, + entry.timestamp, + )] + end + + if entry isa CustomEntry + if !isnothing(options.entry_projectors) && haskey(options.entry_projectors, entry.custom_type) + projector = options.entry_projectors[entry.custom_type] + return projector(entry, index, entries) + end + return AgentMessage[] + end + + return AgentMessage[] +end + +# ============================================================================ +# Build session context +# ============================================================================ + +function buildSessionContext( + path_entries::Vector{SessionTreeEntry}, + options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), +)::SessionContext + state = deriveSessionContextState(path_entries) + context_entries = buildContextEntries(path_entries, options) + messages = SessionTreeEntry[] + for (i, entry) in enumerate(context_entries) + append!(messages, sessionEntryToContextMessages(entry, i, context_entries, options)) + end + return SessionContext(messages, state.thinking_level, state.model, state.active_tool_names) +end + +function deriveSessionContextState(path_entries::Vector{SessionTreeEntry})::Dict{String, Any} + thinking_level = "off" + model = nothing + active_tool_names = nothing + + for entry in path_entries + if entry isa ThinkingLevelChangeEntry + thinking_level = entry.thinking_level + elseif entry isa ModelChangeEntry + model = Dict{String, String}("provider" => entry.provider, "modelId" => entry.model_id) + elseif entry isa MessageEntry && entry.message.role == "assistant" + model = Dict{String, String}("provider" => entry.message.provider, "modelId" => entry.message.model) + elseif entry isa ActiveToolsChangeEntry + active_tool_names = copy(entry.active_tool_names) + end + end + + return Dict{String, Any}( + "thinking_level" => thinking_level, + "model" => model, + "active_tool_names" => active_tool_names, + ) +end + +# ============================================================================ +# Session class +# ============================================================================ + +mutable struct Session{T<:SessionMetadata} + storage::SessionStorage{T} + context_build_options::SessionContextBuildOptions + + function Session( + storage::SessionStorage, + context_build_options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), + ) + new{typeof(storage.metadata)}(storage, context_build_options) + end +end + +# ============================================================================ +# Session methods +# ============================================================================ + +function getMetadata(session::Session)::T + return getMetadata(session.storage) +end + +function getStorage(session::Session)::SessionStorage + return session.storage +end + +function getLeafId(session::Session)::Union{String, Nothing} + return getLeafId(session.storage) +end + +function getEntry(session::Session, id::String)::Union{SessionTreeEntry, Nothing} + return getEntry(session.storage, id) +end + +function getEntries(session::Session, options::Dict{String, Any}=Dict{String, Any}())::Vector{SessionTreeEntry} + return getEntries(session.storage, options) +end + +function getBranch(session::Session, from_id::Union{String, Nothing}=nothing)::Vector{SessionTreeEntry} + leaf_id = if isnothing(from_id) + getLeafId(session.storage) + else + from_id + end + return getPathToRootOrCompaction(session.storage, leaf_id) +end + +function buildContextEntries(session::Session, options::SessionContextBuildOptions=SessionContextBuildOptions())::Vector{SessionTreeEntry} + return buildContextEntries(getBranch(session), mergeContextBuildOptions(session, options)) +end + +function buildContext(session::Session, options::SessionContextBuildOptions=SessionContextBuildOptions())::SessionContext + return buildSessionContext(getBranch(session), mergeContextBuildOptions(session, options)) +end + +function mergeContextBuildOptions(session::Session, options::SessionContextBuildOptions)::SessionContextBuildOptions + return SessionContextBuildOptions( + vcat( + isnothing(session.context_build_options.entry_transforms) ? [] : session.context_build_options.entry_transforms, + isnothing(options.entry_transforms) ? [] : options.entry_transforms, + ), + merge( + isnothing(session.context_build_options.entry_projectors) ? Dict{String, Any}() : session.context_build_options.entry_projectors, + isnothing(options.entry_projectors) ? Dict{String, Any}() : options.entry_projectors, + promote=true, + ), + ) +end + +function getLabel(session::Session, id::String)::Union{String, Nothing} + return getLabel(session.storage, id) +end + +function getSessionStats(session::Session)::SessionStats + return getSessionStats(session.storage) +end + +function getSessionName(session::Session)::Union{String, Nothing} + return getSessionName(session.storage) +end + +function appendMessage(session::Session, message::AgentMessage)::String + return appendTypedEntry(session, MessageEntry( + "message", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + message, + )) +end + +function appendThinkingLevelChange(session::Session, thinking_level::String)::String + return appendTypedEntry(session, ThinkingLevelChangeEntry( + "thinking_level_change", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + thinking_level, + )) +end + +function appendModelChange(session::Session, provider::String, model_id::String)::String + return appendTypedEntry(session, ModelChangeEntry( + "model_change", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + provider, + model_id, + )) +end + +function appendActiveToolsChange(session::Session, active_tool_names::Vector{String})::String + return appendTypedEntry(session, ActiveToolsChangeEntry( + "active_tools_change", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + active_tool_names, + )) +end + +function appendCompaction( + session::Session, + summary::String, + first_kept_entry_id::Union{String, Nothing}, + tokens_before::Int64, + details::Union{Any, Nothing}=nothing, + from_hook::Bool=false, + usage::Union{Usage, Nothing}=nothing, + retained_tail::Union{Vector{AgentMessage}, Nothing}=nothing, +)::String + return appendTypedEntry(session, CompactionEntry( + "compaction", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + summary, + first_kept_entry_id, + tokens_before, + retained_tail, + details, + usage, + from_hook, + )) +end + +function appendCustomEntry(session::Session, custom_type::String, data::Union{Any, Nothing}=nothing)::String + return appendTypedEntry(session, CustomEntry( + "custom", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + custom_type, + data, + )) +end + +function appendCustomMessageEntry( + session::Session, + custom_type::String, + content::String, + display::Bool, + details::Union{Any, Nothing}=nothing, +)::String + return appendTypedEntry(session, CustomMessageEntry( + "custom_message", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + custom_type, + content, + details, + display, + )) +end + +function appendLabel(session::Session, target_id::String, label::Union{String, Nothing})::String + if isnothing(getEntry(session, target_id)) + throw(SessionError("not_found", "Entry $(target_id) not found")) + end + return appendTypedEntry(session, LabelEntry( + "label", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + target_id, + label, + )) +end + +function appendSessionName(session::Session, name::String)::String + sanitizedName = replace(name, r"[\r\n]+" => " ") + return appendTypedEntry(session, SessionInfoEntry( + "session_info", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + sanitizedName, + )) +end + +function moveTo( + session::Session, + entry_id::Union{String, Nothing}, + summary::Union{Dict{String, Any}, Nothing}=nothing, +)::Union{String, Nothing + if !isnothing(entry_id) && isnothing(getEntry(session, entry_id)) + throw(SessionError("not_found", "Entry $(entry_id) not found")) + end + setLeafId(session.storage, entry_id) + if isnothing(summary) + return nothing + end + return appendTypedEntry(session, BranchSummaryEntry( + "branch_summary", + createEntryId(session.storage), + entry_id, + create_timestamp(), + entry_id, + summary["summary"], + get(summary, "details", nothing), + get(summary, "usage", nothing), + get(summary, "from_hook", false), + )) +end + +function appendTypedEntry(session::Session, entry::SessionTreeEntry)::String + appendEntry(session.storage, entry) + return entry.id +end + +end diff --git a/src/skills.jl b/src/skills.jl new file mode 100644 index 0000000..ae83a7f --- /dev/null +++ b/src/skills.jl @@ -0,0 +1,375 @@ +""" + skills.jl - Skill loading and formatting + +This module provides utilities for loading skills from SKILL.md files and formatting skill invocations. +""" + +module Skills + +using ..Types: * +using ..HarnessTypes: Skill, ExecutionEnv, FileSystem, toError, FileError, Result, ok, err + +const MAX_NAME_LENGTH = 64 +const MAX_DESCRIPTION_LENGTH = 1024 +const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"] + +# ============================================================================ +# Skill diagnostic types +# ============================================================================ + +const SkillDiagnosticCode = String +const SKILL_DIAGNOSTIC_FILE_INFO_FAILED = "file_info_failed" +const SKILL_DIAGNOSTIC_LIST_FAILED = "list_failed" +const SKILL_DIAGNOSTIC_READ_FAILED = "read_failed" +const SKILL_DIAGNOSTIC_PARSE_FAILED = "parse_failed" +const SKILL_DIAGNOSTIC_INVALID_METADATA = "invalid_metadata" + +mutable struct SkillDiagnostic + type::String + code::SkillDiagnosticCode + message::String + path::String +end + +# ============================================================================ +# Skill frontmatter +# ============================================================================ + +mutable struct SkillFrontmatter + name::Union{String, Nothing} + description::Union{String, Nothing} + disable_model_invocation::Union{Bool, Nothing} + extra::Dict{String, Any} +end + +# ============================================================================ +# Format skill invocation +# ============================================================================ + +function formatSkillInvocation(skill::Skill, additional_instructions::Union{String, Nothing})::String + skill_block = "\nReferences are relative to $(dirnameEnvPath(skill.filePath)).\n\n$(skill.content)\n" + if isnothing(additional_instructions) + return skill_block + end + return "$(skill_block)\n\n$(additional_instructions)" +end + +# ============================================================================ +# Load skills from directories +# ============================================================================ + +function loadSkills(env::ExecutionEnv, dirs::Union{String, Vector{String}})::Tuple{Vector{Skill}, Vector{SkillDiagnostic}} + skills::Vector{Skill} = Skill[] + diagnostics::Vector{SkillDiagnostic} = SkillDiagnostic[] + + dir_list = if dirs isa String + [dirs] + else + dirs + end + + for dir in dir_list + root_info_result = fileInfo(env, dir, nothing) + if !root_info_result.ok + if root_info_result.error.code != "not_found" + push!(diagnostics, SkillDiagnostic( + "warning", + "file_info_failed", + root_info_result.error.message, + dir, + )) + end + continue + end + + root_info = root_info_result.value + if !isDirectory(env, root_info, diagnostics) + continue + end + + result = loadSkillsFromDirInternal(env, root_info.path, true, Dict{String, Any}(), root_info.path) + append!(skills, result.skills) + append!(diagnostics, result.diagnostics) + end + + return skills, diagnostics +end + +function isDirectory(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{SkillDiagnostic})::Bool + return info.kind == "directory" +end + +function loadSkillsFromDirInternal( + env::ExecutionEnv, + dir::String, + include_root_files::Bool, + ignore_matcher::Dict{String, Any}, + root_dir::String, +)::Tuple{Vector{Skill}, Vector{SkillDiagnostic}} + skills::Vector{Skill} = Skill[] + diagnostics::Vector{SkillDiagnostic} = SkillDiagnostic[] + + dir_info_result = fileInfo(env, dir, nothing) + if !dir_info_result.ok + if dir_info_result.error.code != "not_found" + push!(diagnostics, SkillDiagnostic( + "warning", + "file_info_failed", + dir_info_result.error.message, + dir, + )) + end + return skills, diagnostics + end + + dir_info = dir_info_result.value + if !isDirectory(env, dir_info, diagnostics) + return skills, diagnostics + end + + # TODO: Implement ignore rules + # await addIgnoreRules(env, ignoreMatcher, dir, rootDir, diagnostics); + + entries_result = listDir(env, dir, nothing) + if !entries_result.ok + push!(diagnostics, SkillDiagnostic( + "warning", + "list_failed", + entries_result.error.message, + dir, + )) + return skills, diagnostics + end + + entries = entries_result.value + + # Look for SKILL.md + for entry in entries + if entry.name != "SKILL.md" + continue + end + + full_path = entry.path + if !isFile(env, entry, diagnostics) + continue + end + + result = loadSkillFromFile(env, full_path) + if !isnothing(result.skill) + push!(skills, result.skill) + end + append!(diagnostics, result.diagnostics) + return skills, diagnostics + end + + # Process other files + for entry in sort(entries, by=e -> e.name) + if startswith(entry.name, ".") || entry.name == "node_modules" + continue + end + + full_path = entry.path + kind = getFileKind(env, entry, diagnostics) + if isnothing(kind) + continue + end + + rel_path = relativeEnvPath(root_dir, full_path) + ignore_path = kind == "directory" ? "$(rel_path)/" : rel_path + + if !isnothing(ignore_matcher) && haskey(ignore_matcher, ignore_path) + continue + end + + if kind == "directory" + result = loadSkillsFromDirInternal(env, full_path, false, ignore_matcher, root_dir) + append!(skills, result.skills) + append!(diagnostics, result.diagnostics) + continue + end + + if kind != "file" || !include_root_files || !endswith(entry.name, ".md") + continue + end + + result = loadSkillFromFile(env, full_path) + if !isnothing(result.skill) + push!(skills, result.skill) + end + append!(diagnostics, result.diagnostics) + end + + return skills, diagnostics +end + +function isFile(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{SkillDiagnostic})::Bool + return info.kind == "file" +end + +function getFileKind(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{SkillDiagnostic})::Union{String, Nothing} + if info.kind == "file" || info.kind == "directory" + return info.kind + end + + canonical_path = canonicalPath(env, info.path, nothing) + if !canonical_path.ok + if canonical_path.error.code != "not_found" + push!(diagnostics, SkillDiagnostic( + "warning", + "file_info_failed", + canonical_path.error.message, + info.path, + )) + end + return nothing + end + + target = fileInfo(env, canonical_path.value, nothing) + if !target.ok + if target.error.code != "not_found" + push!(diagnostics, SkillDiagnostic( + "warning", + "file_info_failed", + target.error.message, + info.path, + )) + end + return nothing + end + + if target.value.kind == "file" || target.value.kind == "directory" + return target.value.kind + end + + return nothing +end + +# ============================================================================ +# Load skill from file +# ============================================================================ + +function loadSkillFromFile(env::ExecutionEnv, file_path::String)::Tuple{Union{Skill, Nothing}, Vector{SkillDiagnostic}} + diagnostics::Vector{SkillDiagnostic} = SkillDiagnostic[] + + raw_content = readTextFile(env, file_path, nothing) + if !raw_content.ok + push!(diagnostics, SkillDiagnostic( + "warning", + "read_failed", + raw_content.error.message, + file_path, + )) + return nothing, diagnostics + end + + # TODO: Parse frontmatter + # parsed = parseFrontmatter(rawContent.value); + # if !parsed.ok { + # diagnostics.push({ type: "warning", code: "parse_failed", message: parsed.error.message, path: filePath }); + # return { skill: null, diagnostics }; + # } + + # const { frontmatter, body } = parsed.value; + # const skillDir = dirnameEnvPath(filePath); + # const parentDirName = basenameEnvPath(skillDir); + # const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined; + + # for (const error of validateDescription(description)) { + # diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath }); + # } + + # const frontmatterName = typeof frontmatter.name === "string" ? frontmatter.name : undefined; + # const name = frontmatterName || parentDirName; + # for (const error of validateName(name, parentDirName)) { + # diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath }); + # } + + # if (!description || description.trim() === "") { + # return { skill: null, diagnostics }; + # } + + # return { + # skill: { + # name, + # description, + # content: body, + # filePath, + # disableModelInvocation: frontmatter["disable-model-invocation"] === true, + # }, + # diagnostics, + # }; + + return nothing, diagnostics +end + +# ============================================================================ +# Path utility functions +# ============================================================================ + +function joinEnvPath(base::String, child::String)::String + return "$(rtrim(base, '/'))/$(ltrim(child, '/'))" +end + +function dirnameEnvPath(path::String)::String + normalized = rtrim(path, '/') + slash_index = findlast('/', normalized) + if isnothing(slash_index) || slash_index <= 1 + return "/" + end + return normalized[1:slash_index-1] +end + +function basenameEnvPath(path::String)::String + normalized = rtrim(path, '/') + slash_index = findlast('/', normalized) + if isnothing(slash_index) + return normalized + end + return normalized[slash_index+1:end] +end + +function relativeEnvPath(root::String, path::String)::String + normalized_root = rtrim(root, '/') + normalized_path = rtrim(path, '/') + + if normalized_path == normalized_root + return "" + end + + if startswith(normalized_path, "$(normalized_root)/") + return normalized_path[length(normalized_root)+2:end] + end + + return lstrip(normalized_path, '/') +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function lstrip(s::String, chars::String)::String + idx = 1 + while idx <= length(s) && s[idx] in chars + idx += 1 + end + return s[idx:end] +end + +function rtrim(s::String, chars::String)::String + idx = length(s) + while idx >= 1 && s[idx] in chars + idx -= 1 + end + return s[1:idx] +end + +function findlast(pattern::Char, s::String)::Union{Int64, Nothing} + for i in length(s):-1:1 + if s[i] == pattern + return i + end + end + return nothing +end + +end diff --git a/src/stream_fn.jl b/src/stream_fn.jl new file mode 100644 index 0000000..c78481f --- /dev/null +++ b/src/stream_fn.jl @@ -0,0 +1,45 @@ +""" + stream_fn.jl - Stream function utilities + +This module provides the default stream function configuration for AgentCore. +""" + +module StreamFn + +using ..Types: StreamFn + +let default_stream_fn::Union{StreamFn, Nothing} = nothing + +""" + setDefaultStreamFn(stream_fn) + +Configure the fallback used by Agent and low-level loops when callers omit stream_fn. + +# Arguments +- `stream_fn`: The stream function to set as default +""" +function setDefaultStreamFn(stream_fn::Union{StreamFn, Nothing}) + global default_stream_fn = stream_fn +end + +""" + getDefaultStreamFn() + +Get the configured default stream function, or throw an error if none is configured. + +# Returns +- The configured stream function + +# Throws +- ErrorException if no default stream function is configured +""" +function getDefaultStreamFn()::StreamFn + if isnothing(default_stream_fn) + throw(ErrorException( + "No default stream function configured. Pass stream_fn explicitly or call setDefaultStreamFn()." + )) + end + return default_stream_fn +end + +end diff --git a/src/system_prompt.jl b/src/system_prompt.jl new file mode 100644 index 0000000..ba1a4e8 --- /dev/null +++ b/src/system_prompt.jl @@ -0,0 +1,56 @@ +""" + system_prompt.jl - System prompt formatting + +This module provides utilities for formatting skills in the system prompt. +""" + +module SystemPrompt + +using ..Types: Skill + +""" + formatSkillsForSystemPrompt(skills) + +Format skills for inclusion in the system prompt using XML-formatted blocks. +""" +function formatSkillsForSystemPrompt(skills::Vector{Skill})::String + visible_skills = filter(s -> !s.disableModelInvocation, skills) + if isempty(visible_skills) + return "" + end + + lines = String[ + "The following skills provide specialized instructions for specific tasks.", + "Read the full skill file when the task matches its description.", + "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.", + "", + "", + ] + + for skill in visible_skills + push!(lines, " ") + push!(lines, " $(escapeXml(skill.name))") + push!(lines, " $(escapeXml(skill.description))") + push!(lines, " $(escapeXml(skill.filePath))") + push!(lines, " ") + end + + push!(lines, "") + return join(lines, "\n") +end + +""" + escapeXml(value) + +Escape special characters in a string for XML. +""" +function escapeXml(value::String)::String + result = replace(value, "&" => "&") + result = replace(result, "<" => "<") + result = replace(result, ">" => ">") + result = replace(result, "\"" => """) + result = replace(result, "'" => "'") + return result +end + +end diff --git a/src/tools/bash.jl b/src/tools/bash.jl new file mode 100644 index 0000000..ab4e3b6 --- /dev/null +++ b/src/tools/bash.jl @@ -0,0 +1,49 @@ +""" + tools/bash.jl - Bash execution tool + +This module provides the bash execution tool for AgentCore. +""" + +module Bash + +using ..Types: * + +struct BashExecution + command::String + cwd::String + env::Dict{String, String} + inherit_env::Bool +end + +mutable struct BashPrepare{TContext} + function::Function + context::TContext + signal::Union{Any, Nothing} +end + +mutable struct BashToolOptions{TContext} + command_prefix::Union{String, Nothing} + prepare::Union{BashPrepare{TContext}, Nothing} +end + +mutable struct BashToolDetails + truncation::Union{Any, Nothing} + full_output_path::Union{String, Nothing} +end + +function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing) where TContext + return AgentTool( + "bash", + "bash", + "Execute a bash command in the current working directory.", + Dict{String, Any}(), + (tool_call_id, params, signal, on_update, context) -> begin + # TODO: Implement bash execution + return AgentToolResult([TextContent("Command executed successfully")], nothing, nothing, nothing, nothing) + end, + nothing, + nothing, + ) +end + +end diff --git a/src/tools/edit.jl b/src/tools/edit.jl new file mode 100644 index 0000000..8b00405 --- /dev/null +++ b/src/tools/edit.jl @@ -0,0 +1,32 @@ +""" + tools/edit.jl - File edit tool + +This module provides the file edit tool for AgentCore. +""" + +module Edit + +using ..Types: * + +mutable struct EditToolDetails + diff::String + patch::String + first_changed_line::Union{Int64, Nothing} +end + +function createEditTool{TContext}() where TContext + return AgentTool( + "edit", + "edit", + "Edit a single file using exact text replacement.", + Dict{String, Any}(), + (tool_call_id, params, signal, on_update, context) -> begin + # TODO: Implement edit execution + return AgentToolResult([TextContent("File edited successfully")], nothing, nothing, nothing, nothing) + end, + nothing, + nothing, + ) +end + +end diff --git a/src/tools/edit_diff.jl b/src/tools/edit_diff.jl new file mode 100644 index 0000000..1dd4c7d --- /dev/null +++ b/src/tools/edit_diff.jl @@ -0,0 +1,67 @@ +""" + tools/edit_diff.jl - Edit diff utilities + +This module provides shared diff computation utilities for the edit tool. +""" + +module EditDiff + +using ..Types: * + +function detectLineEnding(content::String)::String + crlf_idx = findfirst("\r\n", content) + lf_idx = findfirst("\n", content) + if isnothing(lf_idx) + return "\n" + end + if isnothing(crlf_idx) + return "\n" + end + return crlf_idx < lf_idx ? "\r\n" : "\n" +end + +function normalizeToLF(text::String)::String + return replace(text, "\r\n" => "\n", "\r" => "\n") +end + +function restoreLineEndings(text::String, ending::String)::String + if ending == "\r\n" + return replace(text, "\n" => "\r\n") + end + return text +end + +function normalizeForFuzzyMatch(text::String)::String + # TODO: Implement fuzzy matching normalization + return text +end + +function splitLinesWithEndings(content::String)::Vector{String} + # TODO: Implement line splitting with endings + return split(content, "\n") +end + +function applyEditsToNormalizedContent( + normalized_content::String, + edits::Vector{Any}, + path::String, +)::Tuple{String, String} + # TODO: Implement edit application + return normalized_content, normalized_content +end + +function generateUnifiedPatch(path::String, old_content::String, new_content::String, context_lines::Int64=4)::String + # TODO: Implement unified patch generation + return "" +end + +function generateDiffString( + old_content::String, + new_content::String, + context_lines::Int64=4, +)::Tuple{String, Union{Int64, Nothing}} + # TODO: Implement diff string generation + return "", nothing +end + +end diff --git a/src/tools/file_mutation_queue.jl b/src/tools/file_mutation_queue.jl new file mode 100644 index 0000000..2f106ce --- /dev/null +++ b/src/tools/file_mutation_queue.jl @@ -0,0 +1,59 @@ +""" + tools/file_mutation_queue.jl - File mutation queue + +This module provides file mutation serialization for safe concurrent file writes. +""" + +module FileMutationQueue + +using ..Types: * +using ..HarnessTypes: ExecutionEnv, getOrThrow, FileError, Result + +# ============================================================================ +# Mutation queue state +# ============================================================================ + +mutable struct MutationQueueState + queues::Dict{String, Any} + registration::Any +end + +# Global state +const states = Dict{ExecutionEnv, MutationQueueState}() + +function getState(env::ExecutionEnv)::MutationQueueState + if !haskey(states, env) + states[env] = MutationQueueState(Dict{String, Any}(), nothing) + end + return states[env] +end + +# ============================================================================ +# File mutation queue helpers +# ============================================================================ + +async function getMutationQueueKey(env::ExecutionEnv, path::String)::String + absolute_path = getOrThrow(getOrThrow(absolutePath(env, path), "Failed to get absolute path")) + canonical_path = canonicalPath(env, absolute_path, nothing) + if canonical_path.ok + return canonical_path.value + end + if canonical_path.error.code in ("not_found", "not_supported") + return absolute_path + end + throw(canonical_path.error) +end + +# ============================================================================ +# Main function - serialize file mutations +# ============================================================================ + +function withFileMutationQueue{T}(env::ExecutionEnv, path::String, fn::Function)::T + state = getState(env) + + # TODO: Implement proper async queueing + # This is a simplified version + return fn() +end + +end diff --git a/src/tools/image.jl b/src/tools/image.jl new file mode 100644 index 0000000..db99146 --- /dev/null +++ b/src/tools/image.jl @@ -0,0 +1,66 @@ +""" + tools/image.jl - Image utilities + +This module provides image detection and encoding utilities. +""" + +module Image + +using ..Types: * + +function detectSupportedImageMimeType(buffer::Vector{UInt8})::Union{String, Nothing} + if length(buffer) >= 3 && buffer[1:3] == [0xff, 0xd8, 0xff] + if buffer[4] == 0xf7 + return nothing + end + return "image/jpeg" + end + + if length(buffer) >= 8 && buffer[1:8] == [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] + return "image/png" + end + + if length(buffer) >= 3 && buffer[1:3] == [0x47, 0x49, 0x46] + return "image/gif" + end + + if length(buffer) >= 12 && buffer[1:4] == [0x52, 0x49, 0x46, 0x46] && buffer[9:12] == [0x57, 0x45, 0x42, 0x50] + return "image/webp" + end + + if length(buffer) >= 2 && buffer[1:2] == [0x42, 0x4d] + return "image/bmp" + end + + return nothing +end + +function encodeBase64(bytes::Vector{UInt8})::String + alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + output = "" + + for i in 1:3:length(bytes) + first_byte = i <= length(bytes) ? bytes[i] : 0 + second_byte = i+1 <= length(bytes) ? bytes[i+1] : 0 + third_byte = i+2 <= length(bytes) ? bytes[i+2] : 0 + + output *= alphabet[first_byte >> 2 + 1] + output *= alphabet[(((first_byte & 0x03) << 4) | ((second_byte >> 4) & 0x0f)) + 1] + + if i+1 <= length(bytes) + output *= alphabet[(((second_byte & 0x0f) << 2) | ((third_byte >> 6) & 0x03)) + 1] + else + output *= "=" + end + + if i+2 <= length(bytes) + output *= alphabet[third_byte & 0x3f + 1] + else + output *= "=" + end + end + + return output +end + +end diff --git a/src/tools/index.jl b/src/tools/index.jl new file mode 100644 index 0000000..d20be3a --- /dev/null +++ b/src/tools/index.jl @@ -0,0 +1,35 @@ +""" + tools/index.jl - Tool exports + +This module exports all tools. +""" + +module ToolsIndex + +using ..Tools.Bash: createBashTool +using ..Tools.Read: createReadTool +using ..Tools.Write: createWriteTool +using ..Tools.Edit: createEditTool +using ..Tools.Edit: EditToolDetails, EditToolInput +using ..Tools.Read: ReadToolDetails, ReadToolInput, ReadToolOptions, ReadImageProcessor, ReadImageProcessorResult + +export + createBashTool, + createReadTool, + createWriteTool, + createEditTool, + BashExecution, + BashPrepare, + BashToolDetails, + BashToolInput, + BashToolOptions, + EditToolDetails, + EditToolInput, + ReadToolDetails, + ReadToolInput, + ReadToolOptions, + ReadImageProcessor, + ReadImageProcessorResult, + WriteToolInput + +end diff --git a/src/tools/path_utils.jl b/src/tools/path_utils.jl new file mode 100644 index 0000000..dadacf4 --- /dev/null +++ b/src/tools/path_utils.jl @@ -0,0 +1,44 @@ +""" + tools/path_utils.jl - Path resolution utilities + +This module provides path resolution utilities for tools. +""" + +module PathUtils + +using ..Types: * +using ..HarnessTypes: ExecutionEnv, getOrThrow, FileError, Result + +function normalizeToolPath(path::String)::String + normalized = replace(path, r"[\u00A0\u2000-\u200A\u202F\u205F\u3000]" => " ") + if startswith(normalized, "@") + return normalized[2:end] + end + return normalized +end + +function resolveToolPath(env::ExecutionEnv, path::String, signal::Union{Any, Nothing}=nothing)::String + return getOrThrow(getOrThrow(absolutePath(env, normalizeToolPath(path), signal), "Failed to resolve path")) +end + +function resolveReadToolPath(env::ExecutionEnv, path::String, signal::Union{Any, Nothing}=nothing)::String + resolved = getOrThrow(getOrThrow(absolutePath(env, normalizeToolPath(path), signal), "Failed to resolve path")) + + variants = String[ + resolved, + replace(resolved, r" (AM|PM)\."i => " $1."), + normalized = replace(resolved, NFC => NFD), + replace(resolved, "'" => "\u2019"), + replace(replace(resolved, NFC => NFD), "'" => "\u2019"), + ] + + for variant in variants + if getOrThrow(getOrThrow(exists(env, variant, signal), "Failed to check existence"), "Not found") + return variant + end + end + + return resolved +end + +end diff --git a/src/tools/read.jl b/src/tools/read.jl new file mode 100644 index 0000000..b358437 --- /dev/null +++ b/src/tools/read.jl @@ -0,0 +1,35 @@ +""" + tools/read.jl - File read tool + +This module provides the file read tool for AgentCore. +""" + +module Read + +using ..Types: * + +mutable struct ReadToolDetails + truncation::Union{Any, Nothing} +end + +mutable struct ReadToolOptions + auto_resize_images::Bool + image_processor::Union{Any, Nothing} +end + +function createReadTool{TContext}(options::Union{ReadToolOptions, Nothing}=nothing) where TContext + return AgentTool( + "read", + "read", + "Read the contents of a file.", + Dict{String, Any}(), + (tool_call_id, params, signal, on_update, context) -> begin + # TODO: Implement read execution + return AgentToolResult([TextContent("File read successfully")], nothing, nothing, nothing, nothing) + end, + nothing, + nothing, + ) +end + +end diff --git a/src/tools/write.jl b/src/tools/write.jl new file mode 100644 index 0000000..5d4acc3 --- /dev/null +++ b/src/tools/write.jl @@ -0,0 +1,26 @@ +""" + tools/write.jl - File write tool + +This module provides the file write tool for AgentCore. +""" + +module Write + +using ..Types: * + +function createWriteTool{TContext}() where TContext + return AgentTool( + "write", + "write", + "Write content to a file.", + Dict{String, Any}(), + (tool_call_id, params, signal, on_update, context) -> begin + # TODO: Implement write execution + return AgentToolResult([TextContent("File written successfully")], nothing, nothing, nothing, nothing) + end, + nothing, + nothing, + ) +end + +end diff --git a/src/type.jl b/src/type.jl deleted file mode 100644 index d554f2a..0000000 --- a/src/type.jl +++ /dev/null @@ -1,375 +0,0 @@ -module type - -export agent, sommelier, companion, virtualcustomer, agentcontext - -using Dates, UUIDs, DataStructures, JSON, NATS -using GeneralUtils - -# ---------------------------------------------- 100 --------------------------------------------- # - - -mutable struct agentcontext - text2textInstructLLM::Function - getTextEmbedding::Function - executeSQL::Function - similarSQLVectorDB::Function - insertSQLVectorDB::Function - similarSommelierDecision::Function - insertSommelierDecision::Function - find_related_tables_for_user_question::Function - pg_conn_str::String - agentconfig::AbstractDict -end - -abstract type agent end - -mutable struct sommelier <: agent - name::String # agent name - id::String # agent id - retailername::String - retailerid::String - tools::Dict - maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized - chathistory::Vector{Dict{String, Any}} - memory::Dict{String, Any} - context::agentcontext - llmFormatName::String -end - -""" A sommelier agent. - -# Arguments - - `context::agentcontext` - Application context containing shared functions for LLM, SQL, and vector database operations. - -# Keyword Arguments - - `name::String` - Agent's name. Default: `"Assistant"` - - `id::String` - Agent's ID. Default: generated UUID string. - - `retailername::String` - Retailer name associated with the sommelier. Default: `"retailer_name"` - - `maxHistoryMsg::Integer` - Maximum history messages. Default: `20` - - `chathistory::Vector{Dict{String, String}}` - Chat history. Default: empty vector. - - `llmFormatName::String` - LLM format name. Default: `"granite3"` - -# Return - - `sommelier`: An instantiated sommelier agent. - -# Example -```julia -julia> using YiemAgent -julia> context = agentcontext( - text2textInstructLLM, - getTextEmbedding, - executeSQL, - similarSQLVectorDB, - insertSQLVectorDB, - similarSommelierDecision, - insertSommelierDecision - ) -julia> agent = sommelier(context, name="WineExpert", id="123", retailername="MyWineShop") -``` -""" -function sommelier( - context::agentcontext, # agent functions, db connect and other context - ; - name::String= "Assistant", - id::String= string(uuid4()), - retailername::String= "not specified", - retailerid::String= "not specified", - maxHistoryMsg::Integer= 20, - chathistory::Vector{Dict{String, Any}} = Vector{Dict{String, Any}}(), - llmFormatName::String= "granite3" - ) - - tools = Dict( # update input format - "chatbox"=> Dict( - "description" => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", - "input" => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", - "output" => "" , - ), - "winestock"=> Dict( - "description" => "A handy tool for searching wine in your inventory that match the user preferences.", - "input" => """Input is a JSON-formatted string that contains a detailed and precise search query.{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}""", - "output" => """Output are wines that match the search query in JSON format.""", - ), - ) - - """ Memory - - Chat history use openai format as follow: - - image1_path = "test/large_image.png" --- - image1_bytes = read(image1_path) | this part must be done - image1_base64_string = base64encode(image1_bytes) | in frontend - mime_type = "image/png" | not in agent code - data1_uri = "data:;base64," --- - - chathistory= [ - Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => "You are a helpful assistant"), - ] - ), - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => " - LLM context here... - - Do you know this wine? Just give me brief intro." - ), - Dict( - "type" => "image_url", - "image_url" => Dict("url" => data1_uri) - ), - ] - ), - ] - - shortmem = Dict( - "1"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."), - "2"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."), - ... - ) - """ - memory = Dict{String, Any}( - "shortmem"=> OrderedDict{String, Any}(), - "scratchpad"=> "", - "recap"=> OrderedDict{String, Any}(), - ) - - newAgent = sommelier( - name, - id, - retailername, - retailerid, - tools, - maxHistoryMsg, - chathistory, - memory, - context, - llmFormatName - ) - systemmsg = - """ - # store_policy - - Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory. - - If you found wines in the store's database, they are in stock. - - You can only recommend wines that are currently in our inventory - - Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences. - - Ask the user one question at a time. - - Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services. - - Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future. - - Spicy foods should be paired only with light red wines. - - We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such. - - Gift box, gift card, and custom messages are available. Inform the user to contact our sales team. - - # store_guidelines - - Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting. - - Customer may provide images for you to look up. - - Encourage the customer to explore different options and try new things. - - If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead. - - Your store carries only wine. - - Vintage 0 means non-vintage. - - Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently. - - User usually ask for something similar. This means you should use the search term based on the profile they like. - - # situation - You are having conversation with a customer. - - # your role - Your name is $(newAgent.name). You are a helpful sommelier for website-based $(newAgent.retailername)'s wine store. - - # objective - - Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences. - - Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences. - - # your responsibility includes - - According to the store's policy and guidelines, and make an informed decision about what available_actions you need to use to achieve the objective. - - Keep the conversation with the customer going smoothly - - # your responsibility does NOT includes - - Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store. - - Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store. - - Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store. - - # you should then respond to the user with interleaving plan, action_name, action_input in JSON format - 1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific. - 2) "action_name", (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name - 3) "action_input", The input to the action you are about to perform according to your plan. - After the action is executed you gets "action_result". It is the output from the action you selected. - - # available actions - "CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to chat with the user according to your plan. - "SEARCH_WINE_DATABASE", allows you to search information about wines you want in your inventory's database. The input is strictly supported search term including: retailer_name, wine price, winery, name, vintage, region, country, type of wine, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity. - Example query 1: "Dry, full-bodied red wine from Burgundy, France. Grape varietal could be Merlot or Syrah. price 100 to 1000 USD." - Example query 2: "Red or white wine, medium tannin, price under 700 USD" - Example query 3: "white wine from Tuscany, Italy or Bordeaux, France - "WINE_PRESENTATION_GUIDELINE", which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow. - "END_CONVER_GUIDELINE", which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow. - """ - - system_msg = Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => systemmsg), - ] - ) - - push!(newAgent.chathistory, system_msg) - - return newAgent -end - - -mutable struct virtualcustomer <: agent - name::String # agent name - id::String # agent id - systemmsg::String # system message - tools::Dict - maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized - chathistory::Vector{Dict{String, Any}} - memory::Dict{String, Any} - context # NamedTuple of functions - llmFormatName::String -end - -function virtualcustomer( - context, # NamedTuple of functions - ; - name::String= "Assistant", - id::String= string(uuid4()), - maxHistoryMsg::Integer= 20, - chathistory::Vector{Dict{String, String}} = Vector{Dict{String, String}}(), - llmFormatName::String= "granite3", - systemmsg::String= - """ - Your name: $name - Your sex: Female - Your role: You are a helpful assistant. - You should follow the following guidelines: - - Focus on the latest conversation. - - Your like to be short and concise. - - Let's begin! - """, - ) - - tools = Dict( # update input format - "chatbox"=> Dict( - "description" => "Useful for when you need to ask the user for more context. Do not ask the user their own question.", - "input" => """Input is a text in JSON format.{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}""", - "output" => "" , - ), - ) - - """ Memory - Ref: Chat prompt format is openai - chathistory = [ - Dict( - "role" => "system", - "content" => [ - Dict("type" => "text", "text" => system_msg), - ] - ), - Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => "Do you know this wine? Just give me brief intro."), - Dict( - "type" => "image_url", - "image_url" => Dict("url" => data1_uri) - ) - ] - ) - ] - """ - memory = Dict{String, Any}( - "shortmem"=> OrderedDict{String, Any}( - ), - "scratchpad"=> "", - "events"=> Vector{Dict{String, Any}}(), - "state"=> Dict{String, Any}( - ), - "recap"=> OrderedDict{String, Any}(), - ) - - newAgent = virtualcustomer( - name, - id, - systemmsg, - tools, - maxHistoryMsg, - chathistory, - memory, - context, - llmFormatName - ) - - return newAgent -end - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -end # module type \ No newline at end of file diff --git a/src/types.jl b/src/types.jl new file mode 100644 index 0000000..fd39e41 --- /dev/null +++ b/src/types.jl @@ -0,0 +1,588 @@ +""" + types.jl - Core types for AgentCore + +This module defines the fundamental types used throughout the AgentCore package. +""" + +module Types + +using Dates +using UUIDs +using JSON3 +using Unicode + +# ============================================================================ +# Basic type aliases +# ============================================================================ + +const Timestamp = Int64 + +# ============================================================================ +# Thinking level enum +# ============================================================================ + +@enum ThinkingLevel begin + THINKING_OFF = "off" + THINKING_MINIMAL = "minimal" + THINKING_LOW = "low" + THINKING_MEDIUM = "medium" + THINKING_HIGH = "high" + THINKING_XHIGH = "xhigh" + THINKING_MAX = "max" +end + +# ============================================================================ +# Tool execution modes +# ============================================================================ + +@enum ToolExecutionMode begin + EXECUTION_SEQUENTIAL = "sequential" + EXECUTION_PARALLEL = "parallel" +end + +# ============================================================================ +# Queue drain modes +# ============================================================================ + +@enum QueueMode begin + QUEUE_ALL = "all" + QUEUE_ONE_AT_A_TIME = "one-at-a-time" +end + +# ============================================================================ +# Message content types +# ============================================================================ + +abstract type MessageContent end + +struct TextContent <: MessageContent + text::String +end + +struct ImageContent <: MessageContent + data::String + mime_type::String +end + +# ============================================================================ +# Message types +# ============================================================================ + +abstract type Message end + +struct UserMessage <: Message + role::String + content::Vector{MessageContent} + timestamp::Timestamp +end + +struct AssistantMessage <: Message + role::String + content::Vector{MessageContent} + api::String + provider::String + model::String + usage::Usage + stop_reason::String + error_message::Union{String, Nothing} + timestamp::Timestamp +end + +struct ToolResultMessage <: Message + role::String + tool_call_id::String + tool_name::String + content::Vector{MessageContent} + details::Any + usage::Union{Usage, Nothing} + added_tool_names::Union{Vector{String}, Nothing} + is_error::Bool + timestamp::Timestamp +end + +# ============================================================================ +# Usage statistics +# ============================================================================ + +struct UsageCost + input::Float64 + output::Float64 + cache_read::Float64 + cache_write::Float64 + total::Float64 +end + +struct Usage + input::Int64 + output::Int64 + cache_read::Int64 + cache_write::Int64 + total_tokens::Int64 + cost::UsageCost +end + +# ============================================================================ +# Model types +# ============================================================================ + +struct ModelCost + input::Float64 + output::Float64 + cache_read::Float64 + cache_write::Float64 +end + +struct Model{Api} + id::String + name::String + api::Api + provider::String + base_url::String + reasoning::Bool + input::Vector{String} + cost::ModelCost + context_window::Int64 + max_tokens::Int64 +end + +# ============================================================================ +# Agent message union type +# ============================================================================ + +abstract type AgentMessage end + +# Custom message types can extend this via multiple dispatch +struct CustomMessage <: AgentMessage + message::AgentMessage + custom_type::String +end + +# ============================================================================ +# Tool types +# ============================================================================ + +struct AgentToolResult{T} + content::Vector{MessageContent} + details::T + usage::Union{Usage, Nothing} + added_tool_names::Union{Vector{String}, Nothing} + terminate::Union{Bool, Nothing} +end + +struct AgentTool{TParameters, TDetails} + name::String + label::String + description::String + parameters::TParameters + execute::Function + prepare_arguments::Union{Function, Nothing} + execution_mode::Union{ToolExecutionMode, Nothing} +end + +# ============================================================================ +# Agent context +# ============================================================================ + +struct AgentContext + system_prompt::String + messages::Vector{AgentMessage} + tools::Union{Vector{AgentTool}, Nothing} +end + +# ============================================================================ +# Event types +# ============================================================================ + +abstract type AgentEvent end + +struct AgentStartEvent <: AgentEvent end +struct AgentEndEvent <: AgentEvent + messages::Vector{AgentMessage} +end +struct TurnStartEvent <: AgentEvent end +struct TurnEndEvent <: AgentEvent + message::AgentMessage + tool_results::Vector{ToolResultMessage} +end +struct MessageStartEvent <: AgentEvent + message::AgentMessage +end +struct MessageUpdateEvent <: AgentEvent + message::AgentMessage + assistant_message_event::Any +end +struct MessageEndEvent <: AgentEvent + message::AgentMessage +end +struct ToolExecutionStartEvent <: AgentEvent + tool_call_id::String + tool_name::String + args::Any +end +struct ToolExecutionUpdateEvent <: AgentEvent + tool_call_id::String + tool_name::String + args::Any + partial_result::Any +end +struct ToolExecutionEndEvent <: AgentEvent + tool_call_id::String + tool_name::String + result::Any + is_error::Bool +end + +# ============================================================================ +# Assistant message event types +# ============================================================================ + +abstract type AssistantMessageEvent end + +struct StartEvent <: AssistantMessageEvent + partial::AssistantMessage +end +struct TextStartEvent <: AssistantMessageEvent + content_index::Int64 + partial::AssistantMessage +end +struct TextDeltaEvent <: AssistantMessageEvent + content_index::Int64 + delta::String + partial::AssistantMessage +end +struct TextEndEvent <: AssistantMessageEvent + content_index::Int64 + content::String + partial::AssistantMessage +end +struct DoneEvent <: AssistantMessageEvent + reason::String + usage::Usage + message::AssistantMessage +end +struct ErrorEvent <: AssistantMessageEvent + reason::String + error_message::Union{String, Nothing} + usage::Usage + error::AssistantMessage +end + +# ============================================================================ +# Agent state +# ============================================================================ + +mutable struct AgentState + system_prompt::String + model::Model + thinking_level::ThinkingLevel + tools::Vector{AgentTool} + messages::Vector{AgentMessage} + is_streaming::Bool + streaming_message::Union{AgentMessage, Nothing} + pending_tool_calls::Set{String} + error_message::Union{String, Nothing} + + function AgentState( + system_prompt::String="", + model::Model=Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0), + thinking_level::ThinkingLevel=THINKING_OFF, + tools::Vector{AgentTool}=AgentTool[], + messages::Vector{AgentMessage}=AgentMessage[], + ) + new( + system_prompt, + model, + thinking_level, + copy(tools), + copy(messages), + false, + nothing, + Set{String}(), + nothing, + ) + end +end + +# ============================================================================ +# Tool call types +# ============================================================================ + +struct ToolCall + type::String + id::String + name::String + arguments::Dict{String, Any} + partial_json::Union{String, Nothing} +end + +# ============================================================================ +# Context transform types +# ============================================================================ + +struct PrepareNextTurnContext + message::AssistantMessage + tool_results::Vector{ToolResultMessage} + context::AgentContext + new_messages::Vector{AgentMessage} +end + +struct AgentLoopTurnUpdate + context::Union{AgentContext, Nothing} + model::Union{Model, Nothing} + thinking_level::Union{ThinkingLevel, Nothing} +end + +# ============================================================================ +# Before/After tool call types +# ============================================================================ + +struct BeforeToolCallContext + assistant_message::AssistantMessage + tool_call::ToolCall + args::Any + context::AgentContext +end + +struct BeforeToolCallResult + block::Union{Bool, Nothing} + reason::Union{String, Nothing} +end + +struct AfterToolCallContext + assistant_message::AssistantMessage + tool_call::ToolCall + args::Any + result::AgentToolResult + is_error::Bool + context::AgentContext +end + +struct AfterToolCallResult + content::Union{Vector{MessageContent}, Nothing} + details::Union{Any, Nothing} + is_error::Union{Bool, Nothing} + usage::Union{Usage, Nothing} + terminate::Union{Bool, Nothing} +end + +# ============================================================================ +# Stream function signature +# ============================================================================ + +const StreamFn = Function + +# ============================================================================ +# File types +# ============================================================================ + +struct FileKind + value::String +end +const FILE_KIND_FILE = FileKind("file") +const FILE_KIND_DIRECTORY = FileKind("directory") +const FILE_KIND_SYMLINK = FileKind("symlink") + +struct FileInfo + name::String + path::String + kind::FileKind + size::Int64 + mtime_ms::Int64 +end + +struct FileError <: Exception + code::String + message::String + path::Union{String, Nothing} + cause::Union{Exception, Nothing} +end + +struct ExecutionError <: Exception + code::String + message::String + cause::Union{Exception, Nothing} +end + +struct CompactionError <: Exception + code::String + message::String + cause::Union{Exception, Nothing} +end + +struct BranchSummaryError <: Exception + code::String + message::String + cause::Union{Exception, Nothing} +end + +struct SessionError <: Exception + code::String + message::String + cause::Union{Exception, Nothing} +end + +struct AgentHarnessError <: Exception + code::String + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Session tree entry types +# ============================================================================ + +abstract type SessionTreeEntry end + +struct SessionTreeEntryBase + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String +end + +struct MessageEntry <: SessionTreeEntry + base::SessionTreeEntryBase + message::AgentMessage +end + +struct ThinkingLevelChangeEntry <: SessionTreeEntry + base::SessionTreeEntryBase + thinking_level::String +end + +struct ModelChangeEntry <: SessionTreeEntry + base::SessionTreeEntryBase + provider::String + model_id::String +end + +struct ActiveToolsChangeEntry <: SessionTreeEntry + base::SessionTreeEntryBase + active_tool_names::Vector{String} +end + +struct CompactionEntry{T} <: SessionTreeEntry + base::SessionTreeEntryBase + summary::String + first_kept_entry_id::Union{String, Nothing} + tokens_before::Int64 + retained_tail::Union{Vector{AgentMessage}, Nothing} + details::Union{T, Nothing} + usage::Union{Usage, Nothing} + from_hook::Bool +end + +struct BranchSummaryEntry{T} <: SessionTreeEntry + base::SessionTreeEntryBase + from_id::String + summary::String + details::Union{T, Nothing} + usage::Union{Usage, Nothing} + from_hook::Bool +end + +struct CustomEntry{T} <: SessionTreeEntry + base::SessionTreeEntryBase + custom_type::String + data::Union{T, Nothing} +end + +struct CustomMessageEntry{T} <: SessionTreeEntry + base::SessionTreeEntryBase + custom_type::String + content::String + details::Union{T, Nothing} + display::Bool +end + +struct LabelEntry <: SessionTreeEntry + base::SessionTreeEntryBase + target_id::String + label::Union{String, Nothing} +end + +struct SessionInfoEntry <: SessionTreeEntry + base::SessionTreeEntryBase + name::Union{String, Nothing} +end + +struct LeafEntry <: SessionTreeEntry + base::SessionTreeEntryBase + target_id::Union{String, Nothing} +end + +# ============================================================================ +# Session context +# ============================================================================ + +struct SessionContext + messages::Vector{AgentMessage} + thinking_level::String + model::Union{Dict{String, String}, Nothing} + active_tool_names::Union{Vector{String}, Nothing} +end + +# ============================================================================ +# Session stats +# ============================================================================ + +struct SessionStats + message_count::Int64 + cached_tokens::Int64 + uncached_tokens::Int64 + total_tokens::Int64 + cost_total::Float64 +end + +# ============================================================================ +# Session metadata +# ============================================================================ + +abstract type SessionMetadata end + +struct JsonlSessionMetadata <: SessionMetadata + id::String + created_at::String + cwd::String + path::String + parent_session_path::Union{String, Nothing} + metadata::Union{Dict{String, Any}, Nothing} +end + +# ============================================================================ +# Session storage interface +# ============================================================================ + +abstract type SessionStorage{T<:SessionMetadata} end + +# ============================================================================ +# Session repo interface +# ============================================================================ + +abstract type SessionRepo< + TMetadata<:SessionMetadata, + TCreateOptions, + TListOptions +> end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function create_timestamp()::String + return string(Dates.now(Dates.UTC)) +end + +function uuidv7()::String + return string(UUIDs.uuid7()) +end + +function uuidstring()::String + return string(UUIDs.uuid4()) +end + +function tempname()::String + return tempname() +end + +end diff --git a/src/util.jl b/src/util.jl deleted file mode 100644 index e6e9f69..0000000 --- a/src/util.jl +++ /dev/null @@ -1,457 +0,0 @@ -module util - -export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline, - availableWineToText, createEventsLog, createChatLog, checkAgentResponse_JSON, - checkAgentResponse_text - -using UUIDs, Dates, DataStructures, HTTP, JSON -using GeneralUtils -using ..type - -# ---------------------------------------------- 100 --------------------------------------------- # - -""" Clear agent chat history. - -# Arguments - - `a::agent` - an agent - -# Return - - nothing - -# Example -```jldoctest -julia> using YiemAgent, MQTTClient, GeneralUtils -julia> client, connection = MakeConnection("test.mosquitto.org", 1883) -julia> connect(client, connection) -julia> msgMeta = GeneralUtils.generate_msgMeta("testtopic") -julia> agentConfig = Dict( - "receiveprompt"=>Dict( - "mqtttopic"=> "testtopic/receive", - ), - "receiveinternal"=>Dict( - "mqtttopic"=> "testtopic/internal", - ), - "text2text"=>Dict( - "mqtttopic"=> "testtopic/text2text", - ), - ) -julia> a = YiemAgent.sommelier( - client, - msgMeta, - agentConfig, - ) -julia> YiemAgent.addNewMessage(a, "user", "hello") -julia> YiemAgent.clearhistory(a) -``` - -# TODO - - [PENDING] clear memory - -# Signature -""" -function clearhistory(a::T) where {T<:agent} - empty!(a.chathistory) - empty!(a.memory["shortmem"]) - empty!(a.memory["events"]) - a.memory["chatbox"] = "" -end - - -""" Add new message to agent. - - messages => Dict( - "role" => "user", - "content" => [ - Dict("type" => "text", "text" => "Describe this image for me"), - Dict( - "type" => "image_url", - "image_url" => Dict("url" => data_uri) - ) - ] - ) - - Arguments\n - ----- - a::agent - an agent - role::String - message sender role i.e. system, user or assistant - text::String - message text - - Return\n - ----- - nothing - - Example\n - ----- - ```jldoctest - - ``` - - Signature\n - ----- -""" -function addNewMessage(a::T1, name::String, userinput::T2; - maximumMsg::Integer=30) where {T1<:agent, T2<:AbstractDict} - # if name ∉ ["system", "user", "assistant"] # guard against typo - # error("name is not in agent.availableRole $(@__LINE__)") - # end - - #TODO summarize the oldest 10 message - if length(a.chathistory) > maximumMsg - summarize(a.chathistory) - else - # userinput["timestamp"] = Dates.now() - push!(a.chathistory, userinput) - end -end - - -""" Converts a vector of dictionaries to a formatted string. -This function takes in a vector of dictionaries and outputs a single string where each dictionary's keys are prefixed by their values. - -# Arguments - - `vecd::Vector` - A vector of dictionaries containing chat messages - - `withkey::Bool` - Whether to include the name as a prefix in the output text. Default is true - - `range::Union{Nothing,UnitRange,Int}` - Optional range of messages to include. If nothing, includes all messages - -# Returns - A formatted string where each line contains either: - - If withkey=true: "name> message\n" - - If withkey=false: "message\n" - -# Example - -julia> using Revise -julia> using GeneralUtils -julia> vecd = [Dict("name" => "John", "text" => "Hello"), Dict("name" => "Jane", "text" => "Goodbye")] -julia> GeneralUtils.vectorOfDictToText(vecd, withkey=true) -"John> Hello\nJane> Goodbye\n" -``` -""" -function chatHistoryToText(vecd::Vector; withkey=true, range=nothing)::String - # Initialize an empty string to hold the final text - text = "" - - # Get the elements within the specified range, or all elements if no range provided - elements = isnothing(range) ? vecd : vecd[range] - - # Determine whether to include the key in the output text or not - if withkey - # Loop through each dictionary in the input vector - for d in elements - # Extract the 'name' and 'text' keys from the dictionary - name = titlecase(d[:name]) - _text = d[:text] - - # Append the formatted string to the text variable - text *= "$name> $_text \n" - end - else - # Loop through each dictionary in the input vector - for d in elements - # Iterate over all key-value pairs in the dictionary - for (k, v) in d - # Append the formatted string to the text variable - text *= "$v \n" - end - end - end - - # Return the final text - return text -end - - -function availableWineToText(vecd::Vector)::String - # Initialize an empty string to hold the final text - rowtext = "" - # Loop through each dictionary in the input vector - for (i, d) in enumerate(vecd) - # Iterate over all key-value pairs in the dictionary - temp = [] - for (k, v) in d - # Append the formatted string to the text variable - t = "$k:$v" - push!(temp, t) - end - _rowtext = join(temp, ',') - rowtext *= "$i) $_rowtext " - end - - return rowtext -end - - - -""" Create a dictionary representing an event with optional details. - -# Arguments - - `event_description::Union{String, Nothing}` - A description of the event - - `timestamp::Union{DateTime, Nothing}` - The time when the event occurred - - `subject::Union{String, Nothing}` - The subject or entity associated with the event - - `thought::Union{AbstractDict, Nothing}` - Any associated thoughts or metadata - - `action_name::Union{String, Nothing}` - The name of the action performed (e.g., "CHAT", "CHECKINVENTORY") - - `action_input::Union{String, Nothing}` - Input or parameters for the action - - `location::Union{String, Nothing}` - Where the event took place - - `equipment_used::Union{String, Nothing}` - Equipment involved in the event - - `material_used::Union{String, Nothing}` - Materials used during the event - - `outcome::Union{String, Nothing}` - The result or consequence of the event after action execution - - `note::Union{String, Nothing}` - Additional notes or comments - -# Returns - A dictionary with event details as symbol-keyed key-value pairs -""" -function eventdict(; - event_description::Union{String, Nothing}=nothing, - timestamp::Union{DateTime, Nothing}=nothing, - subject::Union{String, Nothing}=nothing, - thought::Union{AbstractDict, Nothing}=nothing, - action_name::Union{String, Nothing}=nothing, # "CHAT", "CHECKINVENTORY", "PRESENT_WINE_GUIDELINE", etc - action_input::Union{String, Nothing}=nothing, - location::Union{String, Nothing}=nothing, - equipment_used::Union{String, Nothing}=nothing, - material_used::Union{String, Nothing}=nothing, - observation::Union{String, Nothing}=nothing, - note::Union{String, Nothing}=nothing, - ) - - d = Dict{String, Any}( - "event_description"=> event_description, - "timestamp"=> timestamp, - "subject"=> subject, - "thought"=> thought, - "action_name"=> action_name, - "action_input"=> action_input, - "location"=> location, - "equipment_used"=> equipment_used, - "material_used"=> material_used, - "observation"=> observation, - "note"=> note, - ) - - return d -end - - -""" Create a formatted timeline string from a sequence of events. - -# Arguments - - `events::T1` - Vector of event dictionaries containing subject, action_input and optional outcome fields - Each event dictionary should have the following keys: - - :subject - The subject or entity performing the action - - :action_input - The action or input performed by the subject - - :observation - (Optional) The result or outcome of the action - -# Returns - - `timeline::String` - A formatted string representing the events with their subjects, actions, and optional outcomes - Format: "{index}) {subject}> {action_input} {outcome}\n" for each event - -# Example - -events = [ - Dict("subject" => "User", "action_input" => "Hello", "observation" => nothing), - Dict("subject" => "Assistant", "action_input" => "Hi there!", "observation" => "with a smile") -] -timeline = createTimeline(events) -# 1) User> Hello -# 2) Assistant> Hi there! with a smile - -""" -function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing - ) where {T1<:AbstractVector} - # Initialize empty timeline string - timeline = "" - - # Determine which indices to use - either provided range or full length - ind = - if eventindex !== nothing - [eventindex...] - else - 1:length(events) - end - - # Iterate through events and format each one - for i in ind - event = events[i] - # If no outcome exists, format without outcome -# if event["action_name"] == "CHAT_BOX" - # timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"])\n" - # elseif event["action_name"] == "CHECKINVENTORY" && event["observation"] === nothing - # timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), observation: Not done yet.\n" - if event["action_name"] == "SEARCH_WINE_DATABASE" - timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), observation: $(event["observation"])\\n" - else - timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"])\\n" - end - end - - # Return formatted timeline string - return timeline -end - -function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing - ) where {T1<:AbstractVector} - # Initialize empty log array - log = Dict{String, String}[] - - # Determine which indices to use - either provided range or full length - ind = - if index !== nothing - [index...] - else - 1:length(events) - end - - # Iterate through events and format each one - for i in ind - event = events[i] - # If no outcome exists, format without outcome - if event["observation"] === nothing - subject = event["subject"] - action_name = event["action_name"] - action_input = event["action_input"] - str = "action_name: $action_name, action_input: $action_input" - d = Dict{String, String}("name"=>subject, "text"=>str) - push!(log, d) - else - subject = event["subject"] - action_name = event["action_name"] - action_input = event["action_input"] - observation = event["observation"] - str = "action_name: $action_name, action_input: $action_input, observation: $observation" - d = Dict{String, String}("name"=>subject, "text"=>str) - push!(log, d) - end - end - - return log -end - - -function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing - ) where {T1<:AbstractVector} - # Initialize empty log array - log = Dict{String, String}[] - - # Determine which indices to use - either provided range or full length - ind = - if index !== nothing - [index...] - else - 1:length(chatdict) - end - - # Iterate through events and format each one - for i in ind - event = chatdict[i] - subject = event["name"] - text = event["text"] - d = Dict{String, String}("name"=>subject, "text"=>text) - push!(log, d) - end - - return log -end - - -function checkAgentResponse_text(response::String, requiredHeader::T - )::Tuple where {T<:Array{String}} - detected_kw = GeneralUtils.detectKeywordVariation(requiredHeader, response) - missingkeys = [k for (k, v) in detected_kw if v === nothing] - ispass = false - errormsg = nothing - if !isempty(missingkeys) - errormsg = "$missingkeys are missing from your previous response" - ispass = false - elseif sum([length(i) for i in values(detected_kw)]) > length(requiredHeader) - errormsg = "Your previous attempt has duplicated points according to the required response format" - ispass = false - else - ispass = true - end - return (ispass, errormsg) -end - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -end # module util \ No newline at end of file