workflow_statistic.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. from datetime import datetime
  2. from decimal import Decimal
  3. import pytz
  4. import sqlalchemy as sa
  5. from flask import jsonify
  6. from flask_login import current_user
  7. from flask_restx import Resource, reqparse
  8. from controllers.console import api
  9. from controllers.console.app.wraps import get_app_model
  10. from controllers.console.wraps import account_initialization_required, setup_required
  11. from extensions.ext_database import db
  12. from libs.helper import DatetimeString
  13. from libs.login import login_required
  14. from models.enums import WorkflowRunTriggeredFrom
  15. from models.model import AppMode
  16. class WorkflowDailyRunsStatistic(Resource):
  17. @setup_required
  18. @login_required
  19. @account_initialization_required
  20. @get_app_model
  21. def get(self, app_model):
  22. account = current_user
  23. parser = reqparse.RequestParser()
  24. parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
  25. parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
  26. args = parser.parse_args()
  27. timezone = pytz.timezone(account.timezone)
  28. utc_timezone = pytz.utc
  29. # Check if querying "today" data
  30. is_today = False
  31. if args["start"] and args["end"]:
  32. start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
  33. end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
  34. now = datetime.now(timezone)
  35. today_date = now.date()
  36. if start_datetime.date() == today_date and end_datetime.date() == today_date:
  37. is_today = True
  38. # Choose time truncation based on whether it's today
  39. # Use TO_CHAR with HH24 to ensure 24-hour format (not 12-hour)
  40. if is_today:
  41. time_trunc = "TO_CHAR(DATE_TRUNC('hour', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz), 'YYYY-MM-DD HH24:MI:SS')"
  42. else:
  43. time_trunc = "TO_CHAR(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz), 'YYYY-MM-DD')"
  44. sql_query = f"""SELECT
  45. {time_trunc} AS date,
  46. COUNT(id) AS runs
  47. FROM
  48. workflow_runs
  49. WHERE
  50. app_id = :app_id
  51. AND triggered_from = :triggered_from"""
  52. arg_dict = {
  53. "tz": account.timezone,
  54. "app_id": app_model.id,
  55. "triggered_from": WorkflowRunTriggeredFrom.APP_RUN.value,
  56. }
  57. if args["start"]:
  58. start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
  59. start_datetime = start_datetime.replace(second=0)
  60. start_datetime_timezone = timezone.localize(start_datetime)
  61. start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
  62. sql_query += " AND created_at >= :start"
  63. arg_dict["start"] = start_datetime_utc
  64. if args["end"]:
  65. end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
  66. end_datetime = end_datetime.replace(second=0)
  67. end_datetime_timezone = timezone.localize(end_datetime)
  68. end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
  69. sql_query += " AND created_at < :end"
  70. arg_dict["end"] = end_datetime_utc
  71. sql_query += " GROUP BY date ORDER BY date"
  72. response_data = []
  73. with db.engine.begin() as conn:
  74. rs = conn.execute(sa.text(sql_query), arg_dict)
  75. for i in rs:
  76. # date is already formatted string from TO_CHAR
  77. response_data.append({"date": i.date, "runs": i.runs})
  78. return jsonify({"data": response_data})
  79. class WorkflowDailyTerminalsStatistic(Resource):
  80. @setup_required
  81. @login_required
  82. @account_initialization_required
  83. @get_app_model
  84. def get(self, app_model):
  85. account = current_user
  86. parser = reqparse.RequestParser()
  87. parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
  88. parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
  89. args = parser.parse_args()
  90. timezone = pytz.timezone(account.timezone)
  91. utc_timezone = pytz.utc
  92. # Check if querying "today" data
  93. is_today = False
  94. if args["start"] and args["end"]:
  95. start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
  96. end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
  97. now = datetime.now(timezone)
  98. today_date = now.date()
  99. if start_datetime.date() == today_date and end_datetime.date() == today_date:
  100. is_today = True
  101. # Choose time truncation based on whether it's today
  102. # Use TO_CHAR with HH24 to ensure 24-hour format (not 12-hour)
  103. if is_today:
  104. time_trunc = "TO_CHAR(DATE_TRUNC('hour', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz), 'YYYY-MM-DD HH24:MI:SS')"
  105. else:
  106. time_trunc = "TO_CHAR(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz), 'YYYY-MM-DD')"
  107. sql_query = f"""SELECT
  108. {time_trunc} AS date,
  109. COUNT(DISTINCT workflow_runs.created_by) AS terminal_count
  110. FROM
  111. workflow_runs
  112. WHERE
  113. app_id = :app_id
  114. AND triggered_from = :triggered_from"""
  115. arg_dict = {
  116. "tz": account.timezone,
  117. "app_id": app_model.id,
  118. "triggered_from": WorkflowRunTriggeredFrom.APP_RUN.value,
  119. }
  120. if args["start"]:
  121. start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
  122. start_datetime = start_datetime.replace(second=0)
  123. start_datetime_timezone = timezone.localize(start_datetime)
  124. start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
  125. sql_query += " AND created_at >= :start"
  126. arg_dict["start"] = start_datetime_utc
  127. if args["end"]:
  128. end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
  129. end_datetime = end_datetime.replace(second=0)
  130. end_datetime_timezone = timezone.localize(end_datetime)
  131. end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
  132. sql_query += " AND created_at < :end"
  133. arg_dict["end"] = end_datetime_utc
  134. sql_query += " GROUP BY date ORDER BY date"
  135. response_data = []
  136. with db.engine.begin() as conn:
  137. rs = conn.execute(sa.text(sql_query), arg_dict)
  138. for i in rs:
  139. # date is already formatted string from TO_CHAR
  140. response_data.append({"date": i.date, "terminal_count": i.terminal_count})
  141. return jsonify({"data": response_data})
  142. class WorkflowDailyTokenCostStatistic(Resource):
  143. @setup_required
  144. @login_required
  145. @account_initialization_required
  146. @get_app_model
  147. def get(self, app_model):
  148. account = current_user
  149. parser = reqparse.RequestParser()
  150. parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
  151. parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
  152. args = parser.parse_args()
  153. timezone = pytz.timezone(account.timezone)
  154. utc_timezone = pytz.utc
  155. # Check if querying "today" data
  156. is_today = False
  157. if args["start"] and args["end"]:
  158. start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
  159. end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
  160. now = datetime.now(timezone)
  161. today_date = now.date()
  162. if start_datetime.date() == today_date and end_datetime.date() == today_date:
  163. is_today = True
  164. # Choose time truncation based on whether it's today
  165. # Use TO_CHAR with HH24 to ensure 24-hour format (not 12-hour)
  166. if is_today:
  167. time_trunc = "TO_CHAR(DATE_TRUNC('hour', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz), 'YYYY-MM-DD HH24:MI:SS')"
  168. else:
  169. time_trunc = "TO_CHAR(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz), 'YYYY-MM-DD')"
  170. sql_query = f"""SELECT
  171. {time_trunc} AS date,
  172. SUM(workflow_runs.total_tokens) AS token_count
  173. FROM
  174. workflow_runs
  175. WHERE
  176. app_id = :app_id
  177. AND triggered_from = :triggered_from"""
  178. arg_dict = {
  179. "tz": account.timezone,
  180. "app_id": app_model.id,
  181. "triggered_from": WorkflowRunTriggeredFrom.APP_RUN.value,
  182. }
  183. if args["start"]:
  184. start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
  185. start_datetime = start_datetime.replace(second=0)
  186. start_datetime_timezone = timezone.localize(start_datetime)
  187. start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
  188. sql_query += " AND created_at >= :start"
  189. arg_dict["start"] = start_datetime_utc
  190. if args["end"]:
  191. end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
  192. end_datetime = end_datetime.replace(second=0)
  193. end_datetime_timezone = timezone.localize(end_datetime)
  194. end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
  195. sql_query += " AND created_at < :end"
  196. arg_dict["end"] = end_datetime_utc
  197. sql_query += " GROUP BY date ORDER BY date"
  198. response_data = []
  199. with db.engine.begin() as conn:
  200. rs = conn.execute(sa.text(sql_query), arg_dict)
  201. for i in rs:
  202. # date is already formatted string from TO_CHAR
  203. response_data.append(
  204. {
  205. "date": i.date,
  206. "token_count": i.token_count,
  207. }
  208. )
  209. return jsonify({"data": response_data})
  210. class WorkflowAverageAppInteractionStatistic(Resource):
  211. @setup_required
  212. @login_required
  213. @account_initialization_required
  214. @get_app_model(mode=[AppMode.WORKFLOW])
  215. def get(self, app_model):
  216. account = current_user
  217. parser = reqparse.RequestParser()
  218. parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
  219. parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
  220. args = parser.parse_args()
  221. timezone = pytz.timezone(account.timezone)
  222. utc_timezone = pytz.utc
  223. # Check if querying "today" data
  224. is_today = False
  225. if args["start"] and args["end"]:
  226. start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
  227. end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
  228. now = datetime.now(timezone)
  229. today_date = now.date()
  230. if start_datetime.date() == today_date and end_datetime.date() == today_date:
  231. is_today = True
  232. # Choose time truncation based on whether it's today
  233. # Use TO_CHAR with HH24 to ensure 24-hour format (not 12-hour)
  234. if is_today:
  235. time_trunc = "TO_CHAR(DATE_TRUNC('hour', c.created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz), 'YYYY-MM-DD HH24:MI:SS')"
  236. else:
  237. time_trunc = "TO_CHAR(DATE_TRUNC('day', c.created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz), 'YYYY-MM-DD')"
  238. sql_query = f"""SELECT
  239. AVG(sub.interactions) AS interactions,
  240. sub.date
  241. FROM
  242. (
  243. SELECT
  244. {time_trunc} AS date,
  245. c.created_by,
  246. COUNT(c.id) AS interactions
  247. FROM
  248. workflow_runs c
  249. WHERE
  250. c.app_id = :app_id
  251. AND c.triggered_from = :triggered_from
  252. {{{{start}}}}
  253. {{{{end}}}}
  254. GROUP BY
  255. date, c.created_by
  256. ) sub
  257. GROUP BY
  258. sub.date"""
  259. arg_dict = {
  260. "tz": account.timezone,
  261. "app_id": app_model.id,
  262. "triggered_from": WorkflowRunTriggeredFrom.APP_RUN.value,
  263. }
  264. if args["start"]:
  265. start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
  266. start_datetime = start_datetime.replace(second=0)
  267. start_datetime_timezone = timezone.localize(start_datetime)
  268. start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
  269. sql_query = sql_query.replace("{{start}}", " AND c.created_at >= :start")
  270. arg_dict["start"] = start_datetime_utc
  271. else:
  272. sql_query = sql_query.replace("{{start}}", "")
  273. if args["end"]:
  274. end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
  275. end_datetime = end_datetime.replace(second=0)
  276. end_datetime_timezone = timezone.localize(end_datetime)
  277. end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
  278. sql_query = sql_query.replace("{{end}}", " AND c.created_at < :end")
  279. arg_dict["end"] = end_datetime_utc
  280. else:
  281. sql_query = sql_query.replace("{{end}}", "")
  282. response_data = []
  283. with db.engine.begin() as conn:
  284. rs = conn.execute(sa.text(sql_query), arg_dict)
  285. for i in rs:
  286. # date is already formatted string from TO_CHAR
  287. response_data.append(
  288. {"date": i.date, "interactions": float(i.interactions.quantize(Decimal("0.01")))}
  289. )
  290. return jsonify({"data": response_data})
  291. api.add_resource(WorkflowDailyRunsStatistic, "/apps/<uuid:app_id>/workflow/statistics/daily-conversations")
  292. api.add_resource(WorkflowDailyTerminalsStatistic, "/apps/<uuid:app_id>/workflow/statistics/daily-terminals")
  293. api.add_resource(WorkflowDailyTokenCostStatistic, "/apps/<uuid:app_id>/workflow/statistics/token-costs")
  294. api.add_resource(
  295. WorkflowAverageAppInteractionStatistic, "/apps/<uuid:app_id>/workflow/statistics/average-app-interactions"
  296. )