Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions config/telescope.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,39 @@
*/

'watchers' => [
/*
* The AI watcher records summary information about Laravel AI runs by default.
* Prompt content, messages, tool arguments, tool results, and raw provider
* payloads are opt-in and are still redacted and size limited. Lifecycle
* summaries are capped to prevent large looping runs from growing without bound.
*/
Watchers\AiWatcher::class => [
'enabled' => env('TELESCOPE_AI_WATCHER', true),
'content' => false,
'messages' => false,
'tool_arguments' => false,
'tool_results' => false,
'raw' => false,
'size_limit' => 64,
'max_steps' => 50,
'max_tools' => 50,
'max_failovers' => 25,
'max_approvals' => 25,
'hidden' => [
'password',
'password_confirmation',
'token',
'access_token',
'refresh_token',
'secret',
'api-key',
'api_key',
'apiKey',
'authorization',
'cookie',
],
],

Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true),

Watchers\CacheWatcher::class => [
Expand Down
2 changes: 1 addition & 1 deletion dist/app.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 54 additions & 54 deletions dist/app.js

Large diffs are not rendered by default.

19 changes: 16 additions & 3 deletions resources/js/components/IndexScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,10 @@ export default {
* Update the existing entries if needed.
*/
updateEntries(){
if (this.resource !== 'jobs') return;
if (! this.pendingEntryStatuses().length) return;

this.updateEntriesTimeout = setTimeout(() => {
let uuids = _.chain(this.entries).filter(entry => entry.content.status === 'pending').map('id').value();
let uuids = _.chain(this.entries).filter(entry => this.isPendingEntry(entry)).map('id').value();

if (uuids.length) {
axios.post(Telescope.basePath + '/telescope-api/' + this.resource, {
Expand All @@ -238,7 +238,7 @@ export default {
this.entries = _.map(this.entries, entry => {
if (!_.includes(uuids, entry.id)) return entry;

return _.find(response.data.entries, {id: entry.id});
return _.find(response.data.entries, {id: entry.id}) || entry;
});
})
}
Expand All @@ -248,6 +248,19 @@ export default {
},


pendingEntryStatuses(){
return {
jobs: ['pending'],
ai: ['running', 'waiting_for_approval'],
}[this.resource] || [];
},


isPendingEntry(entry){
return _.includes(this.pendingEntryStatuses(), _.get(entry, 'content.status'));
},


/**
* Focus on the search input when "/" key is hit.
*/
Expand Down
22 changes: 18 additions & 4 deletions resources/js/components/PreviewScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export default {

methods: {
prepareEntry() {
clearTimeout(this.updateEntryTimeout);

document.title = this.title + " - Telescope";
this.ready = false;

Expand Down Expand Up @@ -106,8 +108,7 @@ export default {
* Update the existing entry if needed.
*/
updateEntry(){
if (this.resource != 'jobs') return;
if (this.entry.content.status !== 'pending') return;
if (! this.isPendingEntry(this.entry)) return;

this.updateEntryTimeout = setTimeout(() => {
this.loadEntry((response) => {
Expand All @@ -118,10 +119,23 @@ export default {
this.$parent.batch = response.data.batch;

this.ready = true;
});

this.updateEntry();
this.updateEntry();
});
}, this.updateEntryTimer);
},


pendingEntryStatuses(){
return {
jobs: ['pending'],
ai: ['running', 'waiting_for_approval'],
}[this.resource] || [];
},


isPendingEntry(entry){
return entry && _.includes(this.pendingEntryStatuses(), _.get(entry, 'content.status'));
}
}
}
Expand Down
71 changes: 70 additions & 1 deletion resources/js/components/RelatedEntries.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export default {
this.currentTab = 'models'
} else if (this.jobs.length) {
this.currentTab = 'jobs'
} else if (this.ai.length) {
this.currentTab = 'ai'
} else if (this.mails.length) {
this.currentTab = 'mails'
} else if (this.notifications.length) {
Expand All @@ -74,6 +76,10 @@ export default {
if(window.history.replaceState) {
window.history.replaceState(null, null, '#' + this.currentTab);
}
},

aiStatusLabel(status) {
return status ? status.replace(/_/g, ' ') : 'unknown';
}
},

Expand Down Expand Up @@ -113,6 +119,10 @@ export default {
return _.filter(this.batch, {type: 'job'});
},

ai() {
return _.filter(this.batch, {type: 'ai'});
},

events() {
return _.filter(this.batch, {type: 'event'});
},
Expand Down Expand Up @@ -157,6 +167,7 @@ export default {
{title: "Models", type: "models", count: this.models.length},
{title: "Gates", type: "gates", count: this.gates.length},
{title: "Jobs", type: "jobs", count: this.jobs.length},
{title: "AI", type: "ai", count: this.ai.length},
{title: "Mail", type: "mails", count: this.mails.length},
{title: "Notifications", type: "notifications", count: this.notifications.length},
{title: "Events", type: "events", count: this.events.length},
Expand All @@ -171,7 +182,7 @@ export default {
},

dropdownTabs(){
return _.slice(this.tabs, 7, 10);
return _.slice(this.tabs, 7);
},

dropdownTabSelected(){
Expand Down Expand Up @@ -482,6 +493,64 @@ export default {
</tbody>
</table>

<!-- Related AI Runs -->
<table class="table table-hover mb-0" v-show="currentTab == 'ai' && ai.length">
<thead>
<tr>
<th>Agent</th>
<th scope="col">Status</th>
<th class="text-right">Steps</th>
<th class="text-right">Tools</th>
<th></th>
</tr>
</thead>

<tbody>
<tr v-for="entry in ai">
<td>
<span :title="entry.content.agent">{{ truncate(entry.content.agent || '-', 68) }}</span
><br />
<small class="text-muted">
Provider: {{ entry.content.provider || '-' }} | Model:
{{ truncate(entry.content.model || '-', 40) }}
</small>
</td>

<td class="table-fit">
<span class="badge" :class="'badge-' + aiStatusClass(entry.content.status)">
{{ aiStatusLabel(entry.content.status) }}
</span>
</td>

<td class="table-fit text-right text-muted">
{{ entry.content.step_count || 0 }}
</td>

<td class="table-fit text-right text-muted">
{{ entry.content.tool_count || 0 }}
</td>

<td class="table-fit">
<router-link
:to="{
name: 'ai-preview',
params: { id: entry.id },
}"
class="control-action"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z"
clip-rule="evenodd"
/>
</svg>
</router-link>
</td>
</tr>
</tbody>
</table>

<!-- Related Events -->
<table class="table table-hover mb-0" v-show="currentTab == 'events' && events.length">
<thead>
Expand Down
9 changes: 9 additions & 0 deletions resources/js/mixins/entriesStyles.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
export default {
methods: {
aiStatusClass(status) {
if (status === 'running') return 'info';
if (status === 'completed') return 'success';
if (status === 'waiting_for_approval') return 'warning';
if (status === 'failed') return 'danger';

return 'secondary';
},

cacheActionTypeClass(type) {
if (type === 'hit') return 'success';
if (type === 'set') return 'info';
Expand Down
14 changes: 14 additions & 0 deletions resources/js/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import views from './screens/views/index.vue';
import viewsPreview from './screens/views/preview.vue';
import clientRequests from './screens/client-requests/index.vue';
import clientRequestsPreview from './screens/client-requests/preview.vue';
import ai from './screens/ai/index.vue';
import aiPreview from './screens/ai/preview.vue';

export default [
{ path: '/', redirect: '/requests' },
Expand Down Expand Up @@ -253,4 +255,16 @@ export default [
name: 'client-requests',
component: clientRequests,
},

{
path: '/ai/:id',
name: 'ai-preview',
component: aiPreview,
},

{
path: '/ai',
name: 'ai',
component: ai,
},
];
71 changes: 71 additions & 0 deletions resources/js/screens/ai/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script type="text/ecmascript-6">
import StylesMixin from './../../mixins/entriesStyles';

export default {
mixins: [
StylesMixin,
],

methods: {
statusLabel(status) {
return status ? status.replace(/_/g, ' ') : 'unknown';
},
},
}
</script>

<template>
<index-screen title="AI" resource="ai">
<tr slot="table-header">
<th scope="col">Agent</th>
<th scope="col">Status</th>
<th scope="col">Happened</th>
<th scope="col"></th>
</tr>

<template slot="row" slot-scope="slotProps">
<td>
<span :title="slotProps.entry.content.agent">
{{ truncate(slotProps.entry.content.agent || '-', 68) }}
</span>
<br />
<small class="text-muted">
Provider: {{ slotProps.entry.content.provider || '-' }} | Model:
{{ truncate(slotProps.entry.content.model || '-', 40) }}
</small>
</td>

<td class="table-fit">
<span class="badge" :class="'badge-' + aiStatusClass(slotProps.entry.content.status)">
{{ statusLabel(slotProps.entry.content.status) }}
</span>
</td>

<td
class="table-fit text-muted"
:data-timeago="slotProps.entry.created_at"
:title="slotProps.entry.created_at"
>
{{ timeAgo(slotProps.entry.created_at) }}
</td>

<td class="table-fit">
<router-link
:to="{
name: 'ai-preview',
params: { id: slotProps.entry.id },
}"
class="control-action"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z"
clip-rule="evenodd"
/>
</svg>
</router-link>
</td>
</template>
</index-screen>
</template>
Loading