apowerb 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. apowerb/__init__.py +18 -0
  2. apowerb/agent_store/__init__.py +0 -0
  3. apowerb/agent_store/agent_manager.py +159 -0
  4. apowerb/agent_store/api_key_store.py +112 -0
  5. apowerb/agent_store/hub_manager.py +121 -0
  6. apowerb/auth/__init__.py +0 -0
  7. apowerb/auth/dependencies.py +181 -0
  8. apowerb/auth/exceptions.py +7 -0
  9. apowerb/auth/middleware.py +110 -0
  10. apowerb/auth/router.py +142 -0
  11. apowerb/auth/schemas.py +66 -0
  12. apowerb/auth/service.py +283 -0
  13. apowerb/bi/__init__.py +1 -0
  14. apowerb/bi/chart_refresh_router.py +153 -0
  15. apowerb/bi/charts/__init__.py +1 -0
  16. apowerb/bi/charts/core.py +290 -0
  17. apowerb/bi/charts/router.py +279 -0
  18. apowerb/bi/charts/schemas.py +220 -0
  19. apowerb/bi/charts/service.py +301 -0
  20. apowerb/bi/dashboards/__init__.py +1 -0
  21. apowerb/bi/dashboards/core.py +460 -0
  22. apowerb/bi/dashboards/router.py +470 -0
  23. apowerb/bi/dashboards/schema.py +371 -0
  24. apowerb/bi/dashboards/service.py +304 -0
  25. apowerb/bi/data/__init__.py +1 -0
  26. apowerb/bi/data/_bi_storage.py +107 -0
  27. apowerb/bi/data/agent_executor.py +165 -0
  28. apowerb/bi/data/csv_executor.py +107 -0
  29. apowerb/bi/data/dataset_router.py +636 -0
  30. apowerb/bi/data/db_executor.py +477 -0
  31. apowerb/bi/data/google_drive_executor.py +163 -0
  32. apowerb/bi/data/onedrive_excel_executor.py +243 -0
  33. apowerb/bi/data/router.py +151 -0
  34. apowerb/bi/data/schema.py +117 -0
  35. apowerb/bi/data/service.py +376 -0
  36. apowerb/bi/data/upload_router.py +239 -0
  37. apowerb/bi/db_stores.py +437 -0
  38. apowerb/bi/dependencies.py +51 -0
  39. apowerb/bi/refresh_router.py +338 -0
  40. apowerb/bi/stats_router.py +40 -0
  41. apowerb/cli/agents.py +202 -0
  42. apowerb/cli/main.py +36 -0
  43. apowerb/cli/migrate_integrations.py +68 -0
  44. apowerb/cli/runs.py +83 -0
  45. apowerb/cli/tools.py +23 -0
  46. apowerb/configs/__init__.py +0 -0
  47. apowerb/configs/model_pricing.yaml +82 -0
  48. apowerb/configs/models.py +164 -0
  49. apowerb/configs/paths.py +99 -0
  50. apowerb/configs/settings.py +410 -0
  51. apowerb/configs/th2logger.py +194 -0
  52. apowerb/core/__init__.py +0 -0
  53. apowerb/core/adk_agent_builder.py +143 -0
  54. apowerb/core/adk_runner.py +989 -0
  55. apowerb/core/agent_flow.py +0 -0
  56. apowerb/core/agent_helpers/__init__.py +113 -0
  57. apowerb/core/agent_helpers/agent_utils.py +906 -0
  58. apowerb/core/agent_helpers/callback_chain.py +37 -0
  59. apowerb/core/agent_helpers/callbacks.py +805 -0
  60. apowerb/core/agent_helpers/chat_action_tools.py +433 -0
  61. apowerb/core/agent_helpers/default_llm.py +127 -0
  62. apowerb/core/agent_helpers/extras_loader.py +144 -0
  63. apowerb/core/agent_helpers/file_readers.py +230 -0
  64. apowerb/core/agent_helpers/ingest_file_tool.py +199 -0
  65. apowerb/core/agent_helpers/llm_model_builder.py +180 -0
  66. apowerb/core/agent_helpers/mcp_loader.py +349 -0
  67. apowerb/core/agent_helpers/pdf_to_images_tool.py +377 -0
  68. apowerb/core/agent_helpers/pdf_writer.py +67 -0
  69. apowerb/core/agent_helpers/read_file_tool.py +315 -0
  70. apowerb/core/agent_helpers/run_config_patch.py +132 -0
  71. apowerb/core/agent_helpers/text_utils.py +70 -0
  72. apowerb/core/agent_helpers/tool_factories.py +256 -0
  73. apowerb/core/agent_helpers/tools_binder.py +346 -0
  74. apowerb/core/agent_main.py +764 -0
  75. apowerb/core/agent_seeds.py +529 -0
  76. apowerb/core/api_key_main.py +115 -0
  77. apowerb/core/artifact_executor.py +140 -0
  78. apowerb/core/audio_streaming.py +636 -0
  79. apowerb/core/db/__init__.py +0 -0
  80. apowerb/core/db/write_config.py +73 -0
  81. apowerb/core/extensions/__init__.py +0 -0
  82. apowerb/core/extensions/loader.py +77 -0
  83. apowerb/core/extensions/registry.py +245 -0
  84. apowerb/core/flow_scheduler.py +0 -0
  85. apowerb/core/guardrails.py +279 -0
  86. apowerb/core/history_compaction.py +85 -0
  87. apowerb/core/hub_main.py +527 -0
  88. apowerb/core/invocation_context.py +65 -0
  89. apowerb/core/knowledge_map.py +148 -0
  90. apowerb/core/rag_streaming.py +167 -0
  91. apowerb/core/run_gate.py +80 -0
  92. apowerb/core/superagents/__init__.py +230 -0
  93. apowerb/core/superagents/schemas/__init__.py +0 -0
  94. apowerb/core/superagents/templates/__init__.py +75 -0
  95. apowerb/core/superagents/templates/audio.py +236 -0
  96. apowerb/core/superagents/templates/dashboard.py +189 -0
  97. apowerb/core/superagents/templates/data.py +407 -0
  98. apowerb/core/superagents/templates/image.py +217 -0
  99. apowerb/core/superagents/templates/marketing.py +97 -0
  100. apowerb/core/superagents/templates/rag.py +213 -0
  101. apowerb/core/validation/__init__.py +0 -0
  102. apowerb/core/validation/prompt_safety.py +207 -0
  103. apowerb/helpers/__init__.py +0 -0
  104. apowerb/helpers/audit_log.py +56 -0
  105. apowerb/helpers/business_intelligence_migration.py +76 -0
  106. apowerb/helpers/data_lake.py +101 -0
  107. apowerb/helpers/database.py +84 -0
  108. apowerb/helpers/database_connection.py +26 -0
  109. apowerb/helpers/email_sender.py +93 -0
  110. apowerb/helpers/emails.py +16 -0
  111. apowerb/helpers/encryptor.py +72 -0
  112. apowerb/helpers/env_scope.py +88 -0
  113. apowerb/helpers/google_oidc.py +106 -0
  114. apowerb/helpers/integrations_migration.py +63 -0
  115. apowerb/helpers/jsonify.py +81 -0
  116. apowerb/helpers/litellm_config.py +619 -0
  117. apowerb/helpers/metrics.py +223 -0
  118. apowerb/helpers/metrics_middleware.py +49 -0
  119. apowerb/helpers/model_pricing.py +102 -0
  120. apowerb/helpers/notification_bus.py +45 -0
  121. apowerb/helpers/notification_migration.py +65 -0
  122. apowerb/helpers/notifier_health.py +67 -0
  123. apowerb/helpers/notify_etl.py +97 -0
  124. apowerb/helpers/oauth_state_store.py +137 -0
  125. apowerb/helpers/oauth_states_migration.py +77 -0
  126. apowerb/helpers/ownership.py +72 -0
  127. apowerb/helpers/pagination.py +71 -0
  128. apowerb/helpers/request_id_middleware.py +84 -0
  129. apowerb/helpers/security.py +277 -0
  130. apowerb/helpers/security_headers.py +64 -0
  131. apowerb/helpers/share_migration.py +98 -0
  132. apowerb/helpers/store_migrations.py +38 -0
  133. apowerb/helpers/system_mailer.py +210 -0
  134. apowerb/helpers/title_generator.py +133 -0
  135. apowerb/helpers/user_migration.py +163 -0
  136. apowerb/helpers/webhook_log_migration.py +71 -0
  137. apowerb/helpers/webhook_migration.py +127 -0
  138. apowerb/integrations/__init__.py +0 -0
  139. apowerb/integrations/github.py +215 -0
  140. apowerb/integrations/gmail_webhook.py +400 -0
  141. apowerb/integrations/google.py +243 -0
  142. apowerb/integrations/helpers.py +516 -0
  143. apowerb/integrations/microsoft.py +440 -0
  144. apowerb/integrations/odoo.py +218 -0
  145. apowerb/integrations/outlook_webhook.py +525 -0
  146. apowerb/integrations/slack.py +0 -0
  147. apowerb/main.py +646 -0
  148. apowerb/middleware/__init__.py +1 -0
  149. apowerb/middleware/auth.py +31 -0
  150. apowerb/models.py +631 -0
  151. apowerb/routers/__init__.py +0 -0
  152. apowerb/routers/adk_runner.py +808 -0
  153. apowerb/routers/agent_reload.py +109 -0
  154. apowerb/routers/agents.py +150 -0
  155. apowerb/routers/api_keys.py +48 -0
  156. apowerb/routers/artifacts.py +186 -0
  157. apowerb/routers/audio_stream.py +569 -0
  158. apowerb/routers/config.py +25 -0
  159. apowerb/routers/data_lake.py +102 -0
  160. apowerb/routers/emailing.py +258 -0
  161. apowerb/routers/files.py +272 -0
  162. apowerb/routers/google_drive_browser.py +249 -0
  163. apowerb/routers/health.py +113 -0
  164. apowerb/routers/hub.py +63 -0
  165. apowerb/routers/integrations.py +894 -0
  166. apowerb/routers/models.py +59 -0
  167. apowerb/routers/notifications.py +221 -0
  168. apowerb/routers/onedrive_browser.py +397 -0
  169. apowerb/routers/rag/__init__.py +55 -0
  170. apowerb/routers/rag/index_db.py +271 -0
  171. apowerb/routers/rag/index_files.py +104 -0
  172. apowerb/routers/rag/index_s3.py +120 -0
  173. apowerb/routers/rag/index_url.py +98 -0
  174. apowerb/routers/rag/schemas.py +57 -0
  175. apowerb/routers/rag/status.py +97 -0
  176. apowerb/routers/rag/stream.py +187 -0
  177. apowerb/routers/rag/validators.py +92 -0
  178. apowerb/routers/rag/webhook.py +117 -0
  179. apowerb/routers/runs.py +62 -0
  180. apowerb/routers/scheduler.py +236 -0
  181. apowerb/routers/share.py +109 -0
  182. apowerb/routers/skills.py +472 -0
  183. apowerb/routers/superagents.py +38 -0
  184. apowerb/routers/tools.py +670 -0
  185. apowerb/routers/webhook_handlers/__init__.py +28 -0
  186. apowerb/routers/webhook_handlers/_common.py +311 -0
  187. apowerb/routers/webhook_handlers/gmail.py +501 -0
  188. apowerb/routers/webhook_handlers/outlook.py +891 -0
  189. apowerb/routers/webhooks.py +1042 -0
  190. apowerb/routers/workflows.py +215 -0
  191. apowerb/scheduler/backlog_migrations.py +142 -0
  192. apowerb/scheduler/backlog_status_tool.py +197 -0
  193. apowerb/scheduler/backlog_worker.py +386 -0
  194. apowerb/scheduler/events_retention.py +115 -0
  195. apowerb/scheduler/mage.py +939 -0
  196. apowerb/scheduler/notifier_watch.py +82 -0
  197. apowerb/scheduler/run_agent_background/__init__.py +73 -0
  198. apowerb/scheduler/run_agent_background/agent_runner.py +411 -0
  199. apowerb/scheduler/run_agent_background/schedule_helpers.py +322 -0
  200. apowerb/scheduler/run_agent_background/scheduler_api.py +469 -0
  201. apowerb/scheduler/run_agent_background/token_issuer.py +62 -0
  202. apowerb/scheduler/th2etl_client.py +331 -0
  203. apowerb/scheduler/webhook_renewal.py +259 -0
  204. apowerb/schema/__init__.py +0 -0
  205. apowerb/schema/adk_runner_schema.py +56 -0
  206. apowerb/schema/agent_schema.py +82 -0
  207. apowerb/schema/api_key_schema.py +33 -0
  208. apowerb/schema/data_lake_schema.py +71 -0
  209. apowerb/schema/hub_schema.py +18 -0
  210. apowerb/schema/notification_schema.py +35 -0
  211. apowerb/schema/share_schema.py +28 -0
  212. apowerb/schema/skill_schema.py +66 -0
  213. apowerb/schema/tool_config_schema.py +16 -0
  214. apowerb/schema/usage_schema.py +185 -0
  215. apowerb/schema/webhook_schema.py +87 -0
  216. apowerb/skills_store/__init__.py +0 -0
  217. apowerb/skills_store/portfolio/dashboard-builder/SKILL.md +77 -0
  218. apowerb/skills_store/portfolio/data-visualization/SKILL.md +93 -0
  219. apowerb/skills_store/portfolio/data-visualization/references/chart-guide.md +141 -0
  220. apowerb/skills_store/portfolio/email-campaign/SKILL.md +183 -0
  221. apowerb/skills_store/portfolio/email-campaign/references/templates_en.md +82 -0
  222. apowerb/skills_store/portfolio/email-campaign/references/templates_fr.md +82 -0
  223. apowerb/skills_store/portfolio/error-recovery/SKILL.md +104 -0
  224. apowerb/skills_store/portfolio/error-recovery/references/common-errors.md +70 -0
  225. apowerb/skills_store/portfolio/onedrive-navigation/SKILL.md +116 -0
  226. apowerb/skills_store/portfolio/onedrive-navigation/references/path-patterns.md +123 -0
  227. apowerb/skills_store/portfolio/rag-search/SKILL.md +81 -0
  228. apowerb/skills_store/portfolio/rag-search/references/rag-workflow.md +101 -0
  229. apowerb/skills_store/portfolio/report-generation/SKILL.md +112 -0
  230. apowerb/skills_store/portfolio/text-to-sql/SKILL.md +100 -0
  231. apowerb/skills_store/portfolio/text-to-sql/references/sql-patterns.md +202 -0
  232. apowerb/skills_store/skill_manager.py +105 -0
  233. apowerb/skills_store/skills_loader.py +170 -0
  234. apowerb/sqlgen/__init__.py +44 -0
  235. apowerb/sqlgen/analyst.py +164 -0
  236. apowerb/sqlgen/analyst_prompts.py +186 -0
  237. apowerb/sqlgen/generator.py +29 -0
  238. apowerb/sqlgen/safety.py +22 -0
  239. apowerb/sqlgen/schema_format.py +97 -0
  240. apowerb/sqlgen/table_selection.py +53 -0
  241. apowerb/storage/__init__.py +0 -0
  242. apowerb/storage/filename.py +53 -0
  243. apowerb/storage/s3.py +75 -0
  244. apowerb/storage/storage_service.py +59 -0
  245. apowerb/storage/test.py +16 -0
  246. apowerb/storage/test.txt +531 -0
  247. apowerb/storage/webhook_attachments.py +160 -0
  248. apowerb/tools_store/__init__.py +0 -0
  249. apowerb/tools_store/portfolio/api_call.py +59 -0
  250. apowerb/tools_store/portfolio/artifacts/__init__.py +0 -0
  251. apowerb/tools_store/portfolio/artifacts/save_code_artifact.py +39 -0
  252. apowerb/tools_store/portfolio/audio.py +732 -0
  253. apowerb/tools_store/portfolio/basic.py +486 -0
  254. apowerb/tools_store/portfolio/business_intelligence.py +1862 -0
  255. apowerb/tools_store/portfolio/data_handler.py +249 -0
  256. apowerb/tools_store/portfolio/database.py +570 -0
  257. apowerb/tools_store/portfolio/database_mcp.py +26 -0
  258. apowerb/tools_store/portfolio/db_to_rag.py +244 -0
  259. apowerb/tools_store/portfolio/google_auth.py +205 -0
  260. apowerb/tools_store/portfolio/google_calendar.py +187 -0
  261. apowerb/tools_store/portfolio/google_docs.py +179 -0
  262. apowerb/tools_store/portfolio/google_drive.py +568 -0
  263. apowerb/tools_store/portfolio/google_gmail.py +478 -0
  264. apowerb/tools_store/portfolio/google_sheets.py +156 -0
  265. apowerb/tools_store/portfolio/image_generation.py +305 -0
  266. apowerb/tools_store/portfolio/integration_status.py +95 -0
  267. apowerb/tools_store/portfolio/internal/data_handler.py +207 -0
  268. apowerb/tools_store/portfolio/marketing.py +99 -0
  269. apowerb/tools_store/portfolio/memory.py +280 -0
  270. apowerb/tools_store/portfolio/microsoft_auth.py +342 -0
  271. apowerb/tools_store/portfolio/odoo.py +235 -0
  272. apowerb/tools_store/portfolio/onedrive_core.py +674 -0
  273. apowerb/tools_store/portfolio/onedrive_read.py +669 -0
  274. apowerb/tools_store/portfolio/onedrive_write.py +565 -0
  275. apowerb/tools_store/portfolio/outlook_mail.py +731 -0
  276. apowerb/tools_store/portfolio/rag.py +407 -0
  277. apowerb/tools_store/portfolio/s3_tools.py +315 -0
  278. apowerb/tools_store/portfolio/teams.py +1140 -0
  279. apowerb/tools_store/portfolio/text_to_sql.py +1056 -0
  280. apowerb/tools_store/portfolio/thaink2.py +406 -0
  281. apowerb/tools_store/portfolio/visualization.py +292 -0
  282. apowerb/tools_store/portfolio/web_search_mcp.py +54 -0
  283. apowerb/tools_store/schema/__init__.py +0 -0
  284. apowerb/tools_store/schema/basic.py +13 -0
  285. apowerb/tools_store/schema/text_to_sql.py +22 -0
  286. apowerb/tools_store/tool_config.py +106 -0
  287. apowerb/tools_store/tool_manager.py +228 -0
  288. apowerb/tools_store/tools_helpers.py +497 -0
  289. apowerb/users/dependencies.py +45 -0
  290. apowerb/users/exceptions.py +6 -0
  291. apowerb/users/router.py +108 -0
  292. apowerb/users/schemas.py +77 -0
  293. apowerb/users/service.py +86 -0
  294. apowerb-0.1.2.dist-info/METADATA +1117 -0
  295. apowerb-0.1.2.dist-info/RECORD +298 -0
  296. apowerb-0.1.2.dist-info/WHEEL +4 -0
  297. apowerb-0.1.2.dist-info/entry_points.txt +3 -0
  298. apowerb-0.1.2.dist-info/licenses/LICENSE +21 -0
apowerb/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """
2
+ Docstring for th2agent
3
+
4
+ """
5
+
6
+ # Pydantic hack for Google ADK - must be applied before any google.adk imports
7
+ try:
8
+ import mcp.client.session
9
+ from pydantic_core import core_schema
10
+
11
+ def __get_pydantic_core_schema__(cls, source_type, handler):
12
+ return core_schema.is_instance_schema(cls)
13
+
14
+ mcp.client.session.ClientSession.__get_pydantic_core_schema__ = classmethod(
15
+ __get_pydantic_core_schema__
16
+ )
17
+ except ImportError:
18
+ pass
File without changes
@@ -0,0 +1,159 @@
1
+ from typing import Any
2
+ from apowerb.configs.th2logger import setup_logging
3
+
4
+ from pydantic import BaseModel
5
+ from apowerb.helpers.database_connection import DBConfig
6
+
7
+ from sqlalchemy import (
8
+ create_engine,
9
+ Table,
10
+ Column,
11
+ Integer,
12
+ String,
13
+ MetaData,
14
+ inspect,
15
+ UniqueConstraint,
16
+ )
17
+ from apowerb.configs.settings import get_settings
18
+
19
+ logger = setup_logging(__name__)
20
+
21
+ settings = get_settings()
22
+
23
+
24
+ class AgentStoreConfig(DBConfig):
25
+ """Configuration for the agent store."""
26
+
27
+ table_name: str = settings.db_agent_store_table_name
28
+
29
+
30
+ class AgentStore(BaseModel):
31
+ """Class to manage the agent store in a database.
32
+
33
+ Attributes:
34
+ db_host: str - The database host.
35
+ db_name: str - The database name.
36
+ db_url: str - The database URL.
37
+ table_name: str - The name of the agent table.
38
+ db_user: str - The database user.
39
+ db_password: str - The database password.
40
+ db_type: str - The type of the database.
41
+ db_schema: str - The database schema.
42
+ engine: Any - The SQLAlchemy engine instance.
43
+ metadata: Any - The SQLAlchemy metadata instance.
44
+ agent_table: Any - The SQLAlchemy table instance for agents.
45
+ """
46
+
47
+ agent_config: AgentStoreConfig = AgentStoreConfig()
48
+ db_host: str = agent_config.db_host
49
+ db_name: str = settings.db_name
50
+ db_url: str = f"{agent_config.db_host}:{agent_config.db_port}/{db_name}"
51
+ table_name: str = agent_config.table_name
52
+ db_user: str = agent_config.db_user
53
+ db_password: str = agent_config.db_password
54
+ db_type: str = agent_config.db_type
55
+ db_schema: str = agent_config.db_schema
56
+ engine: Any = None
57
+ metadata: Any = None
58
+ agent_table: Any = None
59
+
60
+ def __init__(self, **data: Any):
61
+ super().__init__(**data)
62
+ self.engine = create_engine(
63
+ f"{self.db_type}://{self.db_user}:{self.db_password}@{self.db_url}"
64
+ )
65
+ self.metadata = MetaData(schema=self.db_schema)
66
+ self.agent_table = Table(
67
+ self.table_name,
68
+ self.metadata,
69
+ Column("agent_id", Integer, primary_key=True, autoincrement=True),
70
+ Column("agent_name", String, nullable=False),
71
+ Column("agent_model", String, nullable=False),
72
+ Column("agent_model_params", String),
73
+ Column("agent_description", String),
74
+ Column("agent_instruction", String),
75
+ Column("agent_tools", String),
76
+ Column("agent_type", String),
77
+ Column("sub_agents", String),
78
+ Column("output_key", String),
79
+ Column("output_schema_name", String),
80
+ Column("skip_when_upstream", String),
81
+ Column("input_schema", String),
82
+ Column("output_schema", String),
83
+ Column("code_executor", String),
84
+ Column("created_at", String),
85
+ Column("updated_at", String),
86
+ Column("status", String),
87
+ Column("organization_id", String, nullable=False),
88
+ Column("project_id", String, nullable=False),
89
+ Column("owner_id", String),
90
+ Column("tags", String),
91
+ Column("guardrails_config", String),
92
+ Column("memory_enabled", String),
93
+ Column("artifacts_enabled", String),
94
+ Column("superagent_template_id", String),
95
+ Column("superagent_template_version_hash", String),
96
+ Column("loop_max_iterations", String),
97
+ Column("loop_exit_instruction", String),
98
+ Column("mcp_servers", String),
99
+ Column("agent_skills", String),
100
+ Column("hub_origin_id", String),
101
+ UniqueConstraint(
102
+ "agent_name",
103
+ "organization_id",
104
+ "project_id",
105
+ name="unique_agent_name_per_org_proj",
106
+ ),
107
+ )
108
+
109
+ def create_table(self):
110
+ """Create the agent metadata table in the database."""
111
+ if not inspect(self.engine).has_table(self.table_name):
112
+ self.metadata.create_all(self.engine)
113
+ logger.info(f"Agent store table '{self.table_name}' created successfully.")
114
+ else:
115
+ logger.info(f"Agent store table '{self.table_name}' already exists.")
116
+ self.ensure_columns()
117
+
118
+ def ensure_columns(self):
119
+ """Add new columns to existing tables if they don't exist."""
120
+ from sqlalchemy import text, inspect as sa_inspect
121
+
122
+ inspector = sa_inspect(self.engine)
123
+ existing = [
124
+ c["name"]
125
+ for c in inspector.get_columns(self.table_name, schema=self.db_schema)
126
+ ]
127
+ new_cols = {
128
+ "output_key": "VARCHAR",
129
+ "output_schema_name": "VARCHAR",
130
+ "skip_when_upstream": "VARCHAR",
131
+ "guardrails_config": "VARCHAR",
132
+ "memory_enabled": "VARCHAR",
133
+ "artifacts_enabled": "VARCHAR",
134
+ "superagent_template_id": "VARCHAR",
135
+ "loop_max_iterations": "VARCHAR",
136
+ "loop_exit_instruction": "VARCHAR",
137
+ "mcp_servers": "VARCHAR",
138
+ "agent_skills": "VARCHAR",
139
+ "hub_origin_id": "VARCHAR",
140
+ # Snapshot of the source template's hash-relevant fields at
141
+ # creation time. The UI compares it to the live template hash
142
+ # to surface "template updated, click to sync" banners.
143
+ "superagent_template_version_hash": "VARCHAR",
144
+ }
145
+ with self.engine.begin() as conn:
146
+ for col, typ in new_cols.items():
147
+ if col not in existing:
148
+ schema_prefix = f'"{self.db_schema}".' if self.db_schema else ""
149
+ conn.execute(
150
+ text(
151
+ f'ALTER TABLE {schema_prefix}"{self.table_name}" ADD COLUMN "{col}" {typ}'
152
+ )
153
+ )
154
+
155
+ def get_list_agents(self, agents_query: Any) -> Any:
156
+ """Get a list of all agents from the database."""
157
+ with self.engine.connect() as conn:
158
+ result = conn.execute(agents_query)
159
+ return result.fetchall()
@@ -0,0 +1,112 @@
1
+ from typing import Any
2
+ from apowerb.configs.th2logger import setup_logging
3
+ from pydantic import BaseModel
4
+ from apowerb.helpers.database_connection import DBConfig
5
+
6
+ from sqlalchemy import (
7
+ create_engine,
8
+ Table,
9
+ Column,
10
+ Integer,
11
+ String,
12
+ MetaData,
13
+ inspect,
14
+ )
15
+ from apowerb.configs.settings import get_settings
16
+
17
+ logger = setup_logging(__name__)
18
+
19
+ settings = get_settings()
20
+
21
+
22
+ class ApiKeyStoreConfig(DBConfig):
23
+ """Configuration for the API key store."""
24
+
25
+ table_name: str = "saved_api_keys"
26
+
27
+
28
+ class ApiKeyStore(BaseModel):
29
+ """Class to manage saved API keys in a database."""
30
+
31
+ api_key_config: ApiKeyStoreConfig = ApiKeyStoreConfig()
32
+ db_host: str = api_key_config.db_host
33
+ db_name: str = settings.db_name
34
+ db_url: str = f"{api_key_config.db_host}:{api_key_config.db_port}/{db_name}"
35
+ table_name: str = api_key_config.table_name
36
+ db_user: str = api_key_config.db_user
37
+ db_password: str = api_key_config.db_password
38
+ db_type: str = api_key_config.db_type
39
+ db_schema: str = api_key_config.db_schema
40
+ engine: Any = None
41
+ metadata: Any = None
42
+ api_key_table: Any = None
43
+
44
+ def __init__(self, **data: Any):
45
+ super().__init__(**data)
46
+ self.engine = create_engine(
47
+ f"{self.db_type}://{self.db_user}:{self.db_password}@{self.db_url}"
48
+ )
49
+ self.metadata = MetaData(schema=self.db_schema)
50
+ self.api_key_table = Table(
51
+ self.table_name,
52
+ self.metadata,
53
+ Column("api_key_id", Integer, primary_key=True, autoincrement=True),
54
+ Column("key_name", String),
55
+ Column("provider", String),
56
+ Column("api_key_value", String),
57
+ Column("model", String, nullable=True),
58
+ Column("model_api_base", String, nullable=True),
59
+ Column("owner_id", String),
60
+ Column("organization_id", String),
61
+ Column("created_at", String),
62
+ Column("updated_at", String),
63
+ Column("status", String),
64
+ )
65
+
66
+ def create_table(self):
67
+ """Create the saved_api_keys table in the database."""
68
+ if not inspect(self.engine).has_table(self.table_name):
69
+ self.metadata.create_all(self.engine)
70
+ logger.info(
71
+ f"API key store table '{self.table_name}' created successfully."
72
+ )
73
+ else:
74
+ logger.info(f"API key store table '{self.table_name}' already exists.")
75
+ self.ensure_columns()
76
+
77
+ def ensure_columns(self):
78
+ """Add new columns to existing tables if they don't exist."""
79
+ from sqlalchemy import text, inspect as sa_inspect
80
+
81
+ inspector = sa_inspect(self.engine)
82
+ existing = [
83
+ c["name"]
84
+ for c in inspector.get_columns(self.table_name, schema=self.db_schema)
85
+ ]
86
+ new_cols: dict[str, str] = {
87
+ "model": "VARCHAR",
88
+ "model_api_base": "VARCHAR",
89
+ }
90
+ with self.engine.begin() as conn:
91
+ for col, typ in new_cols.items():
92
+ if col not in existing:
93
+ schema_prefix = f'"{self.db_schema}".' if self.db_schema else ""
94
+ conn.execute(
95
+ text(
96
+ f'ALTER TABLE {schema_prefix}"{self.table_name}" ADD COLUMN "{col}" {typ}'
97
+ )
98
+ )
99
+
100
+ def get_list(self, query: Any) -> Any:
101
+ """Execute a query and return all rows."""
102
+ with self.engine.connect() as conn:
103
+ result = conn.execute(query)
104
+ return result.fetchall()
105
+
106
+ def delete_api_key(self, api_key_id: int) -> None:
107
+ """Delete an API key by its numeric ID."""
108
+ delete_q = self.api_key_table.delete().where(
109
+ self.api_key_table.c.api_key_id == api_key_id
110
+ )
111
+ with self.engine.begin() as conn:
112
+ conn.execute(delete_q)
@@ -0,0 +1,121 @@
1
+ from typing import Any
2
+ from apowerb.configs.th2logger import setup_logging
3
+ from pydantic import BaseModel
4
+ from apowerb.helpers.database_connection import DBConfig
5
+
6
+ from sqlalchemy import (
7
+ create_engine,
8
+ Table,
9
+ Column,
10
+ Integer,
11
+ String,
12
+ MetaData,
13
+ inspect,
14
+ )
15
+ from apowerb.configs.settings import get_settings
16
+
17
+ logger = setup_logging(__name__)
18
+
19
+ settings = get_settings()
20
+
21
+
22
+ class HubStoreConfig(DBConfig):
23
+ """Configuration for the hub store."""
24
+
25
+ table_name: str = "hub_agents"
26
+
27
+
28
+ class HubStore(BaseModel):
29
+ """Class to manage the hub agent store in a database."""
30
+
31
+ hub_config: HubStoreConfig = HubStoreConfig()
32
+ db_host: str = hub_config.db_host
33
+ db_name: str = settings.db_name
34
+ db_url: str = f"{hub_config.db_host}:{hub_config.db_port}/{db_name}"
35
+ table_name: str = hub_config.table_name
36
+ db_user: str = hub_config.db_user
37
+ db_password: str = hub_config.db_password
38
+ db_type: str = hub_config.db_type
39
+ db_schema: str = hub_config.db_schema
40
+ engine: Any = None
41
+ metadata: Any = None
42
+ hub_table: Any = None
43
+
44
+ def __init__(self, **data: Any):
45
+ super().__init__(**data)
46
+ self.engine = create_engine(
47
+ f"{self.db_type}://{self.db_user}:{self.db_password}@{self.db_url}"
48
+ )
49
+ self.metadata = MetaData(schema=self.db_schema)
50
+ self.hub_table = Table(
51
+ self.table_name,
52
+ self.metadata,
53
+ Column("hub_id", Integer, primary_key=True, autoincrement=True),
54
+ Column("hub_name", String, nullable=False),
55
+ Column("hub_description", String),
56
+ Column("hub_category", String),
57
+ Column("hub_tags", String),
58
+ # Snapshot of the original agent config (no API keys)
59
+ Column("agent_name", String),
60
+ Column("agent_model", String),
61
+ Column("agent_description", String),
62
+ Column("agent_instruction", String),
63
+ Column("agent_tools", String),
64
+ Column("agent_type", String),
65
+ Column("sub_agents", String),
66
+ Column("sub_agents_snapshot", String),
67
+ Column("loop_max_iterations", String),
68
+ Column("loop_exit_instruction", String),
69
+ Column("agent_skills", String),
70
+ Column("memory_enabled", String),
71
+ Column("artifacts_enabled", String),
72
+ Column("guardrails_config", String),
73
+ # Metadata
74
+ Column("publisher_id", String),
75
+ Column("publisher_org", String),
76
+ Column("source_agent_id", String),
77
+ Column("clone_count", Integer, default=0),
78
+ Column("published_at", String),
79
+ Column("updated_at", String),
80
+ Column("status", String),
81
+ )
82
+
83
+ def create_table(self):
84
+ """Create the hub_agents table in the database."""
85
+ if not inspect(self.engine).has_table(self.table_name):
86
+ self.metadata.create_all(self.engine)
87
+ logger.info(f"Hub store table '{self.table_name}' created successfully.")
88
+ else:
89
+ logger.info(f"Hub store table '{self.table_name}' already exists.")
90
+ self.ensure_columns()
91
+
92
+ def ensure_columns(self):
93
+ """Add new columns to existing tables if they don't exist."""
94
+ from sqlalchemy import text, inspect as sa_inspect
95
+
96
+ inspector = sa_inspect(self.engine)
97
+ existing = [
98
+ c["name"]
99
+ for c in inspector.get_columns(self.table_name, schema=self.db_schema)
100
+ ]
101
+ new_cols: dict[str, str] = {
102
+ "sub_agents_snapshot": "VARCHAR",
103
+ "loop_max_iterations": "VARCHAR",
104
+ "loop_exit_instruction": "VARCHAR",
105
+ "agent_skills": "VARCHAR",
106
+ }
107
+ with self.engine.begin() as conn:
108
+ for col, typ in new_cols.items():
109
+ if col not in existing:
110
+ schema_prefix = f'"{self.db_schema}".' if self.db_schema else ""
111
+ conn.execute(
112
+ text(
113
+ f'ALTER TABLE {schema_prefix}"{self.table_name}" ADD COLUMN "{col}" {typ}'
114
+ )
115
+ )
116
+
117
+ def get_list(self, query: Any) -> Any:
118
+ """Execute a query and return all rows."""
119
+ with self.engine.connect() as conn:
120
+ result = conn.execute(query)
121
+ return result.fetchall()
File without changes
@@ -0,0 +1,181 @@
1
+ from apowerb.configs.th2logger import setup_logging
2
+ import os
3
+ from datetime import datetime
4
+ from typing import Annotated
5
+
6
+ from fastapi import Depends, HTTPException, status
7
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
8
+
9
+ from jose import JWTError, jwt
10
+ from sqlalchemy import select
11
+ from sqlalchemy.ext.asyncio import AsyncSession
12
+
13
+ from apowerb.configs.settings import get_settings
14
+ from apowerb.helpers.database import get_db
15
+ from apowerb.helpers.security import get_algorithm, get_secret_key
16
+ from apowerb.models import User as UserModel
17
+ from apowerb.users import schemas as user_schemas
18
+
19
+ logger = setup_logging(__name__)
20
+
21
+ BYPASS_AUTH = os.environ.get("BYPASS_AUTH", "").lower() == "true"
22
+
23
+ settings = get_settings()
24
+ # Make authentication optional
25
+ security = HTTPBearer(auto_error=False)
26
+ DBSessionDep = Annotated[AsyncSession, Depends(get_db)]
27
+
28
+
29
+ async def get_current_user(
30
+ credentials: HTTPAuthorizationCredentials | None = Depends(security),
31
+ db: AsyncSession = Depends(get_db),
32
+ ) -> user_schemas.User:
33
+ if BYPASS_AUTH:
34
+ logger.warning("AUTH BYPASS ACTIVE - Using fake user")
35
+
36
+ # Return fake user with ALL required fields
37
+ return user_schemas.User(
38
+ user_id=1,
39
+ email="test@example.com",
40
+ role="USER",
41
+ first_name="Test",
42
+ last_name="User",
43
+ onboarding_completed=True,
44
+ created_at=datetime.now(),
45
+ updated_at=datetime.now(),
46
+ )
47
+
48
+ if credentials is None:
49
+ logger.debug("No credentials provided for request")
50
+ raise HTTPException(
51
+ status_code=status.HTTP_401_UNAUTHORIZED,
52
+ detail="Not authenticated",
53
+ headers={"WWW-Authenticate": "Bearer"},
54
+ )
55
+
56
+ token = credentials.credentials
57
+ # La clé est résolue AVANT le try : une configuration manquante doit
58
+ # remonter telle quelle, pas se faire convertir en « identifiants
59
+ # invalides » par le except ci-dessous.
60
+ secret = get_secret_key()
61
+ try:
62
+ payload = jwt.decode(
63
+ token,
64
+ secret,
65
+ algorithms=[get_algorithm()],
66
+ )
67
+ # H1 — Reject any token that isn't explicitly an access token.
68
+ # Refresh tokens (30-day cookie) and long-lived agent_refresh tokens
69
+ # (90-day Mage schedule tokens) MUST NOT unlock user-scope endpoints.
70
+ token_type = payload.get("type")
71
+ if token_type != "access":
72
+ logger.warning(
73
+ "Rejected non-access token on protected endpoint (type=%s)",
74
+ token_type,
75
+ )
76
+ raise HTTPException(
77
+ status_code=status.HTTP_401_UNAUTHORIZED,
78
+ detail="Invalid token type. Access token required.",
79
+ headers={"WWW-Authenticate": "Bearer"},
80
+ )
81
+ email: str = payload.get("sub")
82
+ if email is None:
83
+ raise HTTPException(
84
+ status_code=status.HTTP_401_UNAUTHORIZED,
85
+ detail="Could not validate credentials",
86
+ headers={"WWW-Authenticate": "Bearer"},
87
+ )
88
+ except JWTError as e:
89
+ logger.warning("JWT decode failed: %s", type(e).__name__)
90
+ raise HTTPException(
91
+ status_code=status.HTTP_401_UNAUTHORIZED,
92
+ detail="Could not validate credentials",
93
+ headers={"WWW-Authenticate": "Bearer"},
94
+ )
95
+
96
+ result = await db.execute(select(UserModel).where(UserModel.email == email))
97
+ user = result.scalar_one_or_none()
98
+
99
+ if user is None:
100
+ raise HTTPException(
101
+ status_code=status.HTTP_401_UNAUTHORIZED,
102
+ detail="User not found",
103
+ headers={"WWW-Authenticate": "Bearer"},
104
+ )
105
+
106
+ return user_schemas.User(
107
+ user_id=user.user_id,
108
+ email=user.email,
109
+ role=user.role.value if hasattr(user.role, "value") else str(user.role),
110
+ first_name=user.first_name,
111
+ last_name=user.last_name,
112
+ username=user.username,
113
+ full_name=user.full_name,
114
+ avatar_url=user.avatar_url,
115
+ plan=user.plan,
116
+ stripe_customer_id=user.stripe_customer_id,
117
+ mfa_enabled=getattr(user, "mfa_enabled", False) or False,
118
+ onboarding_completed=getattr(user, "onboarding_completed", False) or False,
119
+ created_at=user.created_at,
120
+ updated_at=user.updated_at,
121
+ )
122
+
123
+
124
+ async def get_optional_user(
125
+ credentials: HTTPAuthorizationCredentials | None = Depends(security),
126
+ db: AsyncSession = Depends(get_db),
127
+ ) -> user_schemas.User | None:
128
+ """Like get_current_user but returns None instead of 401 for unauthenticated requests."""
129
+ if credentials is None:
130
+ return None
131
+ token = credentials.credentials
132
+ secret = get_secret_key()
133
+ try:
134
+ payload = jwt.decode(
135
+ token,
136
+ secret,
137
+ algorithms=[get_algorithm()],
138
+ )
139
+ # H1 — Only accept explicit access tokens in optional-auth paths too.
140
+ if payload.get("type") != "access":
141
+ return None
142
+ email: str | None = payload.get("sub")
143
+ if email is None:
144
+ return None
145
+ except JWTError:
146
+ return None
147
+ result = await db.execute(select(UserModel).where(UserModel.email == email))
148
+ user = result.scalar_one_or_none()
149
+ if user is None:
150
+ return None
151
+ return user_schemas.User(
152
+ user_id=user.user_id,
153
+ email=user.email,
154
+ role=user.role.value if hasattr(user.role, "value") else str(user.role),
155
+ first_name=user.first_name,
156
+ last_name=user.last_name,
157
+ username=user.username,
158
+ full_name=user.full_name,
159
+ avatar_url=user.avatar_url,
160
+ plan=user.plan,
161
+ stripe_customer_id=user.stripe_customer_id,
162
+ mfa_enabled=getattr(user, "mfa_enabled", False) or False,
163
+ onboarding_completed=getattr(user, "onboarding_completed", False) or False,
164
+ created_at=user.created_at,
165
+ updated_at=user.updated_at,
166
+ )
167
+
168
+
169
+ async def get_admin_user(
170
+ current_user: user_schemas.User = Depends(get_current_user),
171
+ ) -> user_schemas.User:
172
+ if BYPASS_AUTH:
173
+ logger.warning("ADMIN BYPASS ACTIVE")
174
+ current_user.role = "ADMIN"
175
+ return current_user
176
+
177
+ if getattr(current_user, "role", None) != "ADMIN":
178
+ raise HTTPException(
179
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions"
180
+ )
181
+ return current_user
@@ -0,0 +1,7 @@
1
+ class InvalidCredentials(Exception):
2
+ pass
3
+
4
+
5
+ class EmailNotVerified(Exception):
6
+ """Raised at login when the account email is not verified yet."""
7
+ pass