| @@ -21,6 +21,7 @@ class LangfuseConfig(BaseTracingConfig): | |||
| """ | |||
| public_key: str | |||
| secret_key: str | |||
| project_key: str | |||
| host: str = 'https://api.langfuse.com' | |||
| @field_validator("host") | |||
| @@ -419,3 +419,11 @@ class LangFuseDataTrace(BaseTraceInstance): | |||
| except Exception as e: | |||
| logger.debug(f"LangFuse API check failed: {str(e)}") | |||
| raise ValueError(f"LangFuse API check failed: {str(e)}") | |||
| def get_project_key(self): | |||
| try: | |||
| projects = self.langfuse_client.client.projects.get() | |||
| return projects.data[0].id | |||
| except Exception as e: | |||
| logger.debug(f"LangFuse get project key failed: {str(e)}") | |||
| raise ValueError(f"LangFuse get project key failed: {str(e)}") | |||
| @@ -38,7 +38,7 @@ provider_config_map = { | |||
| TracingProviderEnum.LANGFUSE.value: { | |||
| 'config_class': LangfuseConfig, | |||
| 'secret_keys': ['public_key', 'secret_key'], | |||
| 'other_keys': ['host'], | |||
| 'other_keys': ['host', 'project_key'], | |||
| 'trace_instance': LangFuseDataTrace | |||
| }, | |||
| TracingProviderEnum.LANGSMITH.value: { | |||
| @@ -123,7 +123,6 @@ class OpsTraceManager: | |||
| for key in other_keys: | |||
| new_config[key] = decrypt_tracing_config.get(key, "") | |||
| return config_class(**new_config).model_dump() | |||
| @classmethod | |||
| @@ -252,6 +251,19 @@ class OpsTraceManager: | |||
| tracing_config = config_type(**tracing_config) | |||
| return trace_instance(tracing_config).api_check() | |||
| @staticmethod | |||
| def get_trace_config_project_key(tracing_config: dict, tracing_provider: str): | |||
| """ | |||
| get trace config is project key | |||
| :param tracing_config: tracing config | |||
| :param tracing_provider: tracing provider | |||
| :return: | |||
| """ | |||
| config_type, trace_instance = provider_config_map[tracing_provider]['config_class'], \ | |||
| provider_config_map[tracing_provider]['trace_instance'] | |||
| tracing_config = config_type(**tracing_config) | |||
| return trace_instance(tracing_config).get_project_key() | |||
| class TraceTask: | |||
| def __init__( | |||
| @@ -22,6 +22,10 @@ class OpsService: | |||
| # decrypt_token and obfuscated_token | |||
| tenant_id = db.session.query(App).filter(App.id == app_id).first().tenant_id | |||
| decrypt_tracing_config = OpsTraceManager.decrypt_tracing_config(tenant_id, tracing_provider, trace_config_data.tracing_config) | |||
| if tracing_provider == 'langfuse' and ('project_key' not in decrypt_tracing_config or not decrypt_tracing_config.get('project_key')): | |||
| project_key = OpsTraceManager.get_trace_config_project_key(decrypt_tracing_config, tracing_provider) | |||
| decrypt_tracing_config['project_key'] = project_key | |||
| decrypt_tracing_config = OpsTraceManager.obfuscated_decrypt_token(tracing_provider, decrypt_tracing_config) | |||
| trace_config_data.tracing_config = decrypt_tracing_config | |||
| @@ -37,7 +41,7 @@ class OpsService: | |||
| :param tracing_config: tracing config | |||
| :return: | |||
| """ | |||
| if tracing_provider not in provider_config_map.keys() and tracing_provider != None: | |||
| if tracing_provider not in provider_config_map.keys() and tracing_provider: | |||
| return {"error": f"Invalid tracing provider: {tracing_provider}"} | |||
| config_class, other_keys = provider_config_map[tracing_provider]['config_class'], \ | |||
| @@ -51,6 +55,9 @@ class OpsService: | |||
| if not OpsTraceManager.check_trace_config_is_effective(tracing_config, tracing_provider): | |||
| return {"error": "Invalid Credentials"} | |||
| # get project key | |||
| project_key = OpsTraceManager.get_trace_config_project_key(tracing_config, tracing_provider) | |||
| # check if trace config already exists | |||
| trace_config_data: TraceAppConfig = db.session.query(TraceAppConfig).filter( | |||
| TraceAppConfig.app_id == app_id, TraceAppConfig.tracing_provider == tracing_provider | |||
| @@ -62,6 +69,8 @@ class OpsService: | |||
| # get tenant id | |||
| tenant_id = db.session.query(App).filter(App.id == app_id).first().tenant_id | |||
| tracing_config = OpsTraceManager.encrypt_tracing_config(tenant_id, tracing_provider, tracing_config) | |||
| if tracing_provider == 'langfuse': | |||
| tracing_config['project_key'] = project_key | |||
| trace_config_data = TraceAppConfig( | |||
| app_id=app_id, | |||
| tracing_provider=tracing_provider, | |||
| @@ -6,6 +6,7 @@ import { TracingProvider } from './type' | |||
| import cn from '@/utils/classnames' | |||
| import { LangfuseIconBig, LangsmithIconBig } from '@/app/components/base/icons/src/public/tracing' | |||
| import { Settings04 } from '@/app/components/base/icons/src/vender/line/general' | |||
| import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general' | |||
| const I18N_PREFIX = 'app.tracing' | |||
| @@ -13,6 +14,7 @@ type Props = { | |||
| type: TracingProvider | |||
| readOnly: boolean | |||
| isChosen: boolean | |||
| Config: any | |||
| onChoose: () => void | |||
| hasConfigured: boolean | |||
| onConfig: () => void | |||
| @@ -29,6 +31,7 @@ const ProviderPanel: FC<Props> = ({ | |||
| type, | |||
| readOnly, | |||
| isChosen, | |||
| Config, | |||
| onChoose, | |||
| hasConfigured, | |||
| onConfig, | |||
| @@ -41,6 +44,14 @@ const ProviderPanel: FC<Props> = ({ | |||
| onConfig() | |||
| }, [onConfig]) | |||
| const viewBtnClick = useCallback((e: React.MouseEvent) => { | |||
| e.preventDefault() | |||
| e.stopPropagation() | |||
| const url = `${Config?.host}/project/${Config?.project_key}` | |||
| window.open(url, '_blank', 'noopener,noreferrer') | |||
| }, [Config?.host, Config?.project_key]) | |||
| const handleChosen = useCallback((e: React.MouseEvent) => { | |||
| e.stopPropagation() | |||
| if (isChosen || !hasConfigured || readOnly) | |||
| @@ -58,12 +69,20 @@ const ProviderPanel: FC<Props> = ({ | |||
| {isChosen && <div className='ml-1 flex items-center h-4 px-1 rounded-[4px] border border-primary-500 leading-4 text-xs font-medium text-primary-500 uppercase '>{t(`${I18N_PREFIX}.inUse`)}</div>} | |||
| </div> | |||
| {!readOnly && ( | |||
| <div | |||
| className='flex px-2 items-center h-6 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer text-gray-700 space-x-1' | |||
| onClick={handleConfigBtnClick} | |||
| > | |||
| <Settings04 className='w-3 h-3' /> | |||
| <div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div> | |||
| <div className={'flex justify-between items-center space-x-1'}> | |||
| {hasConfigured && ( | |||
| <div className='flex px-2 items-center h-6 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer text-gray-700 space-x-1' onClick={viewBtnClick} > | |||
| <View className='w-3 h-3'/> | |||
| <div className='text-xs font-medium'>{t(`${I18N_PREFIX}.view`)}</div> | |||
| </div> | |||
| )} | |||
| <div | |||
| className='flex px-2 items-center h-6 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer text-gray-700 space-x-1' | |||
| onClick={handleConfigBtnClick} | |||
| > | |||
| <Settings04 className='w-3 h-3' /> | |||
| <div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div> | |||
| </div> | |||
| </div> | |||
| )} | |||
| @@ -95,6 +95,7 @@ const translation = { | |||
| title: 'Tracing app performance', | |||
| description: 'Configuring a Third-Party LLMOps provider and tracing app performance.', | |||
| config: 'Config', | |||
| view: 'View', | |||
| collapse: 'Collapse', | |||
| expand: 'Expand', | |||
| tracing: 'Tracing', | |||
| @@ -94,6 +94,7 @@ const translation = { | |||
| title: '追踪应用性能', | |||
| description: '配置第三方 LLMOps 提供商并跟踪应用程序性能。', | |||
| config: '配置', | |||
| view: '查看', | |||
| collapse: '折叠', | |||
| expand: '展开', | |||
| tracing: '追踪', | |||
| @@ -90,6 +90,7 @@ const translation = { | |||
| title: '追蹤應用程式效能', | |||
| description: '配置第三方LLMOps提供商並追蹤應用程式效能。', | |||
| config: '配置', | |||
| view: '查看', | |||
| collapse: '收起', | |||
| expand: '展開', | |||
| tracing: '追蹤', | |||