diff --git a/Makefile b/Makefile index b7868dff..bcf5cb7f 100644 --- a/Makefile +++ b/Makefile @@ -108,6 +108,17 @@ chat-powerpoint-agent: .venv chat-arxiv-agent: .venv @ docker compose run abi bash -c 'poetry install && poetry run python -m src.core.apps.terminal_agent.main generic_run_agent ArXivAssistant' +# Update Bitcoin Agent command to use the standard module loading approach +chat-bitcoin-agent: .venv + @ docker compose run abi bash -c 'poetry install && poetry run python -m src.core.apps.terminal_agent.main generic_run_agent BitcoinAssistant' + +# Bitcoin agent test commands +test-bitcoin-agent: .venv + @ docker compose run abi python -m src.custom.modules.bitcoin.tests.run_price_validation + +test-bitcoin-consensus: .venv + @ docker compose run abi bash -c 'poetry install && poetry run python -m src.custom.modules.bitcoin.tests.test_price_providers' + .DEFAULT_GOAL := chat-supervisor-agent .PHONY: all test unit-tests integration-tests lint clean @@ -137,7 +148,7 @@ fix-lint: black src tests isort src tests -.PHONY: test chat-supervisor-agent chat-support-agent chat-content-agent chat-finance-agent chat-growth-agent chat-opendata-agent chat-operations-agent chat-sales-agent api sh lock add abi-add +.PHONY: test chat-supervisor-agent chat-support-agent chat-content-agent chat-finance-agent chat-growth-agent chat-opendata-agent chat-operations-agent chat-sales-agent chat-bitcoin-agent api sh lock add abi-add # Build module - copies components to the module directory build-module: @@ -165,7 +176,7 @@ build-module: echo "Creating module README..." && \ echo "# $$MODULE_NAME Module\n\nDescription of your module and its purpose.\n\n## Components\n\n- Integrations\n- Workflows\n- Pipelines\n- Ontologies\n- Assistants\n\n## Usage\n\nHow to use this module.\n" > src/custom/modules/$$MODULE_NAME/README.md && \ echo "Creating example assistant..." && \ - echo "from langchain_openai import ChatOpenAI\nfrom abi.services.agent.Agent import Agent, AgentConfiguration, AgentSharedState\nfrom src import secret, services\n\nNAME = \"$$MODULE_NAME_PASCAL Assistant\"\nDESCRIPTION = \"A brief description of what your assistant does.\"\nMODEL = \"o3-mini\" # Or another appropriate model\nTEMPERATURE = 1\nAVATAR_URL = \"https://example.com/avatar.png\"\nSYSTEM_PROMPT = \"\"\"You are the $$MODULE_NAME_PASCAL Assistant. Your role is to help users with tasks related to $$MODULE_NAME.\n\nYou can perform the following tasks:\n- Task 1\n- Task 2\n- Task 3\n\nAlways be helpful, concise, and focus on solving the user's problem.\"\"\"\n\ndef create_agent(shared_state: AgentSharedState = None) -> Agent:\n \"\"\"Creates a new instance of the $$MODULE_NAME_PASCAL Assistant.\"\"\"\n # Configure the underlying chat model\n llm = ChatOpenAI(\n model=MODEL,\n temperature=TEMPERATURE,\n api_key=secret.get_openai_api_key()\n )\n \n # Configure the agent\n config = AgentConfiguration(\n name=NAME,\n description=DESCRIPTION,\n model=MODEL,\n temperature=TEMPERATURE,\n system_prompt=SYSTEM_PROMPT,\n avatar_url=AVATAR_URL,\n shared_state=shared_state or AgentSharedState(),\n )\n \n # Create and return the agent\n agent = Agent(llm=llm, config=config)\n \n # Add tools to the agent (uncomment and modify as needed)\n # workflow = YourWorkflow(YourWorkflowConfiguration())\n # agent.add_tools(workflow.as_tools())\n \n return agent\n\n# For testing purposes\nif __name__ == \"__main__\":\n agent = create_agent()\n agent.run(\"Hello, I need help with $$MODULE_NAME\")\n" > src/custom/modules/$$MODULE_NAME/assistants/$${MODULE_NAME_PASCAL}Assistant.py && \ + echo "from langchain_openai import ChatOpenAI\nfrom abi.services.agent.Agent import Agent, AgentConfiguration, AgentSharedState, MemorySaver\nfrom src import secret, services\n\nNAME = \"$$MODULE_NAME_PASCAL Assistant\"\nDESCRIPTION = \"A brief description of what your assistant does.\"\nMODEL = \"o3-mini\" # Or another appropriate model\nTEMPERATURE = 1\nAVATAR_URL = \"https://example.com/avatar.png\"\nSYSTEM_PROMPT = \"\"\"You are the $$MODULE_NAME_PASCAL Assistant. Your role is to help users with tasks related to $$MODULE_NAME.\n\nYou can perform the following tasks:\n- Task 1\n- Task 2\n- Task 3\n\nAlways be helpful, concise, and focus on solving the user's problem.\"\"\"\n\ndef create_agent(shared_state: AgentSharedState = None) -> Agent:\n \"\"\"Creates a new instance of the $$MODULE_NAME_PASCAL Assistant.\"\"\"\n # Configure the underlying chat model\n chat_model = ChatOpenAI(\n model=MODEL,\n temperature=TEMPERATURE,\n api_key=secret.get('OPENAI_API_KEY')\n )\n \n # Create minimal configuration\n config = AgentConfiguration(\n system_prompt=SYSTEM_PROMPT,\n avatar_url=AVATAR_URL\n )\n \n # Create shared state if not provided\n if shared_state is None:\n shared_state = AgentSharedState()\n \n # Create and return the agent\n agent = Agent(\n name=NAME,\n description=DESCRIPTION,\n chat_model=chat_model, # Using chat_model instead of llm\n tools=[], # Empty tools list\n agents=[], # No sub-agents\n state=shared_state,\n configuration=config,\n memory=MemorySaver()\n )\n \n return agent\n\n# For testing purposes\nif __name__ == \"__main__\":\n agent = create_agent()\n agent.run(\"Hello, I need help with $$MODULE_NAME\")\n" > src/custom/modules/$$MODULE_NAME/assistants/$${MODULE_NAME_PASCAL}Assistant.py && \ echo "Creating example workflow..." && \ echo "from pydantic import BaseModel, Field\nfrom typing import Optional, List, Dict, Any\nfrom fastapi import APIRouter\nfrom langchain_core.tools import StructuredTool\n\nclass $${MODULE_NAME_PASCAL}WorkflowConfiguration(BaseModel):\n \"\"\"Configuration for the $$MODULE_NAME_PASCAL Workflow.\"\"\"\n # Add configuration parameters here\n api_key: Optional[str] = Field(None, description=\"API key for external service\")\n\nclass $${MODULE_NAME_PASCAL}WorkflowParameters(BaseModel):\n \"\"\"Parameters for running the $$MODULE_NAME_PASCAL Workflow.\"\"\"\n # Add input parameters here\n query: str = Field(..., description=\"Query to process\")\n max_results: int = Field(10, description=\"Maximum number of results to return\")\n\nclass $${MODULE_NAME_PASCAL}WorkflowResult(BaseModel):\n \"\"\"Result of the $$MODULE_NAME_PASCAL Workflow.\"\"\"\n # Define the structure of the workflow results\n results: List[Dict[str, Any]] = Field(default_factory=list, description=\"List of results\")\n count: int = Field(0, description=\"Number of results found\")\n\nclass $${MODULE_NAME_PASCAL}Workflow:\n \"\"\"A workflow for $$MODULE_NAME operations.\"\"\"\n \n def __init__(self, configuration: $${MODULE_NAME_PASCAL}WorkflowConfiguration):\n self.__configuration = configuration\n \n def as_tools(self) -> list[StructuredTool]:\n \"\"\"Returns a list of LangChain tools for this workflow.\"\"\"\n return [StructuredTool(\n name=\"$${MODULE_NAME}_workflow\",\n description=\"Runs the $$MODULE_NAME_PASCAL workflow with the given parameters\",\n func=lambda **kwargs: self.run($${MODULE_NAME_PASCAL}WorkflowParameters(**kwargs)),\n args_schema=$${MODULE_NAME_PASCAL}WorkflowParameters\n )]\n \n def as_api(self, router: APIRouter) -> None:\n \"\"\"Adds API endpoints for this workflow to the given router.\"\"\"\n @router.post(\"/$${MODULE_NAME_PASCAL}Workflow\")\n def run(parameters: $${MODULE_NAME_PASCAL}WorkflowParameters):\n return self.run(parameters)\n \n def run(self, parameters: $${MODULE_NAME_PASCAL}WorkflowParameters) -> $${MODULE_NAME_PASCAL}WorkflowResult:\n \"\"\"Runs the workflow with the given parameters.\"\"\"\n # Implement your workflow logic here\n # This is a placeholder implementation\n \n # Example placeholder implementation\n results = [\n {\"id\": 1, \"name\": \"Result 1\", \"value\": \"Sample data 1\"},\n {\"id\": 2, \"name\": \"Result 2\", \"value\": \"Sample data 2\"},\n ]\n \n # Take only as many results as requested\n results = results[:parameters.max_results]\n \n return $${MODULE_NAME_PASCAL}WorkflowResult(\n results=results,\n count=len(results)\n )\n\n# For testing purposes\nif __name__ == \"__main__\":\n config = $${MODULE_NAME_PASCAL}WorkflowConfiguration()\n workflow = $${MODULE_NAME_PASCAL}Workflow(config)\n result = workflow.run($${MODULE_NAME_PASCAL}WorkflowParameters(query=\"test query\"))\n print(result)\n" > src/custom/modules/$$MODULE_NAME/workflows/$${MODULE_NAME_PASCAL}Workflow.py && \ echo "Creating example pipeline..." && \ @@ -181,3 +192,7 @@ build-module: echo "Creating sample test file..." && \ echo "import unittest\nfrom unittest.mock import MagicMock, patch\n\nclass Test$${MODULE_NAME_PASCAL}Module(unittest.TestCase):\n \"\"\"Test suite for the $$MODULE_NAME_PASCAL module.\"\"\"\n \n def setUp(self):\n \"\"\"Set up test fixtures.\"\"\"\n pass\n \n def tearDown(self):\n \"\"\"Tear down test fixtures.\"\"\"\n pass\n \n def test_assistant(self):\n \"\"\"Test the $$MODULE_NAME_PASCAL Assistant.\"\"\"\n try:\n from ..assistants.$${MODULE_NAME_PASCAL}Assistant import create_agent\n \n # Test agent creation\n agent = create_agent()\n self.assertIsNotNone(agent)\n \n # Additional tests for the assistant would go here\n except ImportError:\n self.skipTest(\"Assistant not implemented yet\")\n \n def test_workflow(self):\n \"\"\"Test the $$MODULE_NAME_PASCAL Workflow.\"\"\"\n try:\n from ..workflows.$${MODULE_NAME_PASCAL}Workflow import $${MODULE_NAME_PASCAL}Workflow, $${MODULE_NAME_PASCAL}WorkflowConfiguration, $${MODULE_NAME_PASCAL}WorkflowParameters\n \n # Test workflow initialization\n config = $${MODULE_NAME_PASCAL}WorkflowConfiguration()\n workflow = $${MODULE_NAME_PASCAL}Workflow(config)\n self.assertIsNotNone(workflow)\n \n # Test workflow execution\n params = $${MODULE_NAME_PASCAL}WorkflowParameters(query=\"test\")\n result = workflow.run(params)\n self.assertIsNotNone(result)\n \n # Test tool creation\n tools = workflow.as_tools()\n self.assertTrue(len(tools) > 0)\n except ImportError:\n self.skipTest(\"Workflow not implemented yet\")\n \n def test_pipeline(self):\n \"\"\"Test the $$MODULE_NAME_PASCAL Pipeline.\"\"\"\n try:\n from ..pipelines.$${MODULE_NAME_PASCAL}Pipeline import $${MODULE_NAME_PASCAL}Pipeline, $${MODULE_NAME_PASCAL}PipelineConfiguration, $${MODULE_NAME_PASCAL}PipelineParameters\n from abi.services.ontology_store import OntologyStoreService\n \n # Create a mock ontology store\n mock_store = MagicMock(spec=OntologyStoreService)\n \n # Test pipeline initialization\n config = $${MODULE_NAME_PASCAL}PipelineConfiguration(ontology_store=mock_store)\n pipeline = $${MODULE_NAME_PASCAL}Pipeline(config)\n self.assertIsNotNone(pipeline)\n \n # Test pipeline execution\n params = $${MODULE_NAME_PASCAL}PipelineParameters(entity_id=\"123\")\n result = pipeline.run(params)\n self.assertIsNotNone(result)\n \n # Test if result is a graph\n self.assertTrue(hasattr(result, 'serialize'))\n except ImportError:\n self.skipTest(\"Pipeline not implemented yet\")\n \n def test_integration(self):\n \"\"\"Test the $$MODULE_NAME_PASCAL Integration.\"\"\"\n try:\n from ..integrations.$${MODULE_NAME_PASCAL}Integration import $${MODULE_NAME_PASCAL}Integration, $${MODULE_NAME_PASCAL}IntegrationConfiguration, $${MODULE_NAME_PASCAL}SearchParameters\n from pydantic import SecretStr\n \n # Test integration initialization\n config = $${MODULE_NAME_PASCAL}IntegrationConfiguration(api_key=SecretStr(\"test_key\"))\n integration = $${MODULE_NAME_PASCAL}Integration(config)\n self.assertIsNotNone(integration)\n \n # Test search function\n params = $${MODULE_NAME_PASCAL}SearchParameters(query=\"test\")\n results = integration.search(params)\n self.assertIsNotNone(results)\n self.assertTrue(isinstance(results, list))\n except ImportError:\n self.skipTest(\"Integration not implemented yet\")\n\nif __name__ == '__main__':\n unittest.main()" > src/custom/modules/$$MODULE_NAME/tests/test_module.py && \ echo "Module '$$MODULE_NAME' built successfully in src/custom/modules/$$MODULE_NAME/" + +# Add debug target +debug-agents: .venv + @ docker compose run abi bash -c 'poetry install && poetry run python -m src.debug_agents' diff --git a/docs/storage/local.md b/docs/storage/local.md index 8a1ef161..3b133df4 100644 --- a/docs/storage/local.md +++ b/docs/storage/local.md @@ -27,18 +27,18 @@ storage/ │ ├── intermediate/ # Temporary processing results │ └── output/ # Final data products │ -├── triple_store/ # Semantic data storage +├── triplestore/ # Semantic data storage │ ├── ontologies/ # Ontology definitions (.owl, .rdf) │ └── triples/ # RDF triple data (.ttl) │ -└── vector_store/ # Vector embeddings +└── vectorstore/ # Vector embeddings ├── embeddings/ # Raw vector data ├── indexes/ # Vector search indexes └── metadata/ # Associated metadata ### Triple Store Structure -The `triple_store/` directory follows semantic web standards: +The `triplestore/` directory follows semantic web standards: - **ontologies/**: Contains schema definitions and ontology models - `.owl` files define formal ontologies with classes, properties, and rules @@ -50,7 +50,7 @@ The `triple_store/` directory follows semantic web standards: ### Vector Store Structure -The `vector_store/` directory is optimized for machine learning applications: +The `vectorstore/` directory is optimized for machine learning applications: - **embeddings/**: Contains raw vector data, typically in binary formats - Organized by model and dimension (e.g., `bert-base-768d/`) @@ -86,7 +86,7 @@ storage_service = ObjectStorageFactory.ObjectStorageServiceFS("/path/to/storage" # Basic operations # Store a file -storage_service.put_object("triple_store/triples", "people.ttl", ttl_content) +storage_service.put_object("triplestore/triples", "people.ttl", ttl_content) # Retrieve a file content = storage_service.get_object("data_lake/processed", "customers.json") @@ -95,7 +95,7 @@ content = storage_service.get_object("data_lake/processed", "customers.json") files = storage_service.list_objects("documents/pdf") # Delete a file -storage_service.delete_object("vector_store/embeddings", "temp_vectors.bin") +storage_service.delete_object("vectorstore/embeddings", "temp_vectors.bin") ``` ## Synchronization with Remote Storage @@ -117,11 +117,11 @@ These commands automatically handle the authentication and execute the AWS S3 sy 1. **Follow the standard directory structure** to ensure consistency and compatibility with other system components. 2. **Use appropriate directories** for different types of data: - - Document files → `documents/` - - Raw data → `data_lake/raw/` - - Processed data → `data_lake/processed/` - - RDF triples → `triple_store/triples/` - - Vector embeddings → `vector_store/embeddings/` + - Document files → `datastore/documents/` + - Raw data → `datastore/[module_name]/raw/` + - Processed data → `datastore/[module_name]/processed/` + - RDF triples → `triplestore/[module_name]/triples/` + - Vector embeddings → `vectorstore/[module_name]/embeddings/` 3. **Use consistent naming conventions**: - Use lowercase for directories and filenames @@ -132,9 +132,3 @@ These commands automatically handle the authentication and execute the AWS S3 sy 4. **Regularly synchronize** with remote storage to ensure data persistence and backup. 5. **Clean up temporary files** to prevent storage bloat and keep the system organized. - -## Related Documentation - -- [Remote Storage](./remote.md) -- [Triple Store Architecture](../architecture/triple_store.md) -- [Vector Embeddings](../machine_learning/embeddings.md) diff --git a/lib/abi/utils/Module.py b/lib/abi/utils/Module.py index 9555c146..ceb04512 100644 --- a/lib/abi/utils/Module.py +++ b/lib/abi/utils/Module.py @@ -38,6 +38,11 @@ def load(self): def __load_agents(self): + """Loads all agents defined in the module.""" + # Skip modules with a .skip file + if os.path.exists(os.path.join(self.module_path, '.skip')): + return + for file in os.listdir(os.path.join(self.module_path, 'assistants')): if file.endswith('.py'): assistant_path = self.module_import_path + '.assistants.' + file[:-3] diff --git a/src/core/modules/common/ontologies/domain-level/TransactionOntology.ttl b/src/core/modules/common/ontologies/domain-level/TransactionOntology.ttl index f2e6421e..fe9008d7 100644 --- a/src/core/modules/common/ontologies/domain-level/TransactionOntology.ttl +++ b/src/core/modules/common/ontologies/domain-level/TransactionOntology.ttl @@ -12,74 +12,170 @@ rdf:type owl:Ontology ; owl:imports ; owl:versionIRI ; - dc11:contributor "Jeremy Ravenel" , "Maxime Jublou" , "Florent Ravenel" ; - dc:description "Domain ontology for financial transactions."@en ; - dc:license "" ; - dc:title "Transaction Ontology" . + dc11:contributor "Jeremy Ravenel"^^xsd:string , "Maxime Jublou"^^xsd:string , "Florent Ravenel"^^xsd:string ; + dc:description "Domain ontology for transactions, including both cash and non-cash transactions."^^xsd:string ; + dc:license ""^^xsd:string ; + dc:title "Transaction Ontology"^^xsd:string . ################################################################# # Classes ################################################################# -abi:Transaction a owl:Class ; - rdfs:label "Transaction"@en ; +abi:Transaction rdf:type owl:Class ; + rdfs:label "Transaction"^^xsd:string ; rdfs:subClassOf bfo:BFO_0000015 ; # Process - skos:definition "A financial transaction with associated metadata"@en ; - skos:example """A transaction is a financial transaction with associated metadata"""@en . - -abi:TransactionStatus a owl:Class ; - rdfs:label "Transaction Status"@en ; - skos:definition "The status of a transaction"@en ; - skos:example "The status of a transaction is pending, completed, or failed"@en . + skos:definition "A financial or economic transaction with associated metadata, encompassing both monetary and non-monetary exchanges of value"^^xsd:string ; + skos:example "A transaction can be a financial transaction with associated metadata or a non-monetary exchange of value"^^xsd:string . + +abi:CashTransaction rdf:type owl:Class ; + rdfs:label "Cash Transaction"^^xsd:string ; + rdfs:subClassOf abi:Transaction ; + skos:definition "A transaction involving monetary exchange"^^xsd:string ; + skos:example "Payment for goods or services using money, digital currency, or other monetary instruments"^^xsd:string . + +abi:NonCashTransaction rdf:type owl:Class ; + rdfs:label "Non-Cash Transaction"^^xsd:string ; + rdfs:subClassOf abi:Transaction ; + skos:definition "A transaction involving the exchange of value without direct monetary payment"^^xsd:string ; + skos:example "Bartering goods, exchanging services, asset swaps, or in-kind contributions"^^xsd:string . + +abi:BarterTransaction rdf:type owl:Class ; + rdfs:label "Barter Transaction"^^xsd:string ; + rdfs:subClassOf abi:NonCashTransaction ; + skos:definition "A direct exchange of goods or services without using money"^^xsd:string ; + skos:example "Trading consultancy hours for office space"^^xsd:string . + +abi:AssetSwapTransaction rdf:type owl:Class ; + rdfs:label "Asset Swap Transaction"^^xsd:string ; + rdfs:subClassOf abi:NonCashTransaction ; + skos:definition "An exchange where assets are transferred between parties without monetary compensation"^^xsd:string ; + skos:example "Exchanging real estate properties, swapping securities, or transferring ownership rights"^^xsd:string . + +abi:InKindContribution rdf:type owl:Class ; + rdfs:label "In-Kind Contribution"^^xsd:string ; + rdfs:subClassOf abi:NonCashTransaction ; + skos:definition "A contribution of goods, services, or other non-monetary assets instead of cash"^^xsd:string ; + skos:example "Donating equipment to a non-profit organization, providing pro bono services"^^xsd:string . + +abi:TransactionStatus rdf:type owl:Class ; + rdfs:label "Transaction Status"^^xsd:string ; + skos:definition "The status of a transaction"^^xsd:string ; + skos:example "The status of a transaction is pending, completed, or failed"^^xsd:string . + +abi:TransactionType rdf:type owl:Class ; + rdfs:label "Transaction Type"^^xsd:string ; + skos:definition "The classification of a transaction as cash or non-cash"^^xsd:string . + +abi:Cash rdf:type owl:Class ; + rdfs:label "Cash"^^xsd:string ; + rdfs:subClassOf abi:TransactionType ; + skos:definition "Indicates a transaction involving monetary exchange"^^xsd:string . + +abi:NonCash rdf:type owl:Class ; + rdfs:label "Non-Cash"^^xsd:string ; + rdfs:subClassOf abi:TransactionType ; + skos:definition "Indicates a transaction not involving direct monetary exchange"^^xsd:string . ################################################################# # Object Properties ################################################################# -abi:hasAsset a owl:ObjectProperty ; +abi:hasTransactionType rdf:type owl:ObjectProperty ; + rdfs:label "has transaction type"^^xsd:string ; + rdfs:domain abi:Transaction ; + rdfs:range abi:TransactionType ; + skos:definition "Specifies whether the transaction is cash or non-cash"^^xsd:string . + +abi:hasAsset rdf:type owl:ObjectProperty ; rdfs:domain abi:Transaction ; rdfs:range abi:Asset . -abi:hasContract a owl:ObjectProperty ; +abi:hasContract rdf:type owl:ObjectProperty ; rdfs:domain abi:Transaction ; rdfs:range abi:Contract . -abi:hasSender a owl:ObjectProperty ; +abi:hasSender rdf:type owl:ObjectProperty ; rdfs:domain abi:Transaction ; rdfs:range abi:Contact . -abi:hasRecipient a owl:ObjectProperty ; +abi:hasRecipient rdf:type owl:ObjectProperty ; rdfs:domain abi:Transaction ; rdfs:range abi:Contact . -abi:hasOrganization a owl:ObjectProperty ; +abi:hasOrganization rdf:type owl:ObjectProperty ; rdfs:domain abi:Transaction ; rdfs:range abi:Organization . -abi:hasCurrency a owl:ObjectProperty ; - rdfs:domain abi:Transaction ; +abi:hasCurrency rdf:type owl:ObjectProperty ; + rdfs:domain abi:CashTransaction ; rdfs:range abi:Currency . -abi:hasStatus a owl:ObjectProperty ; +abi:hasStatus rdf:type owl:ObjectProperty ; rdfs:domain abi:Transaction ; rdfs:range abi:TransactionStatus . -abi:hasCategory a owl:ObjectProperty ; +abi:hasCategory rdf:type owl:ObjectProperty ; rdfs:domain abi:Transaction ; rdfs:range abi:TransactionCategory . -abi:hasPaymentMethod a owl:ObjectProperty ; - rdfs:domain abi:Transaction ; +abi:hasPaymentMethod rdf:type owl:ObjectProperty ; + rdfs:domain abi:CashTransaction ; rdfs:range abi:PaymentMethod . -abi:hasInvoice a owl:ObjectProperty ; +abi:hasInvoice rdf:type owl:ObjectProperty ; rdfs:domain abi:Transaction ; rdfs:range abi:Invoice . +abi:involvesExchangedService rdf:type owl:ObjectProperty ; + rdfs:domain abi:NonCashTransaction ; + rdfs:range abi:Service ; + skos:definition "Specifies the service exchanged in a non-cash transaction"^^xsd:string . + +abi:involvesExchangedAsset rdf:type owl:ObjectProperty ; + rdfs:domain abi:NonCashTransaction ; + rdfs:range abi:Asset ; + skos:definition "Specifies the asset exchanged in a non-cash transaction"^^xsd:string . + ################################################################# # Data Properties ################################################################# -abi:amount a owl:DatatypeProperty ; +abi:amount rdf:type owl:DatatypeProperty ; + rdfs:domain abi:CashTransaction ; + rdfs:range xsd:decimal ; + skos:definition "The monetary amount involved in a cash transaction"^^xsd:string ; + skos:example "100.50"^^xsd:decimal . + +abi:estimatedMonetaryValue rdf:type owl:DatatypeProperty ; + rdfs:domain abi:NonCashTransaction ; + rdfs:range xsd:decimal ; + skos:definition "The estimated monetary value of a non-cash transaction"^^xsd:string ; + skos:example "1500.00"^^xsd:decimal . + +abi:description rdf:type owl:DatatypeProperty ; rdfs:domain abi:Transaction ; - rdfs:range xsd:decimal . \ No newline at end of file + rdfs:range xsd:string ; + skos:definition "A textual description of the transaction"^^xsd:string ; + skos:example "Payment for consulting services - Q1 2024"^^xsd:string . + +################################################################# +# Individuals +################################################################# + +abi:PendingStatus rdf:type owl:NamedIndividual, abi:TransactionStatus ; + rdfs:label "Pending Status"^^xsd:string ; + skos:definition "Indicates a transaction that has been initiated but not yet completed"^^xsd:string . + +abi:CompletedStatus rdf:type owl:NamedIndividual, abi:TransactionStatus ; + rdfs:label "Completed Status"^^xsd:string ; + skos:definition "Indicates a transaction that has been successfully completed"^^xsd:string . + +abi:FailedStatus rdf:type owl:NamedIndividual, abi:TransactionStatus ; + rdfs:label "Failed Status"^^xsd:string ; + skos:definition "Indicates a transaction that could not be completed successfully"^^xsd:string . + +abi:CashType rdf:type owl:NamedIndividual, abi:Cash ; + rdfs:label "Cash Type"^^xsd:string . + +abi:NonCashType rdf:type owl:NamedIndividual, abi:NonCash ; + rdfs:label "Non-Cash Type"^^xsd:string . \ No newline at end of file diff --git a/src/custom/modules/bitcoin/README.md b/src/custom/modules/bitcoin/README.md new file mode 100644 index 00000000..07e82ee6 --- /dev/null +++ b/src/custom/modules/bitcoin/README.md @@ -0,0 +1,20 @@ +# Bitcoin Module + +This module provides Bitcoin-related functionality for the ABI platform, including: + +- Price tracking and history +- Blockchain data analysis +- Transaction simulation + +## Integrations +- **BitcoinPriceIntegration**: Connects to external price APIs (Yahoo Finance, CoinGecko) + +## Assistants +- **BitcoinAssistant**: Provides conversational interface for Bitcoin information + +``` +/src/custom/modules/bitcoin/ +├── assistants/ # Bitcoin assistant implementation +├── analytics/ # Bitcoin price analytics and charts +├── integration/ # Bitcoin blockchain integration +``` \ No newline at end of file diff --git a/src/custom/modules/bitcoin/__init__.py b/src/custom/modules/bitcoin/__init__.py new file mode 100644 index 00000000..cd4a5476 --- /dev/null +++ b/src/custom/modules/bitcoin/__init__.py @@ -0,0 +1,2 @@ +# Import your module components here +from src.custom.modules.bitcoin.assistants.BitcoinAssistant import create_agent, create_bitcoin_agent, BitcoinAssistant \ No newline at end of file diff --git a/src/custom/modules/bitcoin/adapters/BitcoinPriceAdapter.py b/src/custom/modules/bitcoin/adapters/BitcoinPriceAdapter.py new file mode 100644 index 00000000..3813019d --- /dev/null +++ b/src/custom/modules/bitcoin/adapters/BitcoinPriceAdapter.py @@ -0,0 +1,16 @@ +def format_bitcoin_price_response(price_data): + """Format Bitcoin price data for display in the chat.""" + if "error" in price_data: + return f"Sorry, I couldn't retrieve the Bitcoin price: {price_data['error']}" + + # Use the price_formatted field if available, otherwise format it ourselves + if "price_formatted" in price_data: + price_display = price_data["price_formatted"] + else: + # Ensure proper formatting if price_formatted isn't available + price_display = f"{price_data['price']:,.2f}" + + source = price_data.get("source", "Unknown") + currency = price_data.get("currency", "USD") + + return f"The current price of Bitcoin (BTC) is ${price_display} {currency} (Source: {source})" \ No newline at end of file diff --git a/src/custom/modules/bitcoin/assistants/BitcoinAssistant.py b/src/custom/modules/bitcoin/assistants/BitcoinAssistant.py new file mode 100644 index 00000000..f427c795 --- /dev/null +++ b/src/custom/modules/bitcoin/assistants/BitcoinAssistant.py @@ -0,0 +1,174 @@ +""" +Bitcoin Assistant + +This module provides Bitcoin-related assistance and blockchain information. +""" +# Original imports replaced with new paths +from langchain_openai import ChatOpenAI +from abi.services.agent.Agent import Agent, AgentConfiguration, AgentSharedState, MemorySaver +from src import secret, services +from fastapi import APIRouter +from typing import Optional, List +from langchain_core.tools import Tool + +# Constants for configuration +NAME = "BitcoinAssistant" +SLUG = "bitcoin-assistant" +DESCRIPTION = "Analyzes Bitcoin transactions and provides insights on the blockchain. Generates simulated transaction data for testing and development purposes." +MODEL = "gpt-4-turbo" +TEMPERATURE = 0.7 +AVATAR_URL = "https://logo.clearbit.com/bitcoin.org" +SYSTEM_PROMPT = """You are the Bitcoin Assistant, an expert in Bitcoin and cryptocurrency analysis. + +Your responsibilities include: +- Providing information on Bitcoin technology and concepts +- Analyzing blockchain data and transaction patterns +- Generating simulated Bitcoin transactions for testing +- Answering questions about Bitcoin price trends and market data +- Explaining blockchain technology concepts + +Always be helpful, accurate, and prioritize clear explanations about Bitcoin technology. +""" + +SUGGESTIONS = [ + "What is the current Bitcoin price?", + "Explain how Bitcoin mining works", + "Generate a sample Bitcoin transaction", + "What is the Bitcoin halving?", + "Show me a chart of Bitcoin price history", + "How does the Lightning Network work?" +] + +def create_agent( + agent_shared_state: AgentSharedState = None, + agent_configuration: AgentConfiguration = None +) -> Agent: + """Creates a Bitcoin agent instance.""" + + # Configure the chat model + model = ChatOpenAI( + model=MODEL, + temperature=TEMPERATURE, + api_key=secret.get('OPENAI_API_KEY') + ) + + # Initialize tools + tools = [] + + # Import and use the Bitcoin price integration + from src.custom.modules.bitcoin.integrations.BitcoinPriceIntegration import BitcoinPriceIntegration, BitcoinPriceIntegrationConfiguration + + # Initialize the integration + bitcoin_price_integration = BitcoinPriceIntegration(BitcoinPriceIntegrationConfiguration()) + + # Add the integration's tools + tools.extend(bitcoin_price_integration.as_tools()) + + # Add the Bitcoin price pipeline tool + from src.custom.modules.bitcoin.pipeline.BitcoinPricePipeline import BitcoinPricePipeline + pipeline = BitcoinPricePipeline() + + tools.append( + Tool( + name="get_bitcoin_price_history", + description="Get stored historical Bitcoin price data", + func=lambda start_date=None, end_date=None: pipeline.get_stored_prices( + start_date=start_date, end_date=end_date + ) + ) + ) + + tools.append( + Tool( + name="store_bitcoin_price_history", + description="Store Bitcoin price history for a specified number of days", + func=lambda days=7: pipeline.store_price_history(days=int(days)) + ) + ) + + tools.append( + Tool( + name="get_bitcoin_price_analysis", + description="Get comprehensive Bitcoin price analysis with historical data", + func=lambda days=7: pipeline.run(days=int(days)) + ) + ) + + # Initialize configuration if not provided + if agent_configuration is None: + agent_configuration = AgentConfiguration( + system_prompt=SYSTEM_PROMPT + ) + + # Initialize shared state if not provided + if agent_shared_state is None: + agent_shared_state = AgentSharedState() + + # Create and return the agent + return BitcoinAssistant( + name=NAME.lower().replace(" ", "_"), + description=DESCRIPTION, + chat_model=model, + tools=tools, + state=agent_shared_state, + configuration=agent_configuration, + memory=MemorySaver() + ) + +# Alias for backward compatibility with tests +create_bitcoin_agent = create_agent + +class BitcoinAssistant(Agent): + """Bitcoin Assistant specialized agent class.""" + + def as_api( + self, + router: APIRouter, + route_name: str = "bitcoin", + name: str = NAME, + description: str = "API endpoints to call the Bitcoin assistant completion.", + description_stream: str = "API endpoints to call the Bitcoin assistant stream completion.", + tags: List[str] = [] + ): + return super().as_api(router, route_name, name, description, description_stream, tags) + + def get_tools(self): + tools = super().get_tools() + + # Add the Bitcoin price pipeline tool + from src.custom.modules.bitcoin.pipeline.BitcoinPricePipeline import BitcoinPricePipeline + pipeline = BitcoinPricePipeline() + + tools.append( + Tool( + name="get_bitcoin_price_history", + description="Get stored historical Bitcoin price data", + func=lambda start_date=None, end_date=None: pipeline.get_stored_prices( + start_date=start_date, end_date=end_date + ) + ) + ) + + tools.append( + Tool( + name="store_bitcoin_price_history", + description="Store Bitcoin price history for a specified number of days", + func=lambda days=7: pipeline.store_price_history(days=int(days)) + ) + ) + + tools.append( + Tool( + name="get_bitcoin_price_analysis", + description="Get comprehensive Bitcoin price analysis with historical data", + func=lambda days=7: pipeline.run(days=int(days)) + ) + ) + + return tools + +# For direct execution +if __name__ == "__main__": + agent = create_agent() + response = agent.invoke("What is the current Bitcoin price?") + print(f"Response: {response}") \ No newline at end of file diff --git a/src/custom/modules/bitcoin/assistants/__init__.py b/src/custom/modules/bitcoin/assistants/__init__.py new file mode 100644 index 00000000..5c6e0031 --- /dev/null +++ b/src/custom/modules/bitcoin/assistants/__init__.py @@ -0,0 +1,8 @@ +""" +Bitcoin Assistants Module + +This module provides conversation agents specialized in Bitcoin topics. +""" + +from src.custom.modules.bitcoin.assistants.BitcoinAssistant import BitcoinAssistant +from src.custom.modules.bitcoin.assistants.BitcoinAssistant import create_agent, create_bitcoin_agent \ No newline at end of file diff --git a/src/custom/modules/bitcoin/integrations/BitcoinPriceIntegration.py b/src/custom/modules/bitcoin/integrations/BitcoinPriceIntegration.py new file mode 100644 index 00000000..db6c94d6 --- /dev/null +++ b/src/custom/modules/bitcoin/integrations/BitcoinPriceIntegration.py @@ -0,0 +1,185 @@ +""" +Bitcoin Price Integration + +Provides integration with Bitcoin price data sources like Yahoo Finance and CoinGecko. +""" +from dataclasses import dataclass +from datetime import datetime +from typing import Dict, Any, Optional, List +import yfinance as yf +import requests +import logging + +from langchain_core.tools import Tool + +# Base classes for integration pattern +class IntegrationConfiguration: + """Base configuration class for integrations.""" + pass + +class Integration: + """Base class for integrations.""" + def __init__(self, configuration): + self.configuration = configuration + + def as_tools(self): + """Return the integration's tools.""" + return [] + +@dataclass +class BitcoinPriceIntegrationConfiguration(IntegrationConfiguration): + """Configuration for BitcoinPriceIntegration. + + Attributes: + coingecko_api_key (str, optional): API key for the CoinGecko Pro API. + """ + coingecko_api_key: Optional[str] = None + coingecko_base_url: str = "https://api.coingecko.com/api/v3" + +class BitcoinPriceIntegration(Integration): + """Integration with Bitcoin price data sources.""" + + def __init__(self, configuration: BitcoinPriceIntegrationConfiguration): + super().__init__(configuration) + self.config = configuration + + def get_current_price(self, store: bool = True) -> Dict[str, Any]: + """Get current Bitcoin price from available sources. + + Args: + store: Whether to store the price data (requires pipeline to be initialized) + + Returns: + Dict with price data or error message + """ + # Get price from providers + price_data = self._get_price_from_providers() + + # Ensure the price is displayed with proper formatting in the agent response + if "price" in price_data and "price_formatted" in price_data: + # Create a copy to avoid modifying the data we'll store + display_data = price_data.copy() + # Add formatted price as a separate field for the agent to use + display_data["display_price"] = f"${price_data['price_formatted']}" + + # If store flag is True and we're connected to a pipeline, store the data + if store and hasattr(self, '_pipeline'): + try: + self._pipeline._store_price_data(price_data) + except Exception as e: + logging.warning(f"Failed to store price data: {e}") + + return display_data + + return price_data + + def get_historical_prices(self, days: int = 30, *args) -> Dict[str, Any]: + """Get historical Bitcoin prices for charting. + + Args: + days (int): Number of days of historical data to retrieve. + + Returns: + Dict[str, Any]: Historical price data with dates and prices. + """ + try: + btc_data = yf.Ticker("BTC-USD") + hist = btc_data.history(period=f"{days}d") + + dates = hist.index.strftime('%Y-%m-%d').tolist() + prices = [float(f"{price:.2f}") for price in hist['Close'].tolist()] + + return { + "dates": dates, + "prices": prices, + "currency": "USD", + "source": "Yahoo Finance", + "period_days": days + } + except Exception as e: + print(f"Error fetching historical data: {e}") + return { + "error": "Unable to fetch historical Bitcoin prices", + "timestamp": datetime.now().isoformat() + } + + def as_tools(self) -> List[Tool]: + """Return the tools for the integration. + + Returns: + List[Tool]: List of tools for the integration. + """ + return [ + Tool( + name="get_bitcoin_price", + description="Gets the current Bitcoin price in USD", + func=self.get_current_price + ), + Tool( + name="get_historical_bitcoin_prices", + description="Gets historical Bitcoin prices for the specified number of days", + func=self.get_historical_prices + ) + ] + + def _get_price_from_providers(self) -> Dict[str, Any]: + """Get the current Bitcoin price from multiple sources and return a consensus.""" + # Try Yahoo Finance first + try: + btc_data = yf.Ticker("BTC-USD") + price_info = btc_data.info + current_price = price_info.get('regularMarketPrice', 0) + + if current_price > 0: + # Format price with commas and 2 decimal places (no $ sign) + return { + "price": current_price, + "price_formatted": f"{current_price:,.2f}", + "currency": "USD", + "source": "Yahoo Finance", + "timestamp": datetime.now().isoformat() + } + except Exception as e: + print(f"Error fetching from Yahoo Finance: {e}") + + # Try CoinGecko as backup + try: + headers = {"accept": "application/json"} + if self.config.coingecko_api_key: + headers["x-cg-pro-api-key"] = self.config.coingecko_api_key + + # Request the price with full precision + response = requests.get( + f"{self.config.coingecko_base_url}/simple/price", + params={ + "ids": "bitcoin", + "vs_currencies": "usd", + "precision": "full" # Request full precision + }, + headers=headers + ) + data = response.json() + + if "bitcoin" in data and "usd" in data["bitcoin"]: + btc_price = data['bitcoin']['usd'] + print(f"Raw API price: {btc_price} (type: {type(btc_price).__name__})") + + # Format price with commas and 2 decimal places (no $ sign) + formatted_price = f"{btc_price:,.2f}" + print(f"Formatted price: {formatted_price}") + + return { + "price": btc_price, + "price_formatted": formatted_price, + "currency": "USD", + "source": "CoinGecko", + "timestamp": datetime.now().isoformat() + } + except Exception as e: + print(f"Error fetching from CoinGecko: {e}") + + # Return a fallback message if all sources fail + return { + "error": "Unable to fetch current Bitcoin price", + "timestamp": datetime.now().isoformat() + } \ No newline at end of file diff --git a/src/custom/modules/bitcoin/pipeline/BitcoinPricePipeline.py b/src/custom/modules/bitcoin/pipeline/BitcoinPricePipeline.py new file mode 100644 index 00000000..279d6d6e --- /dev/null +++ b/src/custom/modules/bitcoin/pipeline/BitcoinPricePipeline.py @@ -0,0 +1,344 @@ +""" +Bitcoin Price Pipeline + +A pipeline for processing and analyzing Bitcoin price data +""" +from typing import Dict, Any, Optional, List +from datetime import datetime, date +import os +import json +import logging +import tempfile +import stat + +from abi.pipeline import Pipeline +from langchain_core.tools import Tool +from fastapi import APIRouter +from src.custom.modules.bitcoin.integrations.BitcoinPriceIntegration import ( + BitcoinPriceIntegration, + BitcoinPriceIntegrationConfiguration +) + +class BitcoinPricePipeline(Pipeline): + """Pipeline for processing Bitcoin price data.""" + + def __init__(self): + # Create integration + self.integration = BitcoinPriceIntegration( + BitcoinPriceIntegrationConfiguration() + ) + # Connect pipeline back to integration for storage functionality + self.integration._pipeline = self + + # Focus on the requested storage path + storage_path = "/app/src/custom/modules/bitcoin/data" + print(f"Using Bitcoin data storage path: {storage_path}") + + # Set environment variables based on directory structure + storage_base_path = os.environ.get("BITCOIN_DATA_PATH", storage_path) + # Allow override for datastore path + if "BITCOIN_DATASTORE_PATH" in os.environ: + self.storage_base_path = os.environ.get("BITCOIN_DATASTORE_PATH") + print(f"Using environment-specified datastore path: {self.storage_base_path}") + else: + # Default to original path or environment variable + self.storage_base_path = storage_base_path + + # Try to detect if we're running in development mode + current_file = os.path.abspath(__file__) + if "src/custom/modules" in current_file: + # We're likely in development mode, try to use storage/datastore + repo_root = os.path.abspath(os.path.join(os.path.dirname(current_file), "../../../../../")) + possible_datastore = os.path.join(repo_root, "storage", "datastore") + if os.path.exists(os.path.dirname(possible_datastore)): + self.storage_base_path = possible_datastore + print(f"Development mode detected, using datastore path: {self.storage_base_path}") + + self.prices_path = os.path.join(self.storage_base_path, "bitcoin/prices") + + # Make sure parent directories exist with proper permissions + try: + os.makedirs(self.prices_path, exist_ok=True) + print(f"Created or verified prices path: {self.prices_path}") + except Exception as e: + print(f"Error setting up prices directory: {e}") + + # Print debug info at initialization + print(f"Bitcoin price data will be stored in: {self.prices_path}") + + # Store initial price data on startup + try: + initial_price = self.integration.get_current_price() + if "error" not in initial_price: + self._store_price_data(initial_price) + print("✅ Successfully stored initial Bitcoin price data on startup") + else: + print(f"⚠️ Could not store initial price data: {initial_price.get('error')}") + except Exception as e: + print(f"⚠️ Error storing initial price data: {e}") + + def as_tools(self) -> List[Tool]: + """Return tools for this pipeline. + + Returns: + List of tools for accessing pipeline functionality + """ + return [ + Tool( + name="get_bitcoin_price_data", + description="Get current and historical Bitcoin price data", + func=lambda days=0: self.run(days=int(days)) + ), + Tool( + name="get_stored_bitcoin_prices", + description="Retrieve stored Bitcoin price data", + func=lambda start_date=None, end_date=None: self.get_stored_prices( + start_date=start_date, end_date=end_date + ) + ), + Tool( + name="store_bitcoin_price_history", + description="Store Bitcoin price history for a specified number of days", + func=lambda days=7: self.store_price_history(days=int(days)) + ) + ] + + def as_api(self, router: APIRouter) -> None: + """Register API endpoints for this pipeline. + + Args: + router: FastAPI router to register endpoints with + """ + @router.get("/bitcoin/price/current") + def get_current_price(): + """Get current Bitcoin price.""" + return self.run(days=0) + + @router.get("/bitcoin/price/historical/{days}") + def get_historical_prices(days: int = 7): + """Get historical Bitcoin price data.""" + return self.run(days=days) + + @router.get("/bitcoin/price/stored") + def get_stored_prices(start_date: str = None, end_date: str = None): + """Get stored Bitcoin price data.""" + return self.get_stored_prices(start_date=start_date, end_date=end_date) + + @router.post("/bitcoin/price/store/{days}") + def store_price_history(days: int = 7): + """Store Bitcoin price history for specified days.""" + return self.store_price_history(days=days) + + def run(self, days: int = 0, source: str = None, store: bool = True) -> Dict[str, Any]: + """Run the Bitcoin price pipeline. + + Args: + days: Number of days for historical data (0 = current price only) + source: Preferred data source (None = use default fallback priority) + store: Whether to store the results (defaults to True) + + Returns: + Dict containing price data and analysis + """ + result = {} + + # Get current price + current_price = self.integration.get_current_price() + result["current_price"] = current_price + + # Store current price data + storage_status = "not_attempted" + if store and "error" not in current_price: + try: + self._store_price_data(current_price) + storage_status = "success" + except Exception as e: + storage_status = f"failed: {str(e)}" + logging.error(f"Failed to store Bitcoin price data: {e}") + + # Add storage status to result + result["storage"] = { + "attempted": store and "error" not in current_price, + "status": storage_status, + "location": self.prices_path if storage_status == "success" else None + } + + # Get historical prices if requested + if days > 0: + historical = self.integration.get_historical_prices(days) + result["historical"] = historical + + # Add basic analysis when historical data is available + if "error" not in historical and len(historical.get("prices", [])) > 0: + prices = historical["prices"] + result["analysis"] = { + "average": round(sum(prices) / len(prices), 2), + "max": max(prices), + "min": min(prices), + "volatility": round(max(prices) - min(prices), 2), + "days_analyzed": days + } + + return result + + def _store_price_data(self, price_data: Dict[str, Any]) -> bool: + """Store price data for later retrieval and returns success status. + + Args: + price_data: Price data dictionary to store + + Returns: + True if storage was successful, False otherwise + + Raises: + IOError: If there are file access issues + ValueError: If the data cannot be serialized + """ + print(f"Attempting to store price data in {self.prices_path}") + + # Double-check the directory exists and is writable + try: + os.makedirs(self.prices_path, exist_ok=True) + except Exception as e: + print(f"Failed to create directory {self.prices_path}: {e}") + # Try a fallback to temp directory + self.prices_path = os.path.join(tempfile.mkdtemp(prefix="bitcoin_"), "prices") + os.makedirs(self.prices_path, exist_ok=True) + print(f"Using fallback path: {self.prices_path}") + + # Get current date and time + now = datetime.now() + date_str = now.strftime('%Y%m%d') + timestamp_str = now.strftime('%Y%m%dT%H%M%S') + 'Z' + + # Create dated folder within prices directory + dated_folder = os.path.join(self.prices_path, date_str) + os.makedirs(dated_folder, exist_ok=True) + print(f"Created or verified dated folder: {dated_folder}") + + # Create a filename with the new format + filename = os.path.join(dated_folder, f"{timestamp_str}_BTCUSD_price.json") + print(f"Will store data in file: {filename}") + + # Enhance price data with additional fields if not present + if isinstance(price_data, dict) and "timestamp" not in price_data: + price_data["timestamp"] = now.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + if isinstance(price_data, dict) and "last_updated_at" not in price_data: + price_data["last_updated_at"] = now.timestamp() + if isinstance(price_data, dict) and "currency" not in price_data: + price_data["currency"] = "USD" + + # Store data as a list to match test format + data = [price_data] + + # Write the data to the file + print(f"Writing data to {filename}") + with open(filename, 'w') as f: + json.dump(data, f, indent=4) + + # Verify file was actually created and is readable + if os.path.exists(filename): + try: + with open(filename, 'r') as f: + verification_data = json.load(f) + file_size = os.path.getsize(filename) + print(f"✅ VERIFICATION SUCCESSFUL: File exists ({file_size} bytes) and contains {len(verification_data)} records") + except Exception as e: + print(f"❌ VERIFICATION FAILED: File exists but cannot be read: {e}") + else: + print(f"❌ VERIFICATION FAILED: File does not exist after write operation") + + logging.info(f"Stored Bitcoin price data for {now.isoformat()}") + print(f"Bitcoin price data stored in {filename}") + + return True + + def get_stored_prices(self, start_date: str = None, end_date: str = None) -> List[Dict[str, Any]]: + """Retrieve stored price data. + + Args: + start_date: Optional start date in ISO format (YYYY-MM-DD) + end_date: Optional end date in ISO format (YYYY-MM-DD) + + Returns: + List of stored price data dictionaries + """ + result = [] + + try: + # Check if prices directory exists + if not os.path.exists(self.prices_path): + logging.warning(f"Prices directory {self.prices_path} does not exist") + return [] + + # Get list of date folders + date_folders = [f for f in os.listdir(self.prices_path) + if os.path.isdir(os.path.join(self.prices_path, f))] + + # Filter by date range if specified + if start_date or end_date: + filtered_folders = [] + for folder in date_folders: + # Convert to ISO format for comparison + folder_date = f"{folder[:4]}-{folder[4:6]}-{folder[6:8]}" + if start_date and folder_date < start_date: + continue + if end_date and folder_date > end_date: + continue + filtered_folders.append(folder) + date_folders = filtered_folders + + # Process each date folder + for date_folder in sorted(date_folders): + folder_path = os.path.join(self.prices_path, date_folder) + + # Get all JSON files in this date folder + json_files = [f for f in os.listdir(folder_path) if f.endswith('.json')] + + # Load and append data from each file + for filename in sorted(json_files): + file_path = os.path.join(folder_path, filename) + with open(file_path, 'r') as f: + file_data = json.load(f) + # If file data is a list, extend result with its contents + if isinstance(file_data, list): + result.extend(file_data) + # If file data is a dict, append it directly + elif isinstance(file_data, dict): + result.append(file_data) + + return result + except Exception as e: + logging.error(f"Failed to retrieve stored Bitcoin price data: {e}") + return [] + + def get_price_summary(self) -> Dict[str, Any]: + """Get a summary of Bitcoin price with 7-day analysis. + + Returns: + Dict containing current price and 7-day analysis + """ + return self.run(days=7) + + def store_price_history(self, days: int = 7) -> Dict[str, Any]: + """Explicitly store Bitcoin price history for a specified number of days. + + Args: + days: Number of days of price history to store (default 7) + + Returns: + Dict containing storage result and summary of stored data + """ + # Run the pipeline with explicit storage flag + result = self.run(days=days, store=True) + + # Add additional storage information + result["storage_summary"] = { + "days_requested": days, + "timestamp": datetime.now().isoformat(), + "storage_path": self.prices_path, + "message": f"Stored {days} days of Bitcoin price history" + } + + print(f"Explicitly stored {days} days of Bitcoin price history") + return result \ No newline at end of file diff --git a/src/custom/modules/bitcoin/tests/run_price_validation.py b/src/custom/modules/bitcoin/tests/run_price_validation.py new file mode 100644 index 00000000..4e5f4f42 --- /dev/null +++ b/src/custom/modules/bitcoin/tests/run_price_validation.py @@ -0,0 +1,50 @@ +""" +Test runner for Bitcoin price validation tests +""" +import unittest +import sys +from argparse import ArgumentParser + +from src.custom.modules.bitcoin.tests.test_price_validation import ( + validate_bitcoin_price, + test_bitcoin_agent_price_accuracy, + get_bitcoin_price, + extract_price_from_llm_response +) +from src.custom.modules.bitcoin.assistants.BitcoinAssistant import create_agent as create_bitcoin_agent +from src.custom.modules.bitcoin.models import ModelConfig, ModelProvider + +def run_validation_tests(verbose=False): + """Run Bitcoin price validation tests.""" + print("Running Bitcoin Price Validation Tests") + print("=====================================") + + # Get current price for reference + price_data = get_bitcoin_price() + if "error" in price_data: + print(f"Error: {price_data['error']}") + return False + + print(f"Current Bitcoin price: ${price_data['price']} ({price_data['source']})") + + # Test agent price accuracy + print("\nTesting Bitcoin agent price accuracy...") + test_result = test_bitcoin_agent_price_accuracy() + + print(f"Query: {test_result['query']}") + if verbose: + print(f"Response: {test_result['llm_response']}") + print(f"Extracted price: ${test_result['extracted_price']}") + print(f"Reference price: ${test_result['reference_price']} ({test_result['reference_source']})") + print(f"Result: {'✅ PASSED' if test_result['passed'] else '❌ FAILED'}") + print(f"Details: {test_result['validation_message']}") + + return test_result['passed'] + +if __name__ == "__main__": + parser = ArgumentParser(description="Bitcoin Price Validation Test Runner") + parser.add_argument("-v", "--verbose", action="store_true", help="Show verbose output") + args = parser.parse_args() + + success = run_validation_tests(verbose=args.verbose) + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/src/custom/modules/bitcoin/tests/test_bitcoin_price_pipeline.py b/src/custom/modules/bitcoin/tests/test_bitcoin_price_pipeline.py new file mode 100644 index 00000000..a003f982 --- /dev/null +++ b/src/custom/modules/bitcoin/tests/test_bitcoin_price_pipeline.py @@ -0,0 +1,56 @@ +""" +Tests for Bitcoin Price Pipeline +""" +import unittest +from src.custom.modules.bitcoin.pipeline.BitcoinPricePipeline import BitcoinPricePipeline + +class TestBitcoinPricePipeline(unittest.TestCase): + def setUp(self): + self.pipeline = BitcoinPricePipeline() + + def test_current_price(self): + """Test that the pipeline returns current price data.""" + result = self.pipeline.run() + self.assertIn("current_price", result) + self.assertIsInstance(result["current_price"], dict) + + # If no errors, price should be present + if "error" not in result["current_price"]: + self.assertIn("price", result["current_price"]) + self.assertIn("currency", result["current_price"]) + + def test_historical_analysis(self): + """Test that the pipeline returns historical analysis.""" + days = 7 + result = self.pipeline.run(days=days) + + self.assertIn("historical", result) + + # If we got historical data, check analysis + if "error" not in result.get("historical", {}): + self.assertIn("analysis", result) + self.assertIn("average", result["analysis"]) + self.assertIn("max", result["analysis"]) + self.assertIn("min", result["analysis"]) + self.assertIn("volatility", result["analysis"]) + self.assertEqual(result["analysis"]["days_analyzed"], days) + + def test_data_storage(self): + """Test storing and retrieving price data.""" + # Run pipeline with storage + self.pipeline.run(store=True) + + # Retrieve stored data + stored_data = self.pipeline.get_stored_prices() + + # Verify we have at least one stored price entry + self.assertGreater(len(stored_data), 0) + + # Verify the stored data structure + if stored_data: + self.assertIn("price", stored_data[0]) + self.assertIn("currency", stored_data[0]) + self.assertIn("timestamp", stored_data[0]) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/custom/modules/bitcoin/tests/test_price_providers.py b/src/custom/modules/bitcoin/tests/test_price_providers.py new file mode 100644 index 00000000..4aaa9ec2 --- /dev/null +++ b/src/custom/modules/bitcoin/tests/test_price_providers.py @@ -0,0 +1,67 @@ +""" +Tests for Bitcoin price data providers +""" +import unittest +from typing import Dict, Any + +from src.custom.modules.bitcoin.assistants.BitcoinAssistant import create_agent as create_bitcoin_agent +from src.custom.modules.bitcoin.tests.test_price_validation import ( + extract_price_from_llm_response, + PriceData, + get_bitcoin_price, + get_yahoo_bitcoin_price, + get_coingecko_bitcoin_price +) + +class TestBitcoinPriceProviders(unittest.TestCase): + """Test cases for Bitcoin price providers.""" + + def test_integration_price_provider(self): + """Test that the integrated price provider returns valid data.""" + price_data = get_bitcoin_price() + + # Check if error was returned + if "error" in price_data: + self.fail(f"Price provider returned error: {price_data['error']}") + + # Validate price data + self.assertIn("price", price_data, "Price data missing 'price' field") + self.assertIn("currency", price_data, "Price data missing 'currency' field") + self.assertIn("timestamp", price_data, "Price data missing 'timestamp' field") + + # Price should be a positive number + self.assertGreater(price_data["price"], 0, "Price should be greater than zero") + + # Currency should be USD + self.assertEqual(price_data["currency"], "USD", "Currency should be USD") + + def test_yahoo_price_provider(self): + """Test the Yahoo Finance price provider.""" + price_data = get_yahoo_bitcoin_price() + + # Skip test if price is zero (provider unavailable) + if price_data.price <= 0: + self.skipTest("Yahoo Finance price provider unavailable") + + # Price should be a positive number + self.assertGreater(price_data.price, 0, "Yahoo price should be greater than zero") + + # Currency should be USD + self.assertEqual(price_data.currency, "USD", "Yahoo currency should be USD") + + def test_coingecko_price_provider(self): + """Test the CoinGecko price provider.""" + price_data = get_coingecko_bitcoin_price() + + # Skip test if price is zero (provider unavailable) + if price_data.price <= 0: + self.skipTest("CoinGecko price provider unavailable") + + # Price should be a positive number + self.assertGreater(price_data.price, 0, "CoinGecko price should be greater than zero") + + # Currency should be USD + self.assertEqual(price_data.currency, "USD", "CoinGecko currency should be USD") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/custom/modules/bitcoin/tests/test_price_storage.py b/src/custom/modules/bitcoin/tests/test_price_storage.py new file mode 100644 index 00000000..b4df559b --- /dev/null +++ b/src/custom/modules/bitcoin/tests/test_price_storage.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Tests for Bitcoin price storage functionality +""" +import unittest +import os +import json +import tempfile +from datetime import datetime +from typing import Dict, Any + +from src.custom.modules.bitcoin.pipeline.BitcoinPricePipeline import BitcoinPricePipeline +from src.custom.modules.bitcoin.integrations.BitcoinPriceIntegration import BitcoinPriceIntegration, BitcoinPriceIntegrationConfiguration + +class TestBitcoinPriceStorage(unittest.TestCase): + """Test cases for Bitcoin price storage functionality.""" + + def setUp(self): + """Set up test environment with temporary storage location.""" + # Create a temporary directory for test storage + self.temp_dir = tempfile.TemporaryDirectory() + + # Initialize the pipeline with the temp directory + self.pipeline = BitcoinPricePipeline(storage_base_path=self.temp_dir.name) + + # Create a sample price data entry + self.sample_price_data = { + "price": 45000.0, + "currency": "USD", + "source": "Test", + "timestamp": datetime.now().isoformat() + } + + def tearDown(self): + """Clean up after tests.""" + self.temp_dir.cleanup() + + def test_storage_directories_created(self): + """Test that storage directories are created correctly.""" + # The pipeline should create directories as needed + self.pipeline.run(store=True) + + # Check if the price directory exists + self.assertTrue(os.path.exists(self.pipeline.prices_path), + "Price storage directory was not created") + + # Check directory permissions + self.assertTrue(os.access(self.pipeline.prices_path, os.W_OK), + "Price storage directory is not writable") + + def test_store_price_data(self): + """Test storing price data and retrieving it.""" + # Store the sample price data + self.pipeline._store_price_data(self.sample_price_data) + + # Get stored prices + stored_prices = self.pipeline.get_stored_prices() + + # Verify at least one price is stored + self.assertGreaterEqual(len(stored_prices), 1, + "No price data was stored") + + # Verify the stored data matches what we put in + self.assertEqual(stored_prices[0]["price"], self.sample_price_data["price"], + "Stored price does not match sample price") + self.assertEqual(stored_prices[0]["currency"], self.sample_price_data["currency"], + "Stored currency does not match sample currency") + + def test_pipeline_with_storage(self): + """Test the full pipeline with storage enabled.""" + # Run the pipeline with storage + result = self.pipeline.run(store=True) + + # Check if the pipeline returned current price data + self.assertIn("current_price", result, + "Pipeline did not return current price data") + + # Verify stored data exists + stored_prices = self.pipeline.get_stored_prices() + self.assertGreaterEqual(len(stored_prices), 1, + "Pipeline did not store any price data") + + # Verify stored data structure + for price_entry in stored_prices: + self.assertIn("price", price_entry, "Stored price entry missing 'price' field") + self.assertIn("currency", price_entry, "Stored price entry missing 'currency' field") + self.assertIn("timestamp", price_entry, "Stored price entry missing 'timestamp' field") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/custom/modules/bitcoin/tests/test_price_validation.py b/src/custom/modules/bitcoin/tests/test_price_validation.py new file mode 100644 index 00000000..7a15a0c2 --- /dev/null +++ b/src/custom/modules/bitcoin/tests/test_price_validation.py @@ -0,0 +1,145 @@ +""" +Validation utilities for Bitcoin price data testing. +""" +import re +from dataclasses import dataclass +from typing import Dict, Any, Optional, Union, Tuple + +from src.custom.modules.bitcoin.assistants.BitcoinAssistant import create_agent as create_bitcoin_agent +from src.custom.modules.bitcoin.models import ModelConfig, ModelProvider +from src.custom.modules.bitcoin.integrations.BitcoinPriceIntegration import BitcoinPriceIntegration, BitcoinPriceIntegrationConfiguration + +@dataclass +class PriceData: + """Structured Bitcoin price data for validation.""" + price: float + currency: str = "USD" + source: str = "Unknown" + timestamp: Optional[str] = None + +def get_bitcoin_price() -> Dict[str, Any]: + """Get current Bitcoin price using the integration.""" + integration = BitcoinPriceIntegration(BitcoinPriceIntegrationConfiguration()) + return integration.get_current_price() + +def get_historical_prices(days: int = 30) -> Dict[str, Any]: + """Get historical Bitcoin prices using the integration.""" + integration = BitcoinPriceIntegration(BitcoinPriceIntegrationConfiguration()) + return integration.get_historical_prices(days) + +def get_yahoo_bitcoin_price() -> PriceData: + """Get Bitcoin price from Yahoo Finance.""" + # Implementation details... + return PriceData(price=0.0) + +def get_coingecko_bitcoin_price() -> PriceData: + """Get Bitcoin price from CoinGecko.""" + # Implementation details... + return PriceData(price=0.0) + +def extract_price_from_llm_response(response: str) -> Union[float, None]: + """Extract a Bitcoin price from an LLM's text response. + + Args: + response: Text response from an LLM that might contain a price + + Returns: + Float price or None if no price could be extracted + """ + # Look for price patterns like $45,123.45 or 45,123.45 USD + price_patterns = [ + r'\$([0-9,]+(?:\.[0-9]+)?)', # $45,123.45 + r'([0-9,]+(?:\.[0-9]+)?)\s*(?:USD|dollars)', # 45,123.45 USD + r'(?:USD|dollars)\s*([0-9,]+(?:\.[0-9]+)?)', # USD 45,123.45 + r'(?:price|worth|value)[^\$0-9]*\$?([0-9,]+(?:\.[0-9]+)?)', # price is $45,123.45 + ] + + for pattern in price_patterns: + match = re.search(pattern, response, re.IGNORECASE) + if match: + # Clean the matched price (remove commas) + price_str = match.group(1).replace(',', '') + try: + return float(price_str) + except ValueError: + continue + + return None + +def validate_bitcoin_price(price: float, + reference_source: str = "yahoo", + tolerance_percent: float = 5.0) -> Tuple[bool, str]: + """Validate if a Bitcoin price is within tolerance of a reference source. + + Args: + price: The price to validate + reference_source: Source to compare against ("yahoo" or "coingecko") + tolerance_percent: Maximum allowed percentage difference + + Returns: + Tuple of (is_valid, validation_message) + """ + # Get reference price + if reference_source.lower() == "yahoo": + reference = get_yahoo_bitcoin_price() + else: + reference = get_coingecko_bitcoin_price() + + # If reference price is zero, we can't validate + if reference.price <= 0: + return False, f"Reference price from {reference_source} is unavailable" + + # Calculate percentage difference + diff_percent = abs(price - reference.price) / reference.price * 100 + + if diff_percent <= tolerance_percent: + return True, f"Price ${price} is within {tolerance_percent}% of ${reference.price} from {reference_source}" + else: + return False, f"Price ${price} differs by {diff_percent:.2f}% from ${reference.price} ({reference_source})" + +def test_bitcoin_agent_price_accuracy(model_config: Optional[ModelConfig] = None) -> Dict[str, Any]: + """Test if the Bitcoin assistant provides accurate price information. + + Args: + model_config: Optional model configuration for the agent + + Returns: + Dict with test results + """ + # Create agent with specified model or default + agent = create_bitcoin_agent(model_config) + + # Ask for the current Bitcoin price + response = agent.invoke("What is the current price of Bitcoin?") + response_text = response.content + + # Extract price from response + extracted_price = extract_price_from_llm_response(response_text) + + # Get reference price for comparison + reference_data = get_bitcoin_price() + reference_price = reference_data.get("price", 0) + + # Validate the extracted price + is_valid, validation_message = validate_bitcoin_price( + extracted_price if extracted_price else 0, + "API Integration", + 10.0 # Higher tolerance for LLM responses + ) + + return { + "test_name": "Bitcoin Price Accuracy Test", + "query": "What is the current price of Bitcoin?", + "llm_response": response_text, + "extracted_price": extracted_price, + "reference_price": reference_price, + "reference_source": reference_data.get("source", "Unknown"), + "passed": is_valid, + "validation_message": validation_message + } + +# Export for use in other modules +__all__ = ['get_bitcoin_price', 'get_historical_prices', 'get_yahoo_bitcoin_price', + 'get_coingecko_bitcoin_price', 'extract_price_from_llm_response', + 'validate_bitcoin_price', 'test_bitcoin_agent_price_accuracy', + 'PriceData'] \ No newline at end of file diff --git a/src/custom/modules/bitcoin/tests/test_storage_verification.py b/src/custom/modules/bitcoin/tests/test_storage_verification.py new file mode 100644 index 00000000..a9c2c39f --- /dev/null +++ b/src/custom/modules/bitcoin/tests/test_storage_verification.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +Tests to verify storage functionality for Bitcoin prices +""" +import unittest +import os +import json +from datetime import datetime + +class TestStorageVerification(unittest.TestCase): + """Tests to verify Bitcoin price storage is working correctly.""" + + def setUp(self): + """Set up test environment.""" + # Calculate storage path relative to the repository root + current_dir = os.path.dirname(os.path.abspath(__file__)) + repo_root = os.path.abspath(os.path.join(current_dir, "../../../../..")) + # Path to the datastore directory + self.storage_base_path = os.path.join(repo_root, "storage", "datastore") + self.storage_path = os.path.join(self.storage_base_path, "bitcoin/prices") + + print(f"Storage base path: {self.storage_base_path}") + print(f"Storage prices path: {self.storage_path}") + + # Create test files list to clean up later + self.test_files = [] + + def tearDown(self): + """Clean up after tests.""" + # DISABLED CLEANUP to allow file inspection + print("Test cleanup disabled - files will be preserved for inspection") + return + + # Remove any test files we created + for test_file in self.test_files: + if os.path.exists(test_file): + try: + os.remove(test_file) + print(f"Removed test file: {test_file}") + except Exception as e: + print(f"Warning: Could not remove test file {test_file}: {e}") + + def test_storage_directory_creation(self): + """Test that storage directories are created as needed.""" + # Create directory if it doesn't exist + if not os.path.exists(self.storage_path): + try: + os.makedirs(self.storage_path, exist_ok=True) + print(f"Created storage directory: {self.storage_path}") + except Exception as e: + self.fail(f"Failed to create storage directory: {e}") + + # Directory should exist now + self.assertTrue(os.path.exists(self.storage_path), + "Storage directory does not exist and couldn't be created") + + # Check directory permissions + self.assertTrue(os.access(self.storage_path, os.W_OK), + "Storage directory is not writable") + self.assertTrue(os.access(self.storage_path, os.R_OK), + "Storage directory is not readable") + + def test_file_creation_and_read(self): + """Test that files can be created and read in the storage directory.""" + # Get current date and time + now = datetime.now() + date_str = now.strftime('%Y%m%d') # Date-only folder without Z + timestamp_str = now.strftime('%Y%m%dT%H%M%S') + 'Z' # Timestamp with Z suffix + + # Create dated folder within prices directory + dated_folder = os.path.join(self.storage_path, date_str) + os.makedirs(dated_folder, exist_ok=True) + print(f"Created or verified dated folder: {dated_folder}") + + # Create a sample price data entry + sample_data = { + "price": 50000.0, + "currency": "USD", + "source": "Test", + "timestamp": now.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), # ISO 8601 with Z for UTC + "last_updated_at": now.timestamp(), # Unix timestamp (seconds since epoch) + "market_cap": 950000000000, + "24h_vol": 28000000000, + "24h_change": 2.5, + "provider": "CoinGecko" + } + + # Generate filename with requested format including the currency pair + filename = f"test_{timestamp_str}_BTCUSD_price.json" + test_file = os.path.join(dated_folder, filename) + self.test_files.append(test_file) + print(f"Attempting to create test file: {test_file}") + + # Try to write test file + try: + with open(test_file, 'w') as f: + json.dump([sample_data], f, indent=4) + print(f"Successfully wrote test file: {test_file}") + except Exception as e: + self.fail(f"Failed to write test file: {e}") + + # Check if file exists + file_exists = os.path.exists(test_file) + print(f"File exists check: {file_exists}") + self.assertTrue(file_exists, "Test file was not created") + + # Try to read the file back + try: + with open(test_file, 'r') as f: + read_data = json.load(f) + print(f"Successfully read test file: {test_file}") + except Exception as e: + self.fail(f"Failed to read test file: {e}") + + # Verify data integrity + self.assertEqual(len(read_data), 1, + "Read data should have one item") + self.assertEqual(read_data[0]["price"], sample_data["price"], + "Read price does not match written price") + + print(f"Test completed. JSON file should be available at: {test_file}") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/custom/modules/bitcoin/workflow/BitcoinTransactionWorkflow.py b/src/custom/modules/bitcoin/workflow/BitcoinTransactionWorkflow.py new file mode 100644 index 00000000..0ed4b9f5 --- /dev/null +++ b/src/custom/modules/bitcoin/workflow/BitcoinTransactionWorkflow.py @@ -0,0 +1,16 @@ +from src.custom.modules.bitcoin.models import BitcoinModel +from src.custom.modules.bitcoin.pipeline.BitcoinPricePipeline import BitcoinPricePipeline +from typing import Dict, Any + +class BitcoinTransactionWorkflow: + def get_price_analysis(self, days: int = 7) -> Dict[str, Any]: + """Get Bitcoin price analysis. + + Args: + days: Number of days to analyze + + Returns: + Dict with price analysis + """ + pipeline = BitcoinPricePipeline() + return pipeline.run(days=days) \ No newline at end of file diff --git a/src/custom/modules/example/.skip b/src/custom/modules/example/.skip new file mode 100644 index 00000000..8105ccb5 --- /dev/null +++ b/src/custom/modules/example/.skip @@ -0,0 +1,2 @@ +# This file indicates that this module should be skipped during automatic loading. +# It is only a template for new modules and should not be loaded into the system. \ No newline at end of file diff --git a/src/custom/modules/example/assistants/UexampleAssistant.py b/src/custom/modules/example/assistants/UexampleAssistant.py index 9c244d48..5c2df683 100644 --- a/src/custom/modules/example/assistants/UexampleAssistant.py +++ b/src/custom/modules/example/assistants/UexampleAssistant.py @@ -7,7 +7,7 @@ MODEL = "o3-mini" # Or another appropriate model TEMPERATURE = 1 AVATAR_URL = "https://example.com/avatar.png" -SYSTEM_PROMPT = """You are the Uexample Assistant. Your role is to help users with tasks related to example. +SYSTEM_PROMPT = """You are the Uexample Assistant. Your role is to help users with tasks related to uexample. You can perform the following tasks: - Task 1 @@ -22,7 +22,7 @@ def create_agent(shared_state: AgentSharedState = None) -> Agent: llm = ChatOpenAI( model=MODEL, temperature=TEMPERATURE, - api_key=secret.get_openai_api_key() + api_key=secret.get('OPENAI_API_KEY') ) # Configure the agent @@ -48,5 +48,5 @@ def create_agent(shared_state: AgentSharedState = None) -> Agent: # For testing purposes if __name__ == "__main__": agent = create_agent() - agent.run("Hello, I need help with example") + agent.run("Hello, I need help with uexample")